Unify fishing surfaces and presentation
This commit is contained in:
parent
c596b2aee7
commit
070765741c
19 changed files with 1705 additions and 372 deletions
|
|
@ -3,6 +3,7 @@ extends Node3D
|
||||||
|
|
||||||
signal cast_completed
|
signal cast_completed
|
||||||
signal outcome_completed(outcome: StringName)
|
signal outcome_completed(outcome: StringName)
|
||||||
|
signal presentation_interrupted
|
||||||
|
|
||||||
enum VisualMode {
|
enum VisualMode {
|
||||||
NONE,
|
NONE,
|
||||||
|
|
@ -18,10 +19,6 @@ enum LineMode {
|
||||||
SLACK,
|
SLACK,
|
||||||
}
|
}
|
||||||
|
|
||||||
@export_category("Water")
|
|
||||||
@export var water_surface_offset_y: float = -0.72
|
|
||||||
@export_range(0.1, 10.0, 0.1) var pickup_distance_from_player: float = 5.5
|
|
||||||
|
|
||||||
@export_category("Fishing Line")
|
@export_category("Fishing Line")
|
||||||
@export_range(0.0, 0.5, 0.01) var slack_amount: float = 0.08
|
@export_range(0.0, 0.5, 0.01) var slack_amount: float = 0.08
|
||||||
@export_range(0.0, 2.0, 0.01) var minimum_slack: float = 0.08
|
@export_range(0.0, 2.0, 0.01) var minimum_slack: float = 0.08
|
||||||
|
|
@ -30,19 +27,20 @@ enum LineMode {
|
||||||
@export_range(0.1, 30.0, 0.1) var line_transition_speed: float = 7.0
|
@export_range(0.1, 30.0, 0.1) var line_transition_speed: float = 7.0
|
||||||
|
|
||||||
@export_category("Cast")
|
@export_category("Cast")
|
||||||
@export_range(0.1, 30.0, 0.1) var target_movement_smoothing: float = 14.0
|
|
||||||
@export_range(0.1, 3.0, 0.05) var cast_travel_duration: float = 0.65
|
@export_range(0.1, 3.0, 0.05) var cast_travel_duration: float = 0.65
|
||||||
@export_range(0.1, 5.0, 0.1) var cast_arc_height: float = 2.0
|
@export_range(0.1, 5.0, 0.1) var cast_arc_height: float = 2.0
|
||||||
@export_range(0.0, 90.0, 1.0) var maximum_rod_cock_degrees: float = 50.0
|
@export_range(0.0, 90.0, 1.0) var maximum_rod_cock_degrees: float = 50.0
|
||||||
@export_range(0.0, 60.0, 1.0) var rod_forward_swing_degrees: float = 22.0
|
@export_range(0.0, 60.0, 1.0) var rod_forward_swing_degrees: float = 22.0
|
||||||
@export_range(0.05, 1.0, 0.01) var rod_forward_swing_duration: float = 0.12
|
@export_range(0.05, 1.0, 0.01) var rod_forward_swing_duration: float = 0.12
|
||||||
@export_range(0.05, 1.0, 0.01) var rod_recovery_duration: float = 0.22
|
@export_range(0.05, 1.0, 0.01) var rod_recovery_duration: float = 0.22
|
||||||
@export var valid_target_material: Material
|
|
||||||
@export var invalid_target_material: Material
|
|
||||||
|
|
||||||
@onready var _target_marker: MeshInstance3D = %CastTargetMarker
|
@export_category("Bobber Idle")
|
||||||
@onready var _invalid_cross_a: MeshInstance3D = %InvalidCrossA
|
@export_range(0.0, 0.2, 0.005) var bobber_bob_amplitude: float = 0.035
|
||||||
@onready var _invalid_cross_b: MeshInstance3D = %InvalidCrossB
|
@export_range(0.1, 3.0, 0.05) var bobber_bob_cycles_per_second: float = 0.7
|
||||||
|
@export_range(0.0, 12.0, 0.5) var bobber_tilt_degrees: float = 4.0
|
||||||
|
@onready var _target_marker: Node3D = %CastTargetMarker
|
||||||
|
@onready var _valid_target_marker: MeshInstance3D = %ValidTargetMarker
|
||||||
|
@onready var _invalid_target_marker: Node3D = %InvalidTargetMarker
|
||||||
@onready var _bobber: MeshInstance3D = %Bobber
|
@onready var _bobber: MeshInstance3D = %Bobber
|
||||||
@onready var _line: MeshInstance3D = %FishingLine
|
@onready var _line: MeshInstance3D = %FishingLine
|
||||||
|
|
||||||
|
|
@ -53,44 +51,27 @@ var _current_sag: float = 0.0
|
||||||
var _rod: Node3D
|
var _rod: Node3D
|
||||||
var _rod_tip: Marker3D
|
var _rod_tip: Marker3D
|
||||||
var _rod_neutral_rotation: Vector3
|
var _rod_neutral_rotation: Vector3
|
||||||
var _target_goal: Vector3
|
var _cast_arrival_position: Vector3
|
||||||
var _cast_position: Vector3
|
var _bobber_surface_position: Vector3
|
||||||
var _pickup_position: Vector3
|
var _bobber_idle_elapsed: float = 0.0
|
||||||
var _active_tween: Tween
|
var _active_tween: Tween
|
||||||
var _rod_tween: Tween
|
var _rod_tween: Tween
|
||||||
var _bite_tween: Tween
|
var _bite_tween: Tween
|
||||||
var _default_water_surface_offset_y: float
|
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
_default_water_surface_offset_y = water_surface_offset_y
|
|
||||||
_line.mesh = _line_mesh
|
_line.mesh = _line_mesh
|
||||||
cleanup()
|
cleanup()
|
||||||
|
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
func _process(delta: float) -> void:
|
||||||
if _mode == VisualMode.AIMING:
|
if _mode == VisualMode.FISHING and _bobber.visible:
|
||||||
var weight: float = 1.0 - exp(-target_movement_smoothing * delta)
|
_bobber_idle_elapsed += delta
|
||||||
_target_marker.global_position = _target_marker.global_position.lerp(
|
_apply_bobber_idle_motion()
|
||||||
_target_goal,
|
|
||||||
weight
|
|
||||||
)
|
|
||||||
if _line_mode != LineMode.HIDDEN:
|
if _line_mode != LineMode.HIDDEN:
|
||||||
_update_line(delta)
|
_update_line(delta)
|
||||||
|
|
||||||
|
|
||||||
func get_water_surface_height() -> float:
|
|
||||||
return global_position.y + water_surface_offset_y
|
|
||||||
|
|
||||||
|
|
||||||
func set_water_surface_height(surface_height: float) -> void:
|
|
||||||
water_surface_offset_y = surface_height - global_position.y
|
|
||||||
|
|
||||||
|
|
||||||
func reset_water_surface_height() -> void:
|
|
||||||
water_surface_offset_y = _default_water_surface_offset_y
|
|
||||||
|
|
||||||
|
|
||||||
func set_line_mode(mode: LineMode) -> void:
|
func set_line_mode(mode: LineMode) -> void:
|
||||||
_line_mode = mode
|
_line_mode = mode
|
||||||
_line.visible = mode != LineMode.HIDDEN
|
_line.visible = mode != LineMode.HIDDEN
|
||||||
|
|
@ -103,6 +84,7 @@ func begin_aim(
|
||||||
rod_tip: Marker3D,
|
rod_tip: Marker3D,
|
||||||
rod: Node3D,
|
rod: Node3D,
|
||||||
minimum_target: Vector3,
|
minimum_target: Vector3,
|
||||||
|
target_normal: Vector3,
|
||||||
target_is_fishable: bool,
|
target_is_fishable: bool,
|
||||||
) -> void:
|
) -> void:
|
||||||
cleanup()
|
cleanup()
|
||||||
|
|
@ -113,8 +95,7 @@ func begin_aim(
|
||||||
_rod = rod
|
_rod = rod
|
||||||
_rod_tip = rod_tip
|
_rod_tip = rod_tip
|
||||||
_rod_neutral_rotation = _rod.rotation
|
_rod_neutral_rotation = _rod.rotation
|
||||||
_target_goal = minimum_target
|
_set_target_surface(minimum_target, target_normal)
|
||||||
_target_marker.global_position = minimum_target
|
|
||||||
_target_marker.scale = Vector3.ONE
|
_target_marker.scale = Vector3.ONE
|
||||||
_target_marker.visible = true
|
_target_marker.visible = true
|
||||||
_set_target_validity(target_is_fishable)
|
_set_target_validity(target_is_fishable)
|
||||||
|
|
@ -131,19 +112,37 @@ func update_rod_charge(charge: float) -> void:
|
||||||
|
|
||||||
func update_aim_target(
|
func update_aim_target(
|
||||||
target: Vector3,
|
target: Vector3,
|
||||||
|
target_normal: Vector3,
|
||||||
charge: float,
|
charge: float,
|
||||||
target_is_fishable: bool,
|
target_is_fishable: bool,
|
||||||
) -> void:
|
) -> void:
|
||||||
if _mode != VisualMode.AIMING:
|
if _mode != VisualMode.AIMING:
|
||||||
return
|
return
|
||||||
|
|
||||||
_target_goal = target
|
# The gameplay resolver already advances the horizontal target smoothly.
|
||||||
|
# Snap each sample to its resolved surface so interpolation can never draw
|
||||||
|
# the marker through a vertical shoreline between two valid endpoints.
|
||||||
|
_set_target_surface(target, target_normal)
|
||||||
var maximum_response: float = smoothstep(0.9, 1.0, clampf(charge, 0.0, 1.0))
|
var maximum_response: float = smoothstep(0.9, 1.0, clampf(charge, 0.0, 1.0))
|
||||||
_target_marker.scale = Vector3.ONE * lerpf(1.0, 1.15, maximum_response)
|
_target_marker.scale = Vector3.ONE * lerpf(1.0, 1.15, maximum_response)
|
||||||
_set_target_validity(target_is_fishable)
|
_set_target_validity(target_is_fishable)
|
||||||
|
|
||||||
|
|
||||||
func begin_cast(target: Vector3) -> void:
|
func _set_target_surface(target: Vector3, surface_normal: Vector3) -> void:
|
||||||
|
_target_marker.global_position = target
|
||||||
|
var resolved_normal: Vector3 = surface_normal.normalized()
|
||||||
|
if resolved_normal.is_zero_approx():
|
||||||
|
resolved_normal = Vector3.UP
|
||||||
|
_target_marker.global_basis = Basis(
|
||||||
|
Quaternion(Vector3.UP, resolved_normal)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func begin_cast(
|
||||||
|
target: Vector3,
|
||||||
|
blocked_landing_position: Vector3 = Vector3.ZERO,
|
||||||
|
cast_is_blocked: bool = false,
|
||||||
|
) -> void:
|
||||||
if _mode != VisualMode.AIMING or _rod_tip == null:
|
if _mode != VisualMode.AIMING or _rod_tip == null:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -152,8 +151,9 @@ func begin_cast(target: Vector3) -> void:
|
||||||
_target_marker.visible = false
|
_target_marker.visible = false
|
||||||
_bobber.visible = true
|
_bobber.visible = true
|
||||||
_bobber.scale = Vector3.ONE
|
_bobber.scale = Vector3.ONE
|
||||||
_cast_position = _with_water_height(target)
|
_cast_arrival_position = (
|
||||||
_pickup_position = _calculate_pickup_position(_cast_position)
|
blocked_landing_position if cast_is_blocked else target
|
||||||
|
)
|
||||||
|
|
||||||
var cast_start: Vector3 = _rod_tip.global_position
|
var cast_start: Vector3 = _rod_tip.global_position
|
||||||
_bobber.global_position = cast_start
|
_bobber.global_position = cast_start
|
||||||
|
|
@ -162,11 +162,15 @@ func begin_cast(target: Vector3) -> void:
|
||||||
_active_tween.set_trans(Tween.TRANS_SINE)
|
_active_tween.set_trans(Tween.TRANS_SINE)
|
||||||
_active_tween.set_ease(Tween.EASE_IN_OUT)
|
_active_tween.set_ease(Tween.EASE_IN_OUT)
|
||||||
_active_tween.tween_method(
|
_active_tween.tween_method(
|
||||||
_set_cast_sample.bind(cast_start, _cast_position),
|
_set_cast_sample.bind(cast_start, _cast_arrival_position),
|
||||||
0.0,
|
0.0,
|
||||||
1.0,
|
1.0,
|
||||||
cast_travel_duration
|
cast_travel_duration
|
||||||
)
|
)
|
||||||
|
if cast_is_blocked:
|
||||||
|
# Let the bobber visibly settle on the blocking surface before the
|
||||||
|
# normal invalid-cast return animation begins.
|
||||||
|
_active_tween.tween_interval(0.16)
|
||||||
_active_tween.finished.connect(_on_cast_tween_finished)
|
_active_tween.finished.connect(_on_cast_tween_finished)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -205,23 +209,15 @@ func show_withdrawal_position(world_position: Vector3) -> void:
|
||||||
return
|
return
|
||||||
|
|
||||||
_kill_active_tween()
|
_kill_active_tween()
|
||||||
_bobber.global_position = _with_water_height(world_position)
|
_bobber_surface_position = world_position
|
||||||
|
_apply_bobber_idle_motion()
|
||||||
|
|
||||||
|
|
||||||
func begin_reeling() -> void:
|
func begin_fight() -> void:
|
||||||
if _mode != VisualMode.FISHING:
|
if _mode != VisualMode.FISHING:
|
||||||
return
|
return
|
||||||
|
|
||||||
_kill_active_tween()
|
_kill_active_tween()
|
||||||
_cast_position = _with_water_height(_bobber.global_position)
|
|
||||||
|
|
||||||
|
|
||||||
func show_reel_position(world_position: Vector3, _input_held: bool) -> void:
|
|
||||||
if _mode != VisualMode.FISHING:
|
|
||||||
return
|
|
||||||
|
|
||||||
_kill_active_tween()
|
|
||||||
_bobber.global_position = _with_water_height(world_position)
|
|
||||||
|
|
||||||
|
|
||||||
func play_outcome(outcome: StringName) -> void:
|
func play_outcome(outcome: StringName) -> void:
|
||||||
|
|
@ -237,6 +233,12 @@ func play_outcome(outcome: StringName) -> void:
|
||||||
_kill_rod_tween()
|
_kill_rod_tween()
|
||||||
_restore_rod_neutral()
|
_restore_rod_neutral()
|
||||||
_mode = VisualMode.OUTCOME
|
_mode = VisualMode.OUTCOME
|
||||||
|
_bobber.rotation = Vector3.ZERO
|
||||||
|
if outcome == &"withdrawal":
|
||||||
|
# Withdrawal is a visible return, not an instant cancellation. Keep the
|
||||||
|
# bobber present even if the preceding reel update hid or reset it.
|
||||||
|
_bobber.visible = true
|
||||||
|
_bobber.scale = Vector3.ONE
|
||||||
_active_tween = create_tween()
|
_active_tween = create_tween()
|
||||||
_active_tween.set_trans(Tween.TRANS_QUAD)
|
_active_tween.set_trans(Tween.TRANS_QUAD)
|
||||||
_active_tween.set_ease(Tween.EASE_IN)
|
_active_tween.set_ease(Tween.EASE_IN)
|
||||||
|
|
@ -247,12 +249,7 @@ func play_outcome(outcome: StringName) -> void:
|
||||||
_target_marker.visible = false
|
_target_marker.visible = false
|
||||||
match outcome:
|
match outcome:
|
||||||
&"catch":
|
&"catch":
|
||||||
var catch_target: Vector3 = _bobber.global_position
|
_queue_bobber_return(0.35, 0.45)
|
||||||
if _rod_tip != null:
|
|
||||||
catch_target = _rod_tip.global_position
|
|
||||||
_active_tween.set_parallel(true)
|
|
||||||
_active_tween.tween_property(_bobber, "global_position", catch_target, 0.35)
|
|
||||||
_active_tween.tween_property(_bobber, "scale", Vector3.ZERO, 0.35)
|
|
||||||
&"escape":
|
&"escape":
|
||||||
var start_x: float = _bobber.global_position.x
|
var start_x: float = _bobber.global_position.x
|
||||||
_active_tween.tween_property(
|
_active_tween.tween_property(
|
||||||
|
|
@ -267,31 +264,9 @@ func play_outcome(outcome: StringName) -> void:
|
||||||
start_x + 0.18,
|
start_x + 0.18,
|
||||||
0.08
|
0.08
|
||||||
)
|
)
|
||||||
_active_tween.tween_property(
|
_queue_bobber_return(0.42, 0.65)
|
||||||
_bobber,
|
&"invalid", &"withdrawal":
|
||||||
"global_position:y",
|
_queue_bobber_return(0.42, 0.65)
|
||||||
get_water_surface_height() - 0.4,
|
|
||||||
0.2
|
|
||||||
)
|
|
||||||
&"invalid":
|
|
||||||
var return_target: Vector3 = _bobber.global_position
|
|
||||||
if _rod_tip != null:
|
|
||||||
return_target = _rod_tip.global_position
|
|
||||||
_active_tween.set_parallel(true)
|
|
||||||
_active_tween.tween_property(_bobber, "global_position", return_target, 0.28)
|
|
||||||
_active_tween.tween_property(_bobber, "scale", Vector3.ZERO, 0.28)
|
|
||||||
&"withdrawal":
|
|
||||||
var withdrawal_target: Vector3 = _bobber.global_position
|
|
||||||
if _rod_tip != null:
|
|
||||||
withdrawal_target = _rod_tip.global_position
|
|
||||||
_active_tween.set_parallel(true)
|
|
||||||
_active_tween.tween_property(
|
|
||||||
_bobber,
|
|
||||||
"global_position",
|
|
||||||
withdrawal_target,
|
|
||||||
0.28
|
|
||||||
)
|
|
||||||
_active_tween.tween_property(_bobber, "scale", Vector3.ZERO, 0.28)
|
|
||||||
_:
|
_:
|
||||||
_active_tween.tween_property(_bobber, "scale", Vector3.ZERO, 0.18)
|
_active_tween.tween_property(_bobber, "scale", Vector3.ZERO, 0.18)
|
||||||
|
|
||||||
|
|
@ -308,10 +283,14 @@ func cleanup() -> void:
|
||||||
_rod_tip = null
|
_rod_tip = null
|
||||||
_target_marker.visible = false
|
_target_marker.visible = false
|
||||||
_target_marker.scale = Vector3.ONE
|
_target_marker.scale = Vector3.ONE
|
||||||
_invalid_cross_a.visible = false
|
_valid_target_marker.visible = true
|
||||||
_invalid_cross_b.visible = false
|
_invalid_target_marker.visible = false
|
||||||
_bobber.visible = false
|
_bobber.visible = false
|
||||||
_bobber.scale = Vector3.ONE
|
_bobber.scale = Vector3.ONE
|
||||||
|
_bobber.rotation = Vector3.ZERO
|
||||||
|
_cast_arrival_position = Vector3.ZERO
|
||||||
|
_bobber_surface_position = Vector3.ZERO
|
||||||
|
_bobber_idle_elapsed = 0.0
|
||||||
set_line_mode(LineMode.HIDDEN)
|
set_line_mode(LineMode.HIDDEN)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -330,45 +309,64 @@ func _set_cast_sample(
|
||||||
_bobber.global_position = sample
|
_bobber.global_position = sample
|
||||||
|
|
||||||
|
|
||||||
|
func _queue_bobber_return(duration: float, arc_scale: float) -> void:
|
||||||
|
var return_start: Vector3 = _bobber.global_position
|
||||||
|
var return_target: Vector3 = return_start
|
||||||
|
if _rod_tip != null:
|
||||||
|
return_target = _rod_tip.global_position
|
||||||
|
_active_tween.tween_method(
|
||||||
|
_set_return_sample.bind(return_start, return_target, arc_scale),
|
||||||
|
0.0,
|
||||||
|
1.0,
|
||||||
|
duration,
|
||||||
|
)
|
||||||
|
_active_tween.tween_property(_bobber, "scale", Vector3.ZERO, 0.1)
|
||||||
|
|
||||||
|
|
||||||
|
func _set_return_sample(
|
||||||
|
progress: float,
|
||||||
|
start: Vector3,
|
||||||
|
target: Vector3,
|
||||||
|
arc_scale: float,
|
||||||
|
) -> void:
|
||||||
|
var eased_progress: float = 1.0 - pow(1.0 - progress, 3.0)
|
||||||
|
var sample: Vector3 = start.lerp(target, eased_progress)
|
||||||
|
sample.y += sin(eased_progress * PI) * maxf(
|
||||||
|
cast_arc_height * arc_scale,
|
||||||
|
0.75,
|
||||||
|
)
|
||||||
|
_bobber.global_position = sample
|
||||||
|
|
||||||
|
|
||||||
func _on_cast_tween_finished() -> void:
|
func _on_cast_tween_finished() -> void:
|
||||||
if _mode != VisualMode.CASTING:
|
if _mode != VisualMode.CASTING:
|
||||||
return
|
return
|
||||||
|
|
||||||
_active_tween = null
|
_active_tween = null
|
||||||
_bobber.global_position = _cast_position
|
_bobber.global_position = _cast_arrival_position
|
||||||
|
_bobber_surface_position = _cast_arrival_position
|
||||||
|
_bobber_idle_elapsed = 0.0
|
||||||
_mode = VisualMode.FISHING
|
_mode = VisualMode.FISHING
|
||||||
cast_completed.emit()
|
cast_completed.emit()
|
||||||
|
|
||||||
|
|
||||||
func _calculate_pickup_position(target: Vector3) -> Vector3:
|
func _apply_bobber_idle_motion() -> void:
|
||||||
var player_on_water: Vector3 = target
|
var phase: float = _bobber_idle_elapsed * bobber_bob_cycles_per_second * TAU
|
||||||
if _rod_tip != null:
|
_bobber.global_position = (
|
||||||
player_on_water = _with_water_height(_rod_tip.global_position)
|
_bobber_surface_position
|
||||||
var outward: Vector3 = target - player_on_water
|
+ Vector3.UP * sin(phase) * bobber_bob_amplitude
|
||||||
outward.y = 0.0
|
|
||||||
if outward.is_zero_approx():
|
|
||||||
outward = Vector3.FORWARD
|
|
||||||
else:
|
|
||||||
outward = outward.normalized()
|
|
||||||
var target_distance: float = player_on_water.distance_to(target)
|
|
||||||
var pickup_distance: float = minf(
|
|
||||||
pickup_distance_from_player,
|
|
||||||
target_distance * 0.8
|
|
||||||
)
|
)
|
||||||
return player_on_water + outward * pickup_distance
|
_bobber.rotation.z = (
|
||||||
|
deg_to_rad(bobber_tilt_degrees) * sin(phase * 0.73)
|
||||||
|
|
||||||
func _with_water_height(world_position: Vector3) -> Vector3:
|
|
||||||
return Vector3(
|
|
||||||
world_position.x,
|
|
||||||
get_water_surface_height(),
|
|
||||||
world_position.z,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
func _update_line(delta: float) -> void:
|
func _update_line(delta: float) -> void:
|
||||||
if _rod_tip == null or not is_instance_valid(_rod_tip):
|
if _rod_tip == null or not is_instance_valid(_rod_tip):
|
||||||
|
var was_active: bool = _mode != VisualMode.NONE
|
||||||
cleanup()
|
cleanup()
|
||||||
|
if was_active:
|
||||||
|
presentation_interrupted.emit()
|
||||||
return
|
return
|
||||||
|
|
||||||
var rod_tip_position: Vector3 = _rod_tip.global_position
|
var rod_tip_position: Vector3 = _rod_tip.global_position
|
||||||
|
|
@ -447,12 +445,8 @@ func _restore_rod_neutral() -> void:
|
||||||
|
|
||||||
|
|
||||||
func _set_target_validity(target_is_fishable: bool) -> void:
|
func _set_target_validity(target_is_fishable: bool) -> void:
|
||||||
if target_is_fishable:
|
_valid_target_marker.visible = target_is_fishable
|
||||||
_target_marker.material_override = valid_target_material
|
_invalid_target_marker.visible = not target_is_fishable
|
||||||
else:
|
|
||||||
_target_marker.material_override = invalid_target_material
|
|
||||||
_invalid_cross_a.visible = not target_is_fishable
|
|
||||||
_invalid_cross_b.visible = not target_is_fishable
|
|
||||||
|
|
||||||
|
|
||||||
func _kill_active_tween() -> void:
|
func _kill_active_tween() -> void:
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,12 @@ const NetworkSessionType = preload("res://network/network_session.gd")
|
||||||
const NetworkFishingServiceType = preload(
|
const NetworkFishingServiceType = preload(
|
||||||
"res://network/network_fishing_service.gd"
|
"res://network/network_fishing_service.gd"
|
||||||
)
|
)
|
||||||
|
const FishingSurfaceSampleType = preload(
|
||||||
|
"res://fishing/fishing_surface_sample.gd"
|
||||||
|
)
|
||||||
|
const FishingSurfaceResolverType = preload(
|
||||||
|
"res://fishing/fishing_surface_resolver.gd"
|
||||||
|
)
|
||||||
|
|
||||||
signal status_changed(status: String)
|
signal status_changed(status: String)
|
||||||
signal catch_display_changed(
|
signal catch_display_changed(
|
||||||
|
|
@ -57,6 +63,7 @@ enum FishingState {
|
||||||
WAITING_FOR_BITE,
|
WAITING_FOR_BITE,
|
||||||
FIGHTING,
|
FIGHTING,
|
||||||
SHOWING_CATCH,
|
SHOWING_CATCH,
|
||||||
|
RETURNING,
|
||||||
COOLDOWN,
|
COOLDOWN,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -69,16 +76,27 @@ enum FishingState {
|
||||||
|
|
||||||
@export_category("Target Preview")
|
@export_category("Target Preview")
|
||||||
@export_flags_3d_physics var preview_surface_mask: int = 5
|
@export_flags_3d_physics var preview_surface_mask: int = 5
|
||||||
@export_range(1.0, 100.0, 1.0) var preview_ray_start_height: float = 30.0
|
@export_range(1.0, 200.0, 1.0) var preview_ray_start_height: float = 100.0
|
||||||
@export_range(1.0, 200.0, 1.0) var preview_ray_length: float = 60.0
|
@export_range(1.0, 400.0, 1.0) var preview_surface_ray_length: float = 240.0
|
||||||
@export_range(0.01, 1.0, 0.01) var preview_marker_vertical_offset: float = 0.06
|
@export_range(0.01, 1.0, 0.01) var preview_marker_vertical_offset: float = 0.06
|
||||||
|
@export_range(0.0, 0.5, 0.01) var water_occlusion_tolerance: float = 0.04
|
||||||
|
@export_range(0.01, 0.5, 0.01) var solid_bobber_clearance: float = 0.13
|
||||||
|
|
||||||
@export_category("Withdrawal")
|
@export_category("Withdrawal")
|
||||||
@export_range(0.1, 20.0, 0.1) var withdrawal_rate: float = 4.9
|
@export_range(0.1, 20.0, 0.1) var withdrawal_rate: float = 4.9
|
||||||
@export_range(0.1, 10.0, 0.1) var withdrawal_cancel_distance: float = 1.0
|
@export_range(0.1, 10.0, 0.1) var withdrawal_cancel_distance: float = 1.0
|
||||||
|
@export_range(0.0, 1.0, 0.01) var withdrawal_surface_clearance: float = 0.4
|
||||||
|
|
||||||
@export_category("Timing")
|
@export_category("Timing")
|
||||||
@export_range(0.1, 30.0, 0.1) var wait_time: float = 2.0
|
const BITE_QUICK_MIN_SECONDS: float = 10.0
|
||||||
|
const BITE_QUICK_MAX_SECONDS: float = 30.0
|
||||||
|
const BITE_TYPICAL_MAX_SECONDS: float = 90.0
|
||||||
|
const BITE_LONG_MAX_SECONDS: float = 180.0
|
||||||
|
const BITE_MAX_SECONDS: float = 240.0
|
||||||
|
const BITE_QUICK_PROBABILITY: float = 0.15
|
||||||
|
const BITE_TYPICAL_PROBABILITY: float = 0.55
|
||||||
|
const BITE_LONG_PROBABILITY: float = 0.25
|
||||||
|
const NETWORK_INPUT_RESEND_INTERVAL_SECONDS: float = 0.1
|
||||||
@export_range(0.1, 10.0, 0.1) var cooldown_duration: float = 1.0
|
@export_range(0.1, 10.0, 0.1) var cooldown_duration: float = 1.0
|
||||||
|
|
||||||
@export_category("Selection")
|
@export_category("Selection")
|
||||||
|
|
@ -117,16 +135,18 @@ var _cast_charge: float = 0.0
|
||||||
var _cast_direction: Vector3 = Vector3.FORWARD
|
var _cast_direction: Vector3 = Vector3.FORWARD
|
||||||
var _cast_origin_position: Vector3
|
var _cast_origin_position: Vector3
|
||||||
var _cast_target: Vector3
|
var _cast_target: Vector3
|
||||||
var _cast_landing_is_fishable: bool = false
|
|
||||||
var _withdrawal_endpoint: Vector3
|
var _withdrawal_endpoint: Vector3
|
||||||
var _withdrawal_progress: float = 0.0
|
var _withdrawal_progress: float = 0.0
|
||||||
var _withdrawal_input_held: bool = false
|
var _withdrawal_input_held: bool = false
|
||||||
|
var _network_primary_input_held: bool = false
|
||||||
|
var _network_input_resend_elapsed: float = 0.0
|
||||||
var _new_cast_press_armed: bool = true
|
var _new_cast_press_armed: bool = true
|
||||||
var _bobber_water_position: Vector3
|
var _bobber_water_position: Vector3
|
||||||
var _fight_start_position: Vector3
|
|
||||||
var _cooldown_status: String = ""
|
var _cooldown_status: String = ""
|
||||||
var _fishable_query_shape: CylinderShape3D = CylinderShape3D.new()
|
|
||||||
var _fish_selector: FishSelectorType = FishSelectorType.new()
|
var _fish_selector: FishSelectorType = FishSelectorType.new()
|
||||||
|
var _surface_resolver: FishingSurfaceResolverType = FishingSurfaceResolverType.new()
|
||||||
|
var _aim_surface_sample: FishingSurfaceSampleType = FishingSurfaceSampleType.new()
|
||||||
|
var _cast_path_is_clear: bool = false
|
||||||
var _selected_water_region: FishableWaterRegionType
|
var _selected_water_region: FishableWaterRegionType
|
||||||
var _selection_context: FishingContextType
|
var _selection_context: FishingContextType
|
||||||
var _selected_fish: FishDataType
|
var _selected_fish: FishDataType
|
||||||
|
|
@ -137,14 +157,21 @@ var _showcase_restore_generation: int = 0
|
||||||
var _showcase_outcome_completed: bool = false
|
var _showcase_outcome_completed: bool = false
|
||||||
var _network_auto_click_accumulator: float = 0.0
|
var _network_auto_click_accumulator: float = 0.0
|
||||||
var _network_active_barrier_index: int = -1
|
var _network_active_barrier_index: int = -1
|
||||||
|
var _bite_rng: RandomNumberGenerator = RandomNumberGenerator.new()
|
||||||
|
var _pending_cleanup_message: String = ""
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
_bite_rng.randomize()
|
||||||
|
_configure_surface_resolver()
|
||||||
_catch_controller.encounter_updated.connect(_on_catch_encounter_updated)
|
_catch_controller.encounter_updated.connect(_on_catch_encounter_updated)
|
||||||
_catch_controller.caught.connect(_on_catch_completed)
|
_catch_controller.caught.connect(_on_catch_completed)
|
||||||
_catch_controller.escaped.connect(_on_catch_escaped)
|
_catch_controller.escaped.connect(_on_catch_escaped)
|
||||||
_presentation.cast_completed.connect(_on_cast_completed)
|
_presentation.cast_completed.connect(_on_cast_completed)
|
||||||
_presentation.outcome_completed.connect(_on_outcome_completed)
|
_presentation.outcome_completed.connect(_on_outcome_completed)
|
||||||
|
_presentation.presentation_interrupted.connect(
|
||||||
|
_on_presentation_interrupted
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func setup(
|
func setup(
|
||||||
|
|
@ -387,6 +414,8 @@ func _process(delta: float) -> void:
|
||||||
FishingState.WAITING_FOR_BITE:
|
FishingState.WAITING_FOR_BITE:
|
||||||
if _network_fishing == null:
|
if _network_fishing == null:
|
||||||
_update_waiting_for_bite(delta)
|
_update_waiting_for_bite(delta)
|
||||||
|
else:
|
||||||
|
_resend_network_input_state(delta)
|
||||||
FishingState.FIGHTING:
|
FishingState.FIGHTING:
|
||||||
if _network_fishing == null:
|
if _network_fishing == null:
|
||||||
_catch_controller.set_effective_stats(
|
_catch_controller.set_effective_stats(
|
||||||
|
|
@ -396,6 +425,7 @@ func _process(delta: float) -> void:
|
||||||
if not Input.is_action_pressed("fish_primary"):
|
if not Input.is_action_pressed("fish_primary"):
|
||||||
_catch_controller.set_reel_input(false)
|
_catch_controller.set_reel_input(false)
|
||||||
else:
|
else:
|
||||||
|
_resend_network_input_state(delta)
|
||||||
_update_network_auto_click(delta)
|
_update_network_auto_click(delta)
|
||||||
FishingState.COOLDOWN:
|
FishingState.COOLDOWN:
|
||||||
_state_time_remaining -= delta
|
_state_time_remaining -= delta
|
||||||
|
|
@ -444,15 +474,10 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||||
get_viewport().set_input_as_handled()
|
get_viewport().set_input_as_handled()
|
||||||
return
|
return
|
||||||
_begin_aiming(_local_player)
|
_begin_aiming(_local_player)
|
||||||
FishingState.WAITING_FOR_BITE:
|
|
||||||
_withdrawal_input_held = true
|
|
||||||
if _network_fishing != null:
|
|
||||||
_network_fishing.submit_local_input(true, true)
|
|
||||||
_presentation.set_line_mode(
|
|
||||||
FishingPresentationType.LineMode.TAUT
|
|
||||||
)
|
|
||||||
FishingState.FIGHTING:
|
FishingState.FIGHTING:
|
||||||
if _network_fishing != null:
|
if _network_fishing != null:
|
||||||
|
_network_primary_input_held = true
|
||||||
|
_network_input_resend_elapsed = 0.0
|
||||||
_network_fishing.submit_local_input(true, true)
|
_network_fishing.submit_local_input(true, true)
|
||||||
else:
|
else:
|
||||||
_catch_controller.handle_primary_pressed()
|
_catch_controller.handle_primary_pressed()
|
||||||
|
|
@ -464,6 +489,15 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||||
_put_away_catch()
|
_put_away_catch()
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
|
FishingState.WAITING_FOR_BITE:
|
||||||
|
_withdrawal_input_held = true
|
||||||
|
if _network_fishing != null:
|
||||||
|
_network_primary_input_held = true
|
||||||
|
_network_input_resend_elapsed = 0.0
|
||||||
|
_network_fishing.submit_local_input(true, true)
|
||||||
|
_presentation.set_line_mode(
|
||||||
|
FishingPresentationType.LineMode.TAUT
|
||||||
|
)
|
||||||
_:
|
_:
|
||||||
return
|
return
|
||||||
get_viewport().set_input_as_handled()
|
get_viewport().set_input_as_handled()
|
||||||
|
|
@ -473,11 +507,15 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||||
elif state == FishingState.WAITING_FOR_BITE:
|
elif state == FishingState.WAITING_FOR_BITE:
|
||||||
_withdrawal_input_held = false
|
_withdrawal_input_held = false
|
||||||
if _network_fishing != null:
|
if _network_fishing != null:
|
||||||
|
_network_primary_input_held = false
|
||||||
|
_network_input_resend_elapsed = 0.0
|
||||||
_network_fishing.submit_local_input(false, false)
|
_network_fishing.submit_local_input(false, false)
|
||||||
_presentation.set_line_mode(FishingPresentationType.LineMode.SLACK)
|
_presentation.set_line_mode(FishingPresentationType.LineMode.SLACK)
|
||||||
get_viewport().set_input_as_handled()
|
get_viewport().set_input_as_handled()
|
||||||
elif state == FishingState.FIGHTING:
|
elif state == FishingState.FIGHTING:
|
||||||
if _network_fishing != null:
|
if _network_fishing != null:
|
||||||
|
_network_primary_input_held = false
|
||||||
|
_network_input_resend_elapsed = 0.0
|
||||||
_network_fishing.submit_local_input(false, false)
|
_network_fishing.submit_local_input(false, false)
|
||||||
else:
|
else:
|
||||||
_catch_controller.set_reel_input(false)
|
_catch_controller.set_reel_input(false)
|
||||||
|
|
@ -488,21 +526,25 @@ func _begin_aiming(player: PlayerType) -> void:
|
||||||
if state != FishingState.READY or _active_player != null:
|
if state != FishingState.READY or _active_player != null:
|
||||||
return
|
return
|
||||||
|
|
||||||
_presentation.reset_water_surface_height()
|
|
||||||
_active_player = player
|
_active_player = player
|
||||||
_active_player.set_movement_enabled(false)
|
|
||||||
_cast_direction = _capture_cast_direction(_active_player)
|
_cast_direction = _capture_cast_direction(_active_player)
|
||||||
_cast_origin_position = _active_player.get_cast_origin_position()
|
_cast_origin_position = _active_player.get_cast_origin_position()
|
||||||
_cast_charge = 0.0
|
_cast_charge = 0.0
|
||||||
_cast_target = _calculate_cast_target(minimum_cast_distance)
|
_cast_target = _calculate_cast_target(minimum_cast_distance)
|
||||||
state = FishingState.AIMING_CAST
|
state = FishingState.AIMING_CAST
|
||||||
status_changed.emit("")
|
status_changed.emit("")
|
||||||
var target_is_fishable: bool = is_target_fishable(_cast_target)
|
_cast_path_is_clear = is_cast_path_clear(_cast_origin_position, _cast_target)
|
||||||
var preview_position: Vector3 = _resolve_preview_surface_position(_cast_target)
|
var target_is_fishable: bool = (
|
||||||
|
_aim_surface_sample.is_fishable() and _cast_path_is_clear
|
||||||
|
)
|
||||||
|
var preview_position: Vector3 = _aim_surface_sample.get_marker_position(
|
||||||
|
preview_marker_vertical_offset
|
||||||
|
)
|
||||||
_presentation.begin_aim(
|
_presentation.begin_aim(
|
||||||
_active_player.get_fishing_rod_tip(),
|
_active_player.get_fishing_rod_tip(),
|
||||||
_active_player.get_fishing_rod(),
|
_active_player.get_fishing_rod(),
|
||||||
preview_position,
|
preview_position,
|
||||||
|
_aim_surface_sample.normal,
|
||||||
target_is_fishable
|
target_is_fishable
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -575,6 +617,10 @@ func is_fighting() -> bool:
|
||||||
return state == FishingState.FIGHTING
|
return state == FishingState.FIGHTING
|
||||||
|
|
||||||
|
|
||||||
|
func is_returning() -> bool:
|
||||||
|
return state == FishingState.RETURNING
|
||||||
|
|
||||||
|
|
||||||
func refresh_active_item_status() -> void:
|
func refresh_active_item_status() -> void:
|
||||||
if state == FishingState.READY and has_active_fishing_rod():
|
if state == FishingState.READY and has_active_fishing_rod():
|
||||||
status_changed.emit("")
|
status_changed.emit("")
|
||||||
|
|
@ -584,14 +630,24 @@ func _update_cast_charge(delta: float) -> void:
|
||||||
if state != FishingState.AIMING_CAST:
|
if state != FishingState.AIMING_CAST:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Movement remains available during aiming. Re-sample the player transform
|
||||||
|
# so the cast origin and facing follow the player until release.
|
||||||
|
_cast_origin_position = _active_player.get_cast_origin_position()
|
||||||
|
_cast_direction = _capture_cast_direction(_active_player)
|
||||||
_cast_charge = minf(_cast_charge + delta / cast_charge_duration, 1.0)
|
_cast_charge = minf(_cast_charge + delta / cast_charge_duration, 1.0)
|
||||||
var maximum_distance: float = maxf(minimum_cast_distance, maximum_cast_distance)
|
var maximum_distance: float = maxf(minimum_cast_distance, maximum_cast_distance)
|
||||||
var distance: float = lerpf(minimum_cast_distance, maximum_distance, _cast_charge)
|
var distance: float = lerpf(minimum_cast_distance, maximum_distance, _cast_charge)
|
||||||
_cast_target = _calculate_cast_target(distance)
|
_cast_target = _calculate_cast_target(distance)
|
||||||
var target_is_fishable: bool = is_target_fishable(_cast_target)
|
_cast_path_is_clear = is_cast_path_clear(_cast_origin_position, _cast_target)
|
||||||
var preview_position: Vector3 = _resolve_preview_surface_position(_cast_target)
|
var target_is_fishable: bool = (
|
||||||
|
_aim_surface_sample.is_fishable() and _cast_path_is_clear
|
||||||
|
)
|
||||||
|
var preview_position: Vector3 = _aim_surface_sample.get_marker_position(
|
||||||
|
preview_marker_vertical_offset
|
||||||
|
)
|
||||||
_presentation.update_aim_target(
|
_presentation.update_aim_target(
|
||||||
preview_position,
|
preview_position,
|
||||||
|
_aim_surface_sample.normal,
|
||||||
_cast_charge,
|
_cast_charge,
|
||||||
target_is_fishable
|
target_is_fishable
|
||||||
)
|
)
|
||||||
|
|
@ -602,15 +658,39 @@ func _confirm_cast() -> void:
|
||||||
if state != FishingState.AIMING_CAST:
|
if state != FishingState.AIMING_CAST:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
_cast_path_is_clear = is_cast_path_clear(
|
||||||
|
_cast_origin_position,
|
||||||
|
_cast_target,
|
||||||
|
)
|
||||||
|
var cast_is_invalid: bool = (
|
||||||
|
not _aim_surface_sample.is_fishable() or not _cast_path_is_clear
|
||||||
|
)
|
||||||
|
var arrival_position: Vector3 = _cast_target
|
||||||
|
if not _cast_path_is_clear:
|
||||||
|
arrival_position = _resolve_cast_impact_position(
|
||||||
|
_cast_origin_position,
|
||||||
|
_cast_target,
|
||||||
|
)
|
||||||
|
if _active_player != null:
|
||||||
|
# Movement is available while aiming, then locks once the cast is
|
||||||
|
# released so the active bobber/fishing event remains anchored.
|
||||||
|
_active_player.set_movement_enabled(false)
|
||||||
state = FishingState.CASTING
|
state = FishingState.CASTING
|
||||||
status_changed.emit("")
|
status_changed.emit("")
|
||||||
_presentation.begin_cast(_cast_target)
|
_presentation.begin_cast(
|
||||||
|
_cast_target,
|
||||||
|
arrival_position,
|
||||||
|
cast_is_invalid,
|
||||||
|
)
|
||||||
_presentation.set_line_mode(FishingPresentationType.LineMode.TAUT)
|
_presentation.set_line_mode(FishingPresentationType.LineMode.TAUT)
|
||||||
|
|
||||||
|
|
||||||
func _on_cast_completed() -> void:
|
func _on_cast_completed() -> void:
|
||||||
if state != FishingState.CASTING:
|
if state != FishingState.CASTING:
|
||||||
return
|
return
|
||||||
|
if not _is_cast_target_valid(_cast_target):
|
||||||
|
_cleanup_attempt("", &"invalid")
|
||||||
|
return
|
||||||
if _network_fishing != null:
|
if _network_fishing != null:
|
||||||
_network_fishing.request_local_cast(
|
_network_fishing.request_local_cast(
|
||||||
_cast_origin_position,
|
_cast_origin_position,
|
||||||
|
|
@ -622,10 +702,6 @@ func _on_cast_completed() -> void:
|
||||||
return
|
return
|
||||||
|
|
||||||
_selected_water_region = get_fishable_water_region(_cast_target)
|
_selected_water_region = get_fishable_water_region(_cast_target)
|
||||||
_cast_landing_is_fishable = _selected_water_region != null
|
|
||||||
if not _cast_landing_is_fishable:
|
|
||||||
_cleanup_attempt("can't fish there.", &"invalid")
|
|
||||||
return
|
|
||||||
_selection_context = _build_fishing_context(_selected_water_region)
|
_selection_context = _build_fishing_context(_selected_water_region)
|
||||||
_fish_selector.undiscovered_weight_multiplier = undiscovered_weight_multiplier
|
_fish_selector.undiscovered_weight_multiplier = undiscovered_weight_multiplier
|
||||||
_fish_selector.rarity_weight_multipliers = []
|
_fish_selector.rarity_weight_multipliers = []
|
||||||
|
|
@ -648,7 +724,7 @@ func _on_cast_completed() -> void:
|
||||||
return
|
return
|
||||||
|
|
||||||
state = FishingState.WAITING_FOR_BITE
|
state = FishingState.WAITING_FOR_BITE
|
||||||
_state_time_remaining = wait_time * (
|
_state_time_remaining = roll_bite_wait_time() * (
|
||||||
_item_effects.get_bite_time_multiplier()
|
_item_effects.get_bite_time_multiplier()
|
||||||
if _item_effects != null
|
if _item_effects != null
|
||||||
else 1.0
|
else 1.0
|
||||||
|
|
@ -660,13 +736,40 @@ func _on_cast_completed() -> void:
|
||||||
_bobber_water_position = _cast_target
|
_bobber_water_position = _cast_target
|
||||||
_withdrawal_endpoint = Vector3(
|
_withdrawal_endpoint = Vector3(
|
||||||
_cast_origin_position.x + _cast_direction.x * withdrawal_cancel_distance,
|
_cast_origin_position.x + _cast_direction.x * withdrawal_cancel_distance,
|
||||||
_presentation.get_water_surface_height(),
|
_cast_target.y,
|
||||||
_cast_origin_position.z + _cast_direction.z * withdrawal_cancel_distance
|
_cast_origin_position.z + _cast_direction.z * withdrawal_cancel_distance
|
||||||
)
|
)
|
||||||
_presentation.set_line_mode(FishingPresentationType.LineMode.SLACK)
|
_presentation.set_line_mode(FishingPresentationType.LineMode.SLACK)
|
||||||
status_changed.emit("waiting for a bite...")
|
status_changed.emit("waiting for a bite...")
|
||||||
|
|
||||||
|
|
||||||
|
func roll_bite_wait_time() -> float:
|
||||||
|
var bucket: float = _bite_rng.randf()
|
||||||
|
if bucket < BITE_QUICK_PROBABILITY:
|
||||||
|
return _bite_rng.randf_range(
|
||||||
|
BITE_QUICK_MIN_SECONDS,
|
||||||
|
BITE_QUICK_MAX_SECONDS,
|
||||||
|
)
|
||||||
|
if bucket < BITE_QUICK_PROBABILITY + BITE_TYPICAL_PROBABILITY:
|
||||||
|
return _bite_rng.randf_range(
|
||||||
|
BITE_QUICK_MAX_SECONDS,
|
||||||
|
BITE_TYPICAL_MAX_SECONDS,
|
||||||
|
)
|
||||||
|
if bucket < (
|
||||||
|
BITE_QUICK_PROBABILITY
|
||||||
|
+ BITE_TYPICAL_PROBABILITY
|
||||||
|
+ BITE_LONG_PROBABILITY
|
||||||
|
):
|
||||||
|
return _bite_rng.randf_range(
|
||||||
|
BITE_TYPICAL_MAX_SECONDS,
|
||||||
|
BITE_LONG_MAX_SECONDS,
|
||||||
|
)
|
||||||
|
return _bite_rng.randf_range(
|
||||||
|
BITE_LONG_MAX_SECONDS,
|
||||||
|
BITE_MAX_SECONDS,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func _update_waiting_for_bite(delta: float) -> void:
|
func _update_waiting_for_bite(delta: float) -> void:
|
||||||
if (
|
if (
|
||||||
_withdrawal_input_held
|
_withdrawal_input_held
|
||||||
|
|
@ -713,16 +816,18 @@ func _update_withdrawal(delta: float) -> void:
|
||||||
_withdrawal_endpoint,
|
_withdrawal_endpoint,
|
||||||
next_progress
|
next_progress
|
||||||
)
|
)
|
||||||
var clamped_position: Vector3 = find_last_fishable_position(
|
var withdrawal_surface: FishingSurfaceSampleType = (
|
||||||
|
resolve_safe_withdrawal_surface(
|
||||||
_bobber_water_position,
|
_bobber_water_position,
|
||||||
desired_position
|
desired_position,
|
||||||
)
|
)
|
||||||
_withdrawal_progress = next_progress
|
)
|
||||||
_bobber_water_position = clamped_position
|
if not withdrawal_surface.is_fishable():
|
||||||
_presentation.show_withdrawal_position(_bobber_water_position)
|
|
||||||
if not clamped_position.is_equal_approx(desired_position):
|
|
||||||
_resolve_withdrawal_at_shore()
|
_resolve_withdrawal_at_shore()
|
||||||
return
|
return
|
||||||
|
_withdrawal_progress = next_progress
|
||||||
|
_bobber_water_position = withdrawal_surface.position
|
||||||
|
_presentation.show_withdrawal_position(_bobber_water_position)
|
||||||
if is_equal_approx(_withdrawal_progress, 1.0):
|
if is_equal_approx(_withdrawal_progress, 1.0):
|
||||||
_cancel_from_withdrawal()
|
_cancel_from_withdrawal()
|
||||||
|
|
||||||
|
|
@ -740,20 +845,14 @@ func _get_withdrawal_completion_time(delta: float) -> float:
|
||||||
_withdrawal_endpoint,
|
_withdrawal_endpoint,
|
||||||
next_progress
|
next_progress
|
||||||
)
|
)
|
||||||
var clamped_position: Vector3 = find_last_fishable_position(
|
var withdrawal_surface: FishingSurfaceSampleType = (
|
||||||
|
resolve_safe_withdrawal_surface(
|
||||||
_bobber_water_position,
|
_bobber_water_position,
|
||||||
desired_position
|
desired_position,
|
||||||
)
|
)
|
||||||
if not clamped_position.is_equal_approx(desired_position):
|
|
||||||
var segment_length: float = _bobber_water_position.distance_to(
|
|
||||||
desired_position
|
|
||||||
)
|
)
|
||||||
if segment_length <= 0.0:
|
if not withdrawal_surface.is_fishable():
|
||||||
return 0.0
|
return 0.0
|
||||||
return delta * (
|
|
||||||
_bobber_water_position.distance_to(clamped_position)
|
|
||||||
/ segment_length
|
|
||||||
)
|
|
||||||
if is_equal_approx(next_progress, 1.0):
|
if is_equal_approx(next_progress, 1.0):
|
||||||
return delta * (1.0 - _withdrawal_progress) / progress_step
|
return delta * (1.0 - _withdrawal_progress) / progress_step
|
||||||
return INF
|
return INF
|
||||||
|
|
@ -780,11 +879,10 @@ func _activate_bite() -> void:
|
||||||
state = FishingState.FIGHTING
|
state = FishingState.FIGHTING
|
||||||
_state_time_remaining = 0.0
|
_state_time_remaining = 0.0
|
||||||
_withdrawal_input_held = false
|
_withdrawal_input_held = false
|
||||||
_fight_start_position = _bobber_water_position
|
|
||||||
bite_activated.emit()
|
bite_activated.emit()
|
||||||
status_changed.emit("fish on!")
|
status_changed.emit("fish on!")
|
||||||
_presentation.set_line_mode(FishingPresentationType.LineMode.TAUT)
|
_presentation.set_line_mode(FishingPresentationType.LineMode.TAUT)
|
||||||
_presentation.begin_reeling()
|
_presentation.begin_fight()
|
||||||
_catch_controller.start_encounter(
|
_catch_controller.start_encounter(
|
||||||
_selected_fish.catch_profile,
|
_selected_fish.catch_profile,
|
||||||
_get_effective_reel_speed(),
|
_get_effective_reel_speed(),
|
||||||
|
|
@ -845,19 +943,6 @@ func _on_catch_encounter_updated(
|
||||||
if state != FishingState.FIGHTING or not visible:
|
if state != FishingState.FIGHTING or not visible:
|
||||||
return
|
return
|
||||||
|
|
||||||
var desired_position: Vector3 = _fight_start_position.lerp(
|
|
||||||
_withdrawal_endpoint,
|
|
||||||
progress
|
|
||||||
)
|
|
||||||
_bobber_water_position = find_last_fishable_position(
|
|
||||||
_fight_start_position,
|
|
||||||
desired_position
|
|
||||||
)
|
|
||||||
_presentation.show_reel_position(
|
|
||||||
_bobber_water_position,
|
|
||||||
Input.is_action_pressed("fish_primary")
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
func _on_catch_completed() -> void:
|
func _on_catch_completed() -> void:
|
||||||
if (
|
if (
|
||||||
|
|
@ -892,6 +977,11 @@ func _on_catch_escaped() -> void:
|
||||||
|
|
||||||
|
|
||||||
func _on_outcome_completed(outcome: StringName) -> void:
|
func _on_outcome_completed(outcome: StringName) -> void:
|
||||||
|
if state == FishingState.RETURNING:
|
||||||
|
var cleanup_message: String = _pending_cleanup_message
|
||||||
|
_pending_cleanup_message = ""
|
||||||
|
_finalize_attempt_cleanup(cleanup_message)
|
||||||
|
return
|
||||||
if (
|
if (
|
||||||
outcome != &"catch"
|
outcome != &"catch"
|
||||||
or state != FishingState.SHOWING_CATCH
|
or state != FishingState.SHOWING_CATCH
|
||||||
|
|
@ -911,6 +1001,15 @@ func _on_outcome_completed(outcome: StringName) -> void:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_presentation_interrupted() -> void:
|
||||||
|
if state in [FishingState.READY, FishingState.COOLDOWN]:
|
||||||
|
return
|
||||||
|
if _network_fishing != null and _network_fishing.has_local_attempt():
|
||||||
|
_network_fishing.cancel_local_attempt("")
|
||||||
|
return
|
||||||
|
_finalize_attempt_cleanup("")
|
||||||
|
|
||||||
|
|
||||||
func _put_away_catch() -> void:
|
func _put_away_catch() -> void:
|
||||||
if (
|
if (
|
||||||
state != FishingState.SHOWING_CATCH
|
state != FishingState.SHOWING_CATCH
|
||||||
|
|
@ -954,6 +1053,32 @@ func _cleanup_attempt(
|
||||||
cooldown_message: String = "",
|
cooldown_message: String = "",
|
||||||
visual_outcome: StringName = &"",
|
visual_outcome: StringName = &"",
|
||||||
) -> void:
|
) -> void:
|
||||||
|
if not visual_outcome.is_empty():
|
||||||
|
if state == FishingState.RETURNING:
|
||||||
|
return
|
||||||
|
_showcase_restore_generation += 1
|
||||||
|
_pending_cleanup_message = cooldown_message
|
||||||
|
_showcase_ready = false
|
||||||
|
_showcase_outcome_completed = false
|
||||||
|
_put_away_press_armed = false
|
||||||
|
showcase_changed.emit("", "", 0.0, false)
|
||||||
|
_catch_controller.reset()
|
||||||
|
state = FishingState.RETURNING
|
||||||
|
status_changed.emit("")
|
||||||
|
if visual_outcome in [&"invalid", &"withdrawal", &"escape"]:
|
||||||
|
_presentation.set_line_mode(
|
||||||
|
FishingPresentationType.LineMode.TAUT
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
_presentation.set_line_mode(
|
||||||
|
FishingPresentationType.LineMode.HIDDEN
|
||||||
|
)
|
||||||
|
_presentation.play_outcome(visual_outcome)
|
||||||
|
return
|
||||||
|
_finalize_attempt_cleanup(cooldown_message)
|
||||||
|
|
||||||
|
|
||||||
|
func _finalize_attempt_cleanup(cooldown_message: String) -> void:
|
||||||
_showcase_restore_generation += 1
|
_showcase_restore_generation += 1
|
||||||
if _active_player != null:
|
if _active_player != null:
|
||||||
_active_player.end_catch_showcase()
|
_active_player.end_catch_showcase()
|
||||||
|
|
@ -964,12 +1089,14 @@ func _cleanup_attempt(
|
||||||
_cast_direction = Vector3.FORWARD
|
_cast_direction = Vector3.FORWARD
|
||||||
_cast_origin_position = Vector3.ZERO
|
_cast_origin_position = Vector3.ZERO
|
||||||
_cast_target = Vector3.ZERO
|
_cast_target = Vector3.ZERO
|
||||||
_cast_landing_is_fishable = false
|
_aim_surface_sample = FishingSurfaceSampleType.new()
|
||||||
|
_cast_path_is_clear = false
|
||||||
_withdrawal_endpoint = Vector3.ZERO
|
_withdrawal_endpoint = Vector3.ZERO
|
||||||
_withdrawal_progress = 0.0
|
_withdrawal_progress = 0.0
|
||||||
_withdrawal_input_held = false
|
_withdrawal_input_held = false
|
||||||
|
_network_primary_input_held = false
|
||||||
|
_network_input_resend_elapsed = 0.0
|
||||||
_bobber_water_position = Vector3.ZERO
|
_bobber_water_position = Vector3.ZERO
|
||||||
_fight_start_position = Vector3.ZERO
|
|
||||||
_selected_water_region = null
|
_selected_water_region = null
|
||||||
_selection_context = null
|
_selection_context = null
|
||||||
_selected_fish = null
|
_selected_fish = null
|
||||||
|
|
@ -979,19 +1106,8 @@ func _cleanup_attempt(
|
||||||
_put_away_press_armed = false
|
_put_away_press_armed = false
|
||||||
showcase_changed.emit("", "", 0.0, false)
|
showcase_changed.emit("", "", 0.0, false)
|
||||||
_catch_controller.reset()
|
_catch_controller.reset()
|
||||||
if visual_outcome.is_empty():
|
|
||||||
_presentation.cleanup()
|
_presentation.cleanup()
|
||||||
_presentation.reset_water_surface_height()
|
_pending_cleanup_message = ""
|
||||||
else:
|
|
||||||
if visual_outcome in [&"catch", &"invalid", &"withdrawal"]:
|
|
||||||
_presentation.set_line_mode(
|
|
||||||
FishingPresentationType.LineMode.TAUT
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
_presentation.set_line_mode(
|
|
||||||
FishingPresentationType.LineMode.HIDDEN
|
|
||||||
)
|
|
||||||
_presentation.play_outcome(visual_outcome)
|
|
||||||
|
|
||||||
if cooldown_message.is_empty():
|
if cooldown_message.is_empty():
|
||||||
state = FishingState.READY
|
state = FishingState.READY
|
||||||
|
|
@ -1006,7 +1122,6 @@ func _cleanup_attempt(
|
||||||
|
|
||||||
|
|
||||||
func _return_to_ready() -> void:
|
func _return_to_ready() -> void:
|
||||||
_presentation.reset_water_surface_height()
|
|
||||||
state = FishingState.READY
|
state = FishingState.READY
|
||||||
_state_time_remaining = 0.0
|
_state_time_remaining = 0.0
|
||||||
_cooldown_status = ""
|
_cooldown_status = ""
|
||||||
|
|
@ -1025,6 +1140,9 @@ func _cancel_from_withdrawal() -> void:
|
||||||
if state != FishingState.WAITING_FOR_BITE:
|
if state != FishingState.WAITING_FOR_BITE:
|
||||||
return
|
return
|
||||||
_new_cast_press_armed = false
|
_new_cast_press_armed = false
|
||||||
|
if _network_fishing != null and _network_fishing.has_local_attempt():
|
||||||
|
_network_fishing.cancel_local_attempt("")
|
||||||
|
return
|
||||||
_cleanup_attempt("", &"withdrawal")
|
_cleanup_attempt("", &"withdrawal")
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1046,61 +1164,102 @@ func _capture_cast_direction(player: PlayerType) -> Vector3:
|
||||||
|
|
||||||
|
|
||||||
func _calculate_cast_target(distance: float) -> Vector3:
|
func _calculate_cast_target(distance: float) -> Vector3:
|
||||||
var target := Vector3(
|
var query_position := Vector3(
|
||||||
_cast_origin_position.x + _cast_direction.x * distance,
|
_cast_origin_position.x + _cast_direction.x * distance,
|
||||||
_presentation.get_water_surface_height(),
|
_cast_origin_position.y,
|
||||||
_cast_origin_position.z + _cast_direction.z * distance
|
_cast_origin_position.z + _cast_direction.z * distance
|
||||||
)
|
)
|
||||||
var water_region: FishableWaterRegionType = get_fishable_water_region(
|
_aim_surface_sample = resolve_fishing_surface(
|
||||||
target
|
query_position,
|
||||||
|
_cast_origin_position.y,
|
||||||
|
)
|
||||||
|
if _aim_surface_sample.has_surface:
|
||||||
|
return _aim_surface_sample.get_bobber_position(solid_bobber_clearance)
|
||||||
|
return query_position
|
||||||
|
|
||||||
|
|
||||||
|
func _configure_surface_resolver() -> void:
|
||||||
|
_surface_resolver.fishable_surface_mask = fishable_surface_mask
|
||||||
|
_surface_resolver.solid_surface_mask = (
|
||||||
|
preview_surface_mask & ~fishable_surface_mask
|
||||||
|
)
|
||||||
|
_surface_resolver.query_radius = fishable_query_radius
|
||||||
|
_surface_resolver.ray_start_height = preview_ray_start_height
|
||||||
|
_surface_resolver.ray_length = preview_surface_ray_length
|
||||||
|
_surface_resolver.water_occlusion_tolerance = water_occlusion_tolerance
|
||||||
|
|
||||||
|
|
||||||
|
func resolve_fishing_surface(
|
||||||
|
target: Vector3,
|
||||||
|
reference_y: float = NAN,
|
||||||
|
) -> FishingSurfaceSampleType:
|
||||||
|
if not is_inside_tree():
|
||||||
|
var empty_sample := FishingSurfaceSampleType.new()
|
||||||
|
empty_sample.position = target
|
||||||
|
return empty_sample
|
||||||
|
var resolved_reference_y: float = reference_y
|
||||||
|
if is_nan(resolved_reference_y):
|
||||||
|
resolved_reference_y = (
|
||||||
|
_active_player.global_position.y
|
||||||
|
if _active_player != null
|
||||||
|
else target.y
|
||||||
|
)
|
||||||
|
return _surface_resolver.resolve_surface(
|
||||||
|
get_world_3d().direct_space_state,
|
||||||
|
target,
|
||||||
|
resolved_reference_y,
|
||||||
)
|
)
|
||||||
if water_region != null:
|
|
||||||
target.y = water_region.get_surface_height()
|
|
||||||
_presentation.set_water_surface_height(target.y)
|
|
||||||
else:
|
|
||||||
_presentation.reset_water_surface_height()
|
|
||||||
target.y = _presentation.get_water_surface_height()
|
|
||||||
return target
|
|
||||||
|
|
||||||
|
|
||||||
func is_target_fishable(target: Vector3) -> bool:
|
func is_target_fishable(target: Vector3) -> bool:
|
||||||
return get_fishable_water_region(target) != null
|
return resolve_fishing_surface(target).is_fishable()
|
||||||
|
|
||||||
|
|
||||||
|
func _is_cast_target_valid(target: Vector3) -> bool:
|
||||||
|
return (
|
||||||
|
is_target_fishable(target)
|
||||||
|
and is_cast_path_clear(_cast_origin_position, target)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func is_cast_path_clear(origin: Vector3, target: Vector3) -> bool:
|
||||||
|
return _surface_resolver.find_first_cast_collision(
|
||||||
|
get_world_3d().direct_space_state,
|
||||||
|
origin,
|
||||||
|
target,
|
||||||
|
_presentation.cast_arc_height,
|
||||||
|
).is_empty()
|
||||||
|
|
||||||
|
|
||||||
|
func _resolve_cast_impact_position(origin: Vector3, target: Vector3) -> Vector3:
|
||||||
|
var result: Dictionary = _surface_resolver.find_first_cast_collision(
|
||||||
|
get_world_3d().direct_space_state,
|
||||||
|
origin,
|
||||||
|
target,
|
||||||
|
_presentation.cast_arc_height,
|
||||||
|
)
|
||||||
|
if result.is_empty():
|
||||||
|
return target
|
||||||
|
var hit_position: Vector3 = result["position"]
|
||||||
|
var hit_normal: Vector3 = result.get("normal", Vector3.UP)
|
||||||
|
if hit_normal.is_zero_approx():
|
||||||
|
hit_normal = Vector3.UP
|
||||||
|
return hit_position + hit_normal.normalized() * 0.12
|
||||||
|
|
||||||
|
|
||||||
func get_fishable_water_region(
|
func get_fishable_water_region(
|
||||||
target: Vector3,
|
target: Vector3,
|
||||||
) -> FishableWaterRegionType:
|
) -> FishableWaterRegionType:
|
||||||
_fishable_query_shape.radius = fishable_query_radius
|
var reference_y: float = (
|
||||||
_fishable_query_shape.height = preview_ray_length
|
_active_player.global_position.y
|
||||||
var query := PhysicsShapeQueryParameters3D.new()
|
if _active_player != null
|
||||||
query.shape = _fishable_query_shape
|
else target.y
|
||||||
query.transform = Transform3D(
|
|
||||||
Basis.IDENTITY,
|
|
||||||
Vector3(target.x, target.y, target.z)
|
|
||||||
)
|
)
|
||||||
query.collision_mask = fishable_surface_mask
|
var surface: FishingSurfaceSampleType = resolve_fishing_surface(
|
||||||
query.collide_with_areas = true
|
target,
|
||||||
query.collide_with_bodies = false
|
reference_y,
|
||||||
var results: Array[Dictionary] = get_world_3d().direct_space_state.intersect_shape(
|
|
||||||
query,
|
|
||||||
32
|
|
||||||
)
|
)
|
||||||
var selected_region: FishableWaterRegionType = null
|
return surface.water_region if surface.is_fishable() else null
|
||||||
for result: Dictionary in results:
|
|
||||||
var collider: Object = result.get("collider")
|
|
||||||
var region: FishableWaterRegionType = collider as FishableWaterRegionType
|
|
||||||
if region == null or region.fish_pool == null:
|
|
||||||
continue
|
|
||||||
if (
|
|
||||||
selected_region == null
|
|
||||||
or region.selection_priority > selected_region.selection_priority
|
|
||||||
or (
|
|
||||||
region.selection_priority == selected_region.selection_priority
|
|
||||||
and region.get_instance_id() < selected_region.get_instance_id()
|
|
||||||
)
|
|
||||||
):
|
|
||||||
selected_region = region
|
|
||||||
return selected_region
|
|
||||||
|
|
||||||
|
|
||||||
func _build_fishing_context(
|
func _build_fishing_context(
|
||||||
|
|
@ -1166,6 +1325,9 @@ func _on_network_cast_accepted(
|
||||||
_cast_target = target
|
_cast_target = target
|
||||||
_bobber_water_position = target
|
_bobber_water_position = target
|
||||||
state = FishingState.WAITING_FOR_BITE
|
state = FishingState.WAITING_FOR_BITE
|
||||||
|
_network_primary_input_held = false
|
||||||
|
_network_input_resend_elapsed = 0.0
|
||||||
|
_presentation.show_withdrawal_position(_bobber_water_position)
|
||||||
_presentation.set_line_mode(FishingPresentationType.LineMode.SLACK)
|
_presentation.set_line_mode(FishingPresentationType.LineMode.SLACK)
|
||||||
status_changed.emit("waiting for a bite...")
|
status_changed.emit("waiting for a bite...")
|
||||||
|
|
||||||
|
|
@ -1173,7 +1335,17 @@ func _on_network_cast_accepted(
|
||||||
func _on_network_cast_rejected(message: String) -> void:
|
func _on_network_cast_rejected(message: String) -> void:
|
||||||
if state not in [FishingState.CASTING, FishingState.AIMING_CAST]:
|
if state not in [FishingState.CASTING, FishingState.AIMING_CAST]:
|
||||||
return
|
return
|
||||||
_cleanup_attempt(message, &"invalid")
|
var visible_message: String = message
|
||||||
|
if message.strip_edges().to_lower() in [
|
||||||
|
"cannot fish here.",
|
||||||
|
"cannot fish here",
|
||||||
|
"can't fish here.",
|
||||||
|
"can't fish here",
|
||||||
|
"can't fish there.",
|
||||||
|
"can't fish there",
|
||||||
|
]:
|
||||||
|
visible_message = ""
|
||||||
|
_cleanup_attempt(visible_message, &"invalid")
|
||||||
|
|
||||||
|
|
||||||
func _on_network_bite_started(_attempt_id: String) -> void:
|
func _on_network_bite_started(_attempt_id: String) -> void:
|
||||||
|
|
@ -1181,20 +1353,34 @@ func _on_network_bite_started(_attempt_id: String) -> void:
|
||||||
return
|
return
|
||||||
state = FishingState.FIGHTING
|
state = FishingState.FIGHTING
|
||||||
_withdrawal_input_held = false
|
_withdrawal_input_held = false
|
||||||
|
_network_primary_input_held = Input.is_action_pressed("fish_primary")
|
||||||
|
_network_input_resend_elapsed = 0.0
|
||||||
_network_auto_click_accumulator = 0.0
|
_network_auto_click_accumulator = 0.0
|
||||||
_network_active_barrier_index = -1
|
_network_active_barrier_index = -1
|
||||||
_fight_start_position = _bobber_water_position
|
|
||||||
bite_activated.emit()
|
bite_activated.emit()
|
||||||
status_changed.emit("fish on!")
|
status_changed.emit("fish on!")
|
||||||
_presentation.set_line_mode(FishingPresentationType.LineMode.TAUT)
|
_presentation.set_line_mode(FishingPresentationType.LineMode.TAUT)
|
||||||
_presentation.begin_reeling()
|
_presentation.begin_fight()
|
||||||
_presentation.show_bite()
|
_presentation.show_bite()
|
||||||
_network_fishing.submit_local_input(
|
_network_fishing.submit_local_input(
|
||||||
Input.is_action_pressed("fish_primary"),
|
_network_primary_input_held,
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _resend_network_input_state(delta: float) -> void:
|
||||||
|
if _network_fishing == null or not _network_fishing.has_local_attempt():
|
||||||
|
return
|
||||||
|
_network_input_resend_elapsed += delta
|
||||||
|
if _network_input_resend_elapsed < NETWORK_INPUT_RESEND_INTERVAL_SECONDS:
|
||||||
|
return
|
||||||
|
_network_input_resend_elapsed = fmod(
|
||||||
|
_network_input_resend_elapsed,
|
||||||
|
NETWORK_INPUT_RESEND_INTERVAL_SECONDS,
|
||||||
|
)
|
||||||
|
_network_fishing.submit_local_input(_network_primary_input_held, false)
|
||||||
|
|
||||||
|
|
||||||
func _on_network_fishing_snapshot(snapshot: Dictionary) -> void:
|
func _on_network_fishing_snapshot(snapshot: Dictionary) -> void:
|
||||||
if state not in [
|
if state not in [
|
||||||
FishingState.WAITING_FOR_BITE,
|
FishingState.WAITING_FOR_BITE,
|
||||||
|
|
@ -1230,11 +1416,6 @@ func _on_network_fishing_snapshot(snapshot: Dictionary) -> void:
|
||||||
)
|
)
|
||||||
if state == FishingState.WAITING_FOR_BITE:
|
if state == FishingState.WAITING_FOR_BITE:
|
||||||
_presentation.show_withdrawal_position(_bobber_water_position)
|
_presentation.show_withdrawal_position(_bobber_water_position)
|
||||||
else:
|
|
||||||
_presentation.show_reel_position(
|
|
||||||
_bobber_water_position,
|
|
||||||
Input.is_action_pressed("fish_primary")
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
func _update_network_auto_click(delta: float) -> void:
|
func _update_network_auto_click(delta: float) -> void:
|
||||||
|
|
@ -1281,67 +1462,32 @@ func _on_network_attempt_ended(
|
||||||
FishingState.FIGHTING,
|
FishingState.FIGHTING,
|
||||||
]:
|
]:
|
||||||
return
|
return
|
||||||
_cleanup_attempt(
|
var visual_outcome: StringName = &"cancel"
|
||||||
message,
|
if outcome == &"escape":
|
||||||
&"escape" if outcome == &"escape" else &"cancel"
|
visual_outcome = &"escape"
|
||||||
|
elif outcome == &"withdrawal":
|
||||||
|
visual_outcome = &"withdrawal"
|
||||||
|
_cleanup_attempt(message, visual_outcome)
|
||||||
|
|
||||||
|
|
||||||
|
func resolve_safe_withdrawal_surface(
|
||||||
|
from: Vector3,
|
||||||
|
to: Vector3,
|
||||||
|
toward_player: Vector3 = Vector3.ZERO,
|
||||||
|
) -> FishingSurfaceSampleType:
|
||||||
|
var direction_to_player: Vector3 = toward_player
|
||||||
|
if direction_to_player.is_zero_approx():
|
||||||
|
direction_to_player = to - from
|
||||||
|
var reference_y: float = (
|
||||||
|
_active_player.global_position.y
|
||||||
|
if _active_player != null
|
||||||
|
else maxf(from.y, to.y)
|
||||||
)
|
)
|
||||||
|
return _surface_resolver.resolve_withdrawal_surface(
|
||||||
|
get_world_3d().direct_space_state,
|
||||||
func find_last_fishable_position(from: Vector3, to: Vector3) -> Vector3:
|
from,
|
||||||
if from.is_equal_approx(to):
|
to,
|
||||||
return from
|
direction_to_player,
|
||||||
|
reference_y,
|
||||||
var segment_length: float = from.distance_to(to)
|
withdrawal_surface_clearance,
|
||||||
var sample_spacing: float = maxf(fishable_query_radius, 0.05)
|
|
||||||
var sample_count: int = maxi(1, ceili(segment_length / sample_spacing))
|
|
||||||
var last_valid_fraction: float = 0.0
|
|
||||||
|
|
||||||
for sample_index: int in range(1, sample_count + 1):
|
|
||||||
var sample_fraction: float = float(sample_index) / float(sample_count)
|
|
||||||
var sample_position: Vector3 = from.lerp(to, sample_fraction)
|
|
||||||
if is_target_fishable(sample_position):
|
|
||||||
last_valid_fraction = sample_fraction
|
|
||||||
continue
|
|
||||||
|
|
||||||
var invalid_fraction: float = sample_fraction
|
|
||||||
for _iteration: int in range(10):
|
|
||||||
var midpoint: float = (
|
|
||||||
last_valid_fraction + invalid_fraction
|
|
||||||
) * 0.5
|
|
||||||
if is_target_fishable(from.lerp(to, midpoint)):
|
|
||||||
last_valid_fraction = midpoint
|
|
||||||
else:
|
|
||||||
invalid_fraction = midpoint
|
|
||||||
return from.lerp(to, last_valid_fraction)
|
|
||||||
|
|
||||||
return to
|
|
||||||
|
|
||||||
|
|
||||||
func _resolve_preview_surface_position(target: Vector3) -> Vector3:
|
|
||||||
var ray_start := Vector3(
|
|
||||||
target.x,
|
|
||||||
target.y + preview_ray_start_height,
|
|
||||||
target.z
|
|
||||||
)
|
|
||||||
var ray_end := Vector3(
|
|
||||||
target.x,
|
|
||||||
ray_start.y - preview_ray_length,
|
|
||||||
target.z
|
|
||||||
)
|
|
||||||
var query := PhysicsRayQueryParameters3D.create(
|
|
||||||
ray_start,
|
|
||||||
ray_end,
|
|
||||||
preview_surface_mask
|
|
||||||
)
|
|
||||||
query.collide_with_areas = true
|
|
||||||
query.collide_with_bodies = true
|
|
||||||
var result: Dictionary = get_world_3d().direct_space_state.intersect_ray(query)
|
|
||||||
if result.is_empty():
|
|
||||||
return target
|
|
||||||
|
|
||||||
var hit_position: Vector3 = result["position"]
|
|
||||||
return Vector3(
|
|
||||||
target.x,
|
|
||||||
hit_position.y + preview_marker_vertical_offset,
|
|
||||||
target.z
|
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
[gd_scene load_steps=11 format=3]
|
[gd_scene load_steps=12 format=3]
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://fishing/fishing_spot.gd" id="1_spot"]
|
[ext_resource type="Script" path="res://fishing/fishing_spot.gd" id="1_spot"]
|
||||||
[ext_resource type="Script" path="res://fishing/fishing_presentation.gd" id="3_presentation"]
|
[ext_resource type="Script" path="res://fishing/fishing_presentation.gd" id="3_presentation"]
|
||||||
|
|
@ -36,7 +36,7 @@ emission = Color(1, 0.01, 0.08, 1)
|
||||||
emission_energy_multiplier = 2.2
|
emission_energy_multiplier = 2.2
|
||||||
|
|
||||||
[sub_resource type="BoxMesh" id="InvalidCrossMesh"]
|
[sub_resource type="BoxMesh" id="InvalidCrossMesh"]
|
||||||
size = Vector3(0.72, 0.06, 0.12)
|
size = Vector3(0.62, 0.04, 0.09)
|
||||||
|
|
||||||
[node name="FishingSpot" type="Node3D"]
|
[node name="FishingSpot" type="Node3D"]
|
||||||
script = ExtResource("1_spot")
|
script = ExtResource("1_spot")
|
||||||
|
|
@ -48,24 +48,24 @@ script = ExtResource("4_catch")
|
||||||
[node name="FishingPresentation" type="Node3D" parent="."]
|
[node name="FishingPresentation" type="Node3D" parent="."]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
script = ExtResource("3_presentation")
|
script = ExtResource("3_presentation")
|
||||||
valid_target_material = SubResource("CastTargetMaterial")
|
|
||||||
invalid_target_material = SubResource("InvalidTargetMaterial")
|
|
||||||
|
|
||||||
[node name="CastTargetMarker" type="MeshInstance3D" parent="FishingPresentation"]
|
[node name="CastTargetMarker" type="Node3D" parent="FishingPresentation"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
|
||||||
|
[node name="ValidTargetMarker" type="MeshInstance3D" parent="FishingPresentation/CastTargetMarker"]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
mesh = SubResource("CastTargetMesh")
|
mesh = SubResource("CastTargetMesh")
|
||||||
material_override = SubResource("CastTargetMaterial")
|
material_override = SubResource("CastTargetMaterial")
|
||||||
|
|
||||||
[node name="InvalidCrossA" type="MeshInstance3D" parent="FishingPresentation/CastTargetMarker"]
|
[node name="InvalidTargetMarker" type="Node3D" parent="FishingPresentation/CastTargetMarker"]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
position = Vector3(0, 0.075, 0)
|
|
||||||
|
[node name="StrokeA" type="MeshInstance3D" parent="FishingPresentation/CastTargetMarker/InvalidTargetMarker"]
|
||||||
rotation = Vector3(0, 0.785398, 0)
|
rotation = Vector3(0, 0.785398, 0)
|
||||||
mesh = SubResource("InvalidCrossMesh")
|
mesh = SubResource("InvalidCrossMesh")
|
||||||
material_override = SubResource("InvalidTargetMaterial")
|
material_override = SubResource("InvalidTargetMaterial")
|
||||||
|
|
||||||
[node name="InvalidCrossB" type="MeshInstance3D" parent="FishingPresentation/CastTargetMarker"]
|
[node name="StrokeB" type="MeshInstance3D" parent="FishingPresentation/CastTargetMarker/InvalidTargetMarker"]
|
||||||
unique_name_in_owner = true
|
|
||||||
position = Vector3(0, 0.075, 0)
|
|
||||||
rotation = Vector3(0, -0.785398, 0)
|
rotation = Vector3(0, -0.785398, 0)
|
||||||
mesh = SubResource("InvalidCrossMesh")
|
mesh = SubResource("InvalidCrossMesh")
|
||||||
material_override = SubResource("InvalidTargetMaterial")
|
material_override = SubResource("InvalidTargetMaterial")
|
||||||
|
|
|
||||||
230
fishing/fishing_surface_resolver.gd
Normal file
230
fishing/fishing_surface_resolver.gd
Normal file
|
|
@ -0,0 +1,230 @@
|
||||||
|
class_name FishingSurfaceResolver
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
const FishableWaterRegionType = preload(
|
||||||
|
"res://world/fishable_water_region.gd"
|
||||||
|
)
|
||||||
|
const FishingSurfaceSampleType = preload(
|
||||||
|
"res://fishing/fishing_surface_sample.gd"
|
||||||
|
)
|
||||||
|
|
||||||
|
var fishable_surface_mask: int = 4
|
||||||
|
var solid_surface_mask: int = 1
|
||||||
|
var query_radius: float = 0.08
|
||||||
|
var ray_start_height: float = 100.0
|
||||||
|
var ray_length: float = 240.0
|
||||||
|
var water_occlusion_tolerance: float = 0.04
|
||||||
|
var arc_sample_spacing: float = 0.3
|
||||||
|
|
||||||
|
var _water_query_shape: CylinderShape3D = CylinderShape3D.new()
|
||||||
|
|
||||||
|
|
||||||
|
func resolve_surface(
|
||||||
|
space_state: PhysicsDirectSpaceState3D,
|
||||||
|
query_position: Vector3,
|
||||||
|
reference_y: float,
|
||||||
|
) -> FishingSurfaceSampleType:
|
||||||
|
var sample := FishingSurfaceSampleType.new()
|
||||||
|
var ray_top: float = maxf(query_position.y, reference_y) + ray_start_height
|
||||||
|
var ray_bottom: float = ray_top - ray_length
|
||||||
|
var water_region: FishableWaterRegionType = _find_highest_water_region(
|
||||||
|
space_state,
|
||||||
|
query_position,
|
||||||
|
ray_top,
|
||||||
|
ray_bottom,
|
||||||
|
)
|
||||||
|
var solid_hit: Dictionary = _find_top_solid_surface(
|
||||||
|
space_state,
|
||||||
|
query_position,
|
||||||
|
ray_top,
|
||||||
|
ray_bottom,
|
||||||
|
)
|
||||||
|
|
||||||
|
if water_region != null:
|
||||||
|
var water_height: float = water_region.get_surface_height()
|
||||||
|
var solid_is_above_water: bool = false
|
||||||
|
if not solid_hit.is_empty():
|
||||||
|
var solid_position: Vector3 = solid_hit["position"]
|
||||||
|
solid_is_above_water = (
|
||||||
|
solid_position.y >= water_height - water_occlusion_tolerance
|
||||||
|
)
|
||||||
|
if not solid_is_above_water:
|
||||||
|
sample.has_surface = true
|
||||||
|
sample.position = Vector3(
|
||||||
|
query_position.x,
|
||||||
|
water_height,
|
||||||
|
query_position.z,
|
||||||
|
)
|
||||||
|
sample.normal = Vector3.UP
|
||||||
|
sample.water_region = water_region
|
||||||
|
sample.is_water_surface = true
|
||||||
|
return sample
|
||||||
|
|
||||||
|
if not solid_hit.is_empty():
|
||||||
|
sample.has_surface = true
|
||||||
|
sample.position = solid_hit["position"]
|
||||||
|
var hit_normal: Vector3 = solid_hit.get("normal", Vector3.UP)
|
||||||
|
sample.normal = (
|
||||||
|
hit_normal.normalized()
|
||||||
|
if not hit_normal.is_zero_approx()
|
||||||
|
else Vector3.UP
|
||||||
|
)
|
||||||
|
|
||||||
|
return sample
|
||||||
|
|
||||||
|
|
||||||
|
func find_first_cast_collision(
|
||||||
|
space_state: PhysicsDirectSpaceState3D,
|
||||||
|
origin: Vector3,
|
||||||
|
target: Vector3,
|
||||||
|
arc_height: float,
|
||||||
|
) -> Dictionary:
|
||||||
|
if origin.is_equal_approx(target):
|
||||||
|
return {}
|
||||||
|
var segment_count: int = maxi(
|
||||||
|
12,
|
||||||
|
ceili(origin.distance_to(target) / maxf(arc_sample_spacing, 0.05)),
|
||||||
|
)
|
||||||
|
var previous_position: Vector3 = origin
|
||||||
|
for segment_index: int in range(1, segment_count + 1):
|
||||||
|
var progress: float = float(segment_index) / float(segment_count)
|
||||||
|
var next_position: Vector3 = origin.lerp(target, progress)
|
||||||
|
next_position.y += sin(progress * PI) * arc_height
|
||||||
|
var hit: Dictionary = _intersect_solid_segment(
|
||||||
|
space_state,
|
||||||
|
previous_position,
|
||||||
|
next_position,
|
||||||
|
)
|
||||||
|
if not hit.is_empty():
|
||||||
|
return hit
|
||||||
|
previous_position = next_position
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
func resolve_withdrawal_surface(
|
||||||
|
space_state: PhysicsDirectSpaceState3D,
|
||||||
|
current_position: Vector3,
|
||||||
|
desired_position: Vector3,
|
||||||
|
toward_player: Vector3,
|
||||||
|
reference_y: float,
|
||||||
|
shore_clearance: float,
|
||||||
|
) -> FishingSurfaceSampleType:
|
||||||
|
var desired_sample: FishingSurfaceSampleType = resolve_surface(
|
||||||
|
space_state,
|
||||||
|
desired_position,
|
||||||
|
reference_y,
|
||||||
|
)
|
||||||
|
if not desired_sample.is_fishable():
|
||||||
|
return _blocked_withdrawal_sample(current_position)
|
||||||
|
|
||||||
|
var horizontal_direction: Vector3 = toward_player
|
||||||
|
horizontal_direction.y = 0.0
|
||||||
|
if not horizontal_direction.is_zero_approx() and shore_clearance > 0.0:
|
||||||
|
horizontal_direction = horizontal_direction.normalized()
|
||||||
|
var lookahead_position: Vector3 = (
|
||||||
|
desired_sample.position + horizontal_direction * shore_clearance
|
||||||
|
)
|
||||||
|
var lookahead_sample: FishingSurfaceSampleType = resolve_surface(
|
||||||
|
space_state,
|
||||||
|
lookahead_position,
|
||||||
|
reference_y,
|
||||||
|
)
|
||||||
|
if not lookahead_sample.is_fishable():
|
||||||
|
return _blocked_withdrawal_sample(current_position)
|
||||||
|
|
||||||
|
return desired_sample
|
||||||
|
|
||||||
|
|
||||||
|
func _blocked_withdrawal_sample(
|
||||||
|
current_position: Vector3,
|
||||||
|
) -> FishingSurfaceSampleType:
|
||||||
|
var sample := FishingSurfaceSampleType.new()
|
||||||
|
sample.position = current_position
|
||||||
|
return sample
|
||||||
|
|
||||||
|
|
||||||
|
func _find_highest_water_region(
|
||||||
|
space_state: PhysicsDirectSpaceState3D,
|
||||||
|
query_position: Vector3,
|
||||||
|
ray_top: float,
|
||||||
|
ray_bottom: float,
|
||||||
|
) -> FishableWaterRegionType:
|
||||||
|
_water_query_shape.radius = maxf(query_radius, 0.01)
|
||||||
|
_water_query_shape.height = maxf(ray_top - ray_bottom, 0.1)
|
||||||
|
var query := PhysicsShapeQueryParameters3D.new()
|
||||||
|
query.shape = _water_query_shape
|
||||||
|
query.transform = Transform3D(
|
||||||
|
Basis.IDENTITY,
|
||||||
|
Vector3(
|
||||||
|
query_position.x,
|
||||||
|
(ray_top + ray_bottom) * 0.5,
|
||||||
|
query_position.z,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
query.collision_mask = fishable_surface_mask
|
||||||
|
query.collide_with_areas = true
|
||||||
|
query.collide_with_bodies = false
|
||||||
|
var results: Array[Dictionary] = space_state.intersect_shape(query, 64)
|
||||||
|
var selected_region: FishableWaterRegionType = null
|
||||||
|
for result: Dictionary in results:
|
||||||
|
var region: FishableWaterRegionType = (
|
||||||
|
result.get("collider") as FishableWaterRegionType
|
||||||
|
)
|
||||||
|
# A water surface remains the visible top surface even when it has no
|
||||||
|
# fish pool. Fishability is a separate policy checked by the sample.
|
||||||
|
if region == null:
|
||||||
|
continue
|
||||||
|
var surface_height: float = region.get_surface_height()
|
||||||
|
if surface_height < ray_bottom or surface_height > ray_top:
|
||||||
|
continue
|
||||||
|
if selected_region == null:
|
||||||
|
selected_region = region
|
||||||
|
continue
|
||||||
|
var selected_height: float = selected_region.get_surface_height()
|
||||||
|
if (
|
||||||
|
surface_height > selected_height + 0.001
|
||||||
|
or (
|
||||||
|
is_equal_approx(surface_height, selected_height)
|
||||||
|
and (
|
||||||
|
region.selection_priority > selected_region.selection_priority
|
||||||
|
or (
|
||||||
|
region.selection_priority
|
||||||
|
== selected_region.selection_priority
|
||||||
|
and region.get_instance_id()
|
||||||
|
< selected_region.get_instance_id()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
):
|
||||||
|
selected_region = region
|
||||||
|
return selected_region
|
||||||
|
|
||||||
|
|
||||||
|
func _find_top_solid_surface(
|
||||||
|
space_state: PhysicsDirectSpaceState3D,
|
||||||
|
query_position: Vector3,
|
||||||
|
ray_top: float,
|
||||||
|
ray_bottom: float,
|
||||||
|
) -> Dictionary:
|
||||||
|
return _intersect_solid_segment(
|
||||||
|
space_state,
|
||||||
|
Vector3(query_position.x, ray_top, query_position.z),
|
||||||
|
Vector3(query_position.x, ray_bottom, query_position.z),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _intersect_solid_segment(
|
||||||
|
space_state: PhysicsDirectSpaceState3D,
|
||||||
|
from: Vector3,
|
||||||
|
to: Vector3,
|
||||||
|
) -> Dictionary:
|
||||||
|
if from.is_equal_approx(to) or solid_surface_mask == 0:
|
||||||
|
return {}
|
||||||
|
var query := PhysicsRayQueryParameters3D.create(
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
solid_surface_mask,
|
||||||
|
)
|
||||||
|
query.collide_with_areas = false
|
||||||
|
query.collide_with_bodies = true
|
||||||
|
return space_state.intersect_ray(query)
|
||||||
1
fishing/fishing_surface_resolver.gd.uid
Normal file
1
fishing/fishing_surface_resolver.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://dcyvrak5pvlc
|
||||||
28
fishing/fishing_surface_sample.gd
Normal file
28
fishing/fishing_surface_sample.gd
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
class_name FishingSurfaceSample
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
const FishableWaterRegionType = preload(
|
||||||
|
"res://world/fishable_water_region.gd"
|
||||||
|
)
|
||||||
|
|
||||||
|
var has_surface: bool = false
|
||||||
|
var position: Vector3 = Vector3.ZERO
|
||||||
|
var normal: Vector3 = Vector3.UP
|
||||||
|
var water_region: FishableWaterRegionType
|
||||||
|
var is_water_surface: bool = false
|
||||||
|
|
||||||
|
|
||||||
|
func is_fishable() -> bool:
|
||||||
|
return is_water_surface and water_region != null and water_region.fish_pool != null
|
||||||
|
|
||||||
|
|
||||||
|
func get_marker_position(vertical_offset: float) -> Vector3:
|
||||||
|
if not has_surface:
|
||||||
|
return position
|
||||||
|
return position + normal.normalized() * vertical_offset
|
||||||
|
|
||||||
|
|
||||||
|
func get_bobber_position(solid_clearance: float) -> Vector3:
|
||||||
|
if not has_surface or is_water_surface:
|
||||||
|
return position
|
||||||
|
return position + normal.normalized() * solid_clearance
|
||||||
1
fishing/fishing_surface_sample.gd.uid
Normal file
1
fishing/fishing_surface_sample.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://5n0ku0fxh7xt
|
||||||
|
|
@ -1,12 +1,20 @@
|
||||||
class_name RemoteFishingPresentation
|
class_name RemoteFishingPresentation
|
||||||
extends Node3D
|
extends Node3D
|
||||||
|
|
||||||
|
signal return_completed
|
||||||
|
|
||||||
var _owner: Player
|
var _owner: Player
|
||||||
var _bobber: MeshInstance3D
|
var _bobber: MeshInstance3D
|
||||||
var _line: MeshInstance3D
|
var _line: MeshInstance3D
|
||||||
var _line_mesh := ImmediateMesh.new()
|
var _line_mesh := ImmediateMesh.new()
|
||||||
var _target: Vector3
|
var _target: Vector3
|
||||||
|
var _pending_target: Vector3
|
||||||
var _active: bool = false
|
var _active: bool = false
|
||||||
|
var _cast_tween: Tween
|
||||||
|
var _return_tween: Tween
|
||||||
|
var _showcase_tween: Tween
|
||||||
|
var _return_showcase_catch: FishCatch
|
||||||
|
var _bobber_idle_elapsed: float = 0.0
|
||||||
|
|
||||||
|
|
||||||
func setup(owning_player: Player) -> void:
|
func setup(owning_player: Player) -> void:
|
||||||
|
|
@ -31,21 +39,42 @@ func setup(owning_player: Player) -> void:
|
||||||
cleanup()
|
cleanup()
|
||||||
|
|
||||||
|
|
||||||
func show_cast(target: Vector3) -> void:
|
func show_cast(origin: Vector3, target: Vector3) -> void:
|
||||||
if _owner == null or not target.is_finite():
|
if (
|
||||||
|
_owner == null
|
||||||
|
or not origin.is_finite()
|
||||||
|
or not target.is_finite()
|
||||||
|
):
|
||||||
return
|
return
|
||||||
|
_kill_cast_tween()
|
||||||
_active = true
|
_active = true
|
||||||
_target = target
|
_target = origin
|
||||||
_bobber.global_position = target
|
_pending_target = target
|
||||||
|
_bobber_idle_elapsed = 0.0
|
||||||
|
_bobber.global_position = origin
|
||||||
|
_bobber.scale = Vector3.ONE
|
||||||
_bobber.visible = true
|
_bobber.visible = true
|
||||||
_line.visible = true
|
_line.visible = true
|
||||||
_owner.set_active_item_is_rod(true)
|
_owner.set_active_item_is_rod(true)
|
||||||
_redraw_line()
|
_redraw_line()
|
||||||
|
_cast_tween = create_tween()
|
||||||
|
_cast_tween.set_trans(Tween.TRANS_SINE)
|
||||||
|
_cast_tween.set_ease(Tween.EASE_IN_OUT)
|
||||||
|
_cast_tween.tween_method(
|
||||||
|
_set_cast_sample.bind(origin, target),
|
||||||
|
0.0,
|
||||||
|
1.0,
|
||||||
|
0.65,
|
||||||
|
)
|
||||||
|
_cast_tween.finished.connect(_on_cast_finished)
|
||||||
|
|
||||||
|
|
||||||
func update_bobber(world_position: Vector3) -> void:
|
func update_bobber(world_position: Vector3) -> void:
|
||||||
if not _active or not world_position.is_finite():
|
if not _active or not world_position.is_finite():
|
||||||
return
|
return
|
||||||
|
_pending_target = world_position
|
||||||
|
if _cast_tween != null:
|
||||||
|
return
|
||||||
_target = world_position
|
_target = world_position
|
||||||
_bobber.global_position = world_position
|
_bobber.global_position = world_position
|
||||||
_redraw_line()
|
_redraw_line()
|
||||||
|
|
@ -59,8 +88,43 @@ func show_bite() -> void:
|
||||||
tween.tween_property(_bobber, "scale", Vector3.ONE, 0.12)
|
tween.tween_property(_bobber, "scale", Vector3.ONE, 0.12)
|
||||||
|
|
||||||
|
|
||||||
|
func play_return(showcase_catch: FishCatch = null) -> void:
|
||||||
|
if not _active or _bobber == null:
|
||||||
|
cleanup()
|
||||||
|
return_completed.emit()
|
||||||
|
return
|
||||||
|
_kill_cast_tween()
|
||||||
|
_kill_return_tween()
|
||||||
|
_return_showcase_catch = showcase_catch
|
||||||
|
var start: Vector3 = _bobber.global_position
|
||||||
|
var target: Vector3 = start
|
||||||
|
if _owner != null and is_instance_valid(_owner):
|
||||||
|
var tip: Marker3D = _owner.get_fishing_rod_tip()
|
||||||
|
if tip != null:
|
||||||
|
target = tip.global_position
|
||||||
|
_return_tween = create_tween()
|
||||||
|
_return_tween.set_trans(Tween.TRANS_QUAD)
|
||||||
|
_return_tween.set_ease(Tween.EASE_IN)
|
||||||
|
_return_tween.tween_method(
|
||||||
|
_set_return_sample.bind(start, target),
|
||||||
|
0.0,
|
||||||
|
1.0,
|
||||||
|
0.42,
|
||||||
|
)
|
||||||
|
_return_tween.tween_property(_bobber, "scale", Vector3.ZERO, 0.1)
|
||||||
|
_return_tween.finished.connect(_on_return_finished)
|
||||||
|
|
||||||
|
|
||||||
func cleanup() -> void:
|
func cleanup() -> void:
|
||||||
|
_kill_cast_tween()
|
||||||
|
_kill_return_tween()
|
||||||
|
_kill_showcase_tween()
|
||||||
_active = false
|
_active = false
|
||||||
|
_pending_target = Vector3.ZERO
|
||||||
|
_return_showcase_catch = null
|
||||||
|
_bobber_idle_elapsed = 0.0
|
||||||
|
if _owner != null and is_instance_valid(_owner):
|
||||||
|
_owner.end_catch_showcase(Callable(), true)
|
||||||
if _bobber != null:
|
if _bobber != null:
|
||||||
_bobber.visible = false
|
_bobber.visible = false
|
||||||
_bobber.scale = Vector3.ONE
|
_bobber.scale = Vector3.ONE
|
||||||
|
|
@ -69,12 +133,96 @@ func cleanup() -> void:
|
||||||
_line_mesh.clear_surfaces()
|
_line_mesh.clear_surfaces()
|
||||||
|
|
||||||
|
|
||||||
func _process(_delta: float) -> void:
|
func _set_cast_sample(
|
||||||
|
progress: float,
|
||||||
|
start: Vector3,
|
||||||
|
target: Vector3,
|
||||||
|
) -> void:
|
||||||
|
_target = start.lerp(target, progress)
|
||||||
|
_target.y += sin(progress * PI) * 2.0
|
||||||
|
_bobber.global_position = _target
|
||||||
|
|
||||||
|
|
||||||
|
func _on_cast_finished() -> void:
|
||||||
|
_cast_tween = null
|
||||||
|
_bobber_idle_elapsed = 0.0
|
||||||
|
_apply_bobber_idle_motion()
|
||||||
|
_redraw_line()
|
||||||
|
|
||||||
|
|
||||||
|
func _apply_bobber_idle_motion() -> void:
|
||||||
|
var phase: float = _bobber_idle_elapsed * 0.7 * TAU
|
||||||
|
_target = _pending_target + Vector3.UP * sin(phase) * 0.035
|
||||||
|
_bobber.global_position = _target
|
||||||
|
_bobber.rotation.z = deg_to_rad(4.0) * sin(phase * 0.73)
|
||||||
|
|
||||||
|
|
||||||
|
func _set_return_sample(
|
||||||
|
progress: float,
|
||||||
|
start: Vector3,
|
||||||
|
target: Vector3,
|
||||||
|
) -> void:
|
||||||
|
var eased_progress: float = 1.0 - pow(1.0 - progress, 3.0)
|
||||||
|
_target = start.lerp(target, eased_progress)
|
||||||
|
_target.y += sin(eased_progress * PI) * 1.3
|
||||||
|
_bobber.global_position = _target
|
||||||
|
_bobber.rotation = Vector3.ZERO
|
||||||
|
|
||||||
|
|
||||||
|
func _on_return_finished() -> void:
|
||||||
|
_return_tween = null
|
||||||
|
_active = false
|
||||||
|
_bobber.visible = false
|
||||||
|
_line.visible = false
|
||||||
|
_line_mesh.clear_surfaces()
|
||||||
|
if (
|
||||||
|
_return_showcase_catch != null
|
||||||
|
and _owner != null
|
||||||
|
and is_instance_valid(_owner)
|
||||||
|
):
|
||||||
|
_owner.begin_remote_catch_showcase(_return_showcase_catch)
|
||||||
|
_return_showcase_catch = null
|
||||||
|
_showcase_tween = create_tween()
|
||||||
|
_showcase_tween.tween_interval(2.5)
|
||||||
|
_showcase_tween.finished.connect(_on_showcase_finished)
|
||||||
|
return
|
||||||
|
return_completed.emit()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_showcase_finished() -> void:
|
||||||
|
_showcase_tween = null
|
||||||
|
if _owner != null and is_instance_valid(_owner):
|
||||||
|
_owner.end_catch_showcase()
|
||||||
|
return_completed.emit()
|
||||||
|
|
||||||
|
|
||||||
|
func _kill_cast_tween() -> void:
|
||||||
|
if _cast_tween != null and _cast_tween.is_valid():
|
||||||
|
_cast_tween.kill()
|
||||||
|
_cast_tween = null
|
||||||
|
|
||||||
|
|
||||||
|
func _kill_return_tween() -> void:
|
||||||
|
if _return_tween != null and _return_tween.is_valid():
|
||||||
|
_return_tween.kill()
|
||||||
|
_return_tween = null
|
||||||
|
|
||||||
|
|
||||||
|
func _kill_showcase_tween() -> void:
|
||||||
|
if _showcase_tween != null and _showcase_tween.is_valid():
|
||||||
|
_showcase_tween.kill()
|
||||||
|
_showcase_tween = null
|
||||||
|
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
if _owner != null and not _owner.is_remote_presentation_visible():
|
if _owner != null and not _owner.is_remote_presentation_visible():
|
||||||
_bobber.visible = false
|
_bobber.visible = false
|
||||||
_line.visible = false
|
_line.visible = false
|
||||||
return
|
return
|
||||||
if _active:
|
if _active:
|
||||||
|
if _cast_tween == null and _return_tween == null:
|
||||||
|
_bobber_idle_elapsed += delta
|
||||||
|
_apply_bobber_idle_motion()
|
||||||
_redraw_line()
|
_redraw_line()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -256,10 +256,21 @@ func _handle_cast_request(peer_id: int, data: Dictionary) -> void:
|
||||||
):
|
):
|
||||||
_record_and_reject(peer_id, request_id, "Cannot fish here.")
|
_record_and_reject(peer_id, request_id, "Cannot fish here.")
|
||||||
return
|
return
|
||||||
var region: FishableWaterRegion = (
|
var surface: FishingSurfaceSample = _fishing_spot.resolve_fishing_surface(
|
||||||
_fishing_spot.get_fishable_water_region(target)
|
target,
|
||||||
|
authoritative_origin.y,
|
||||||
)
|
)
|
||||||
if region == null or region.fish_pool == null:
|
var region: FishableWaterRegion = surface.water_region
|
||||||
|
var authoritative_target: Vector3 = surface.position
|
||||||
|
if (
|
||||||
|
not surface.is_fishable()
|
||||||
|
or region == null
|
||||||
|
or region.fish_pool == null
|
||||||
|
or not _fishing_spot.is_cast_path_clear(
|
||||||
|
authoritative_origin,
|
||||||
|
authoritative_target
|
||||||
|
)
|
||||||
|
):
|
||||||
_record_and_reject(peer_id, request_id, "Cannot fish here.")
|
_record_and_reject(peer_id, request_id, "Cannot fish here.")
|
||||||
return
|
return
|
||||||
var effects: PlayerItemEffects = (
|
var effects: PlayerItemEffects = (
|
||||||
|
|
@ -281,8 +292,8 @@ func _handle_cast_request(peer_id: int, data: Dictionary) -> void:
|
||||||
attempt.session_id = _session.get_session_id()
|
attempt.session_id = _session.get_session_id()
|
||||||
attempt.phase = NetworkFishingAttempt.Phase.WAITING_FOR_BITE
|
attempt.phase = NetworkFishingAttempt.Phase.WAITING_FOR_BITE
|
||||||
attempt.origin = authoritative_origin
|
attempt.origin = authoritative_origin
|
||||||
attempt.target = target
|
attempt.target = authoritative_target
|
||||||
attempt.bobber_position = target
|
attempt.bobber_position = authoritative_target
|
||||||
attempt.fish_id = selected_fish.id
|
attempt.fish_id = selected_fish.id
|
||||||
attempt.reel_speed = float(data["reel_speed"]) * (
|
attempt.reel_speed = float(data["reel_speed"]) * (
|
||||||
effects.get_reel_multiplier() if effects != null else 1.0
|
effects.get_reel_multiplier() if effects != null else 1.0
|
||||||
|
|
@ -290,7 +301,7 @@ func _handle_cast_request(peer_id: int, data: Dictionary) -> void:
|
||||||
attempt.barrier_damage = int(data["barrier_damage"]) + (
|
attempt.barrier_damage = int(data["barrier_damage"]) + (
|
||||||
effects.get_barrier_bonus() if effects != null else 0
|
effects.get_barrier_bonus() if effects != null else 0
|
||||||
)
|
)
|
||||||
attempt.bite_time_remaining = _fishing_spot.wait_time * float(
|
attempt.bite_time_remaining = _fishing_spot.roll_bite_wait_time() * float(
|
||||||
effects.get_bite_time_multiplier() if effects != null else 1.0
|
effects.get_bite_time_multiplier() if effects != null else 1.0
|
||||||
)
|
)
|
||||||
attempt.controller = CatchController.new()
|
attempt.controller = CatchController.new()
|
||||||
|
|
@ -320,7 +331,7 @@ func _update_waiting_attempt(
|
||||||
flat_offset.length() - _fishing_spot.withdrawal_cancel_distance
|
flat_offset.length() - _fishing_spot.withdrawal_cancel_distance
|
||||||
)
|
)
|
||||||
if withdrawable_distance <= 0.0:
|
if withdrawable_distance <= 0.0:
|
||||||
_cancel_attempt(attempt.owner_peer_id, "")
|
_cancel_attempt(attempt.owner_peer_id, "", &"withdrawal")
|
||||||
return
|
return
|
||||||
attempt.withdrawal_progress = minf(
|
attempt.withdrawal_progress = minf(
|
||||||
attempt.withdrawal_progress
|
attempt.withdrawal_progress
|
||||||
|
|
@ -336,16 +347,20 @@ func _update_waiting_attempt(
|
||||||
endpoint,
|
endpoint,
|
||||||
attempt.withdrawal_progress
|
attempt.withdrawal_progress
|
||||||
)
|
)
|
||||||
attempt.bobber_position = _fishing_spot.find_last_fishable_position(
|
var withdrawal_surface: FishingSurfaceSample = (
|
||||||
|
_fishing_spot.resolve_safe_withdrawal_surface(
|
||||||
attempt.bobber_position,
|
attempt.bobber_position,
|
||||||
desired
|
desired,
|
||||||
|
endpoint - attempt.bobber_position,
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
if not withdrawal_surface.is_fishable():
|
||||||
|
_cancel_attempt(attempt.owner_peer_id, "", &"withdrawal")
|
||||||
|
return
|
||||||
|
attempt.bobber_position = withdrawal_surface.position
|
||||||
attempt.set_meta("snapshot", _make_waiting_snapshot(attempt))
|
attempt.set_meta("snapshot", _make_waiting_snapshot(attempt))
|
||||||
if (
|
if is_equal_approx(attempt.withdrawal_progress, 1.0):
|
||||||
not attempt.bobber_position.is_equal_approx(desired)
|
_cancel_attempt(attempt.owner_peer_id, "", &"withdrawal")
|
||||||
or is_equal_approx(attempt.withdrawal_progress, 1.0)
|
|
||||||
):
|
|
||||||
_cancel_attempt(attempt.owner_peer_id, "")
|
|
||||||
|
|
||||||
|
|
||||||
func _make_waiting_snapshot(
|
func _make_waiting_snapshot(
|
||||||
|
|
@ -479,7 +494,6 @@ func _on_encounter_updated(
|
||||||
var attempt: NetworkFishingAttempt = _attempts.get(peer_id)
|
var attempt: NetworkFishingAttempt = _attempts.get(peer_id)
|
||||||
if attempt == null or attempt.phase != NetworkFishingAttempt.Phase.FIGHTING:
|
if attempt == null or attempt.phase != NetworkFishingAttempt.Phase.FIGHTING:
|
||||||
return
|
return
|
||||||
attempt.bobber_position = attempt.target.lerp(attempt.origin, progress)
|
|
||||||
attempt.set_meta("snapshot", {
|
attempt.set_meta("snapshot", {
|
||||||
"attempt_id": attempt.attempt_id,
|
"attempt_id": attempt.attempt_id,
|
||||||
"owner_peer_id": peer_id,
|
"owner_peer_id": peer_id,
|
||||||
|
|
@ -606,7 +620,7 @@ func _on_attempt_caught(peer_id: int) -> void:
|
||||||
return
|
return
|
||||||
attempt.phase = NetworkFishingAttempt.Phase.PENDING_CAPACITY
|
attempt.phase = NetworkFishingAttempt.Phase.PENDING_CAPACITY
|
||||||
attempt.result_id = _new_id("result")
|
attempt.result_id = _new_id("result")
|
||||||
attempt.catch_payload = fish_catch.to_save_dict()
|
attempt.catch_payload = fish_catch.to_network_dict()
|
||||||
attempt.capacity_nonce = _new_id("capacity")
|
attempt.capacity_nonce = _new_id("capacity")
|
||||||
attempt.capacity_deadline = (
|
attempt.capacity_deadline = (
|
||||||
Time.get_ticks_msec() / 1000.0 + CAPACITY_RESPONSE_TIMEOUT
|
Time.get_ticks_msec() / 1000.0 + CAPACITY_RESPONSE_TIMEOUT
|
||||||
|
|
@ -708,7 +722,12 @@ func _finalize_catch(attempt: NetworkFishingAttempt) -> void:
|
||||||
_apply_target_outcome(outcome)
|
_apply_target_outcome(outcome)
|
||||||
else:
|
else:
|
||||||
receive_target_outcome.rpc_id(attempt.owner_peer_id, outcome)
|
receive_target_outcome.rpc_id(attempt.owner_peer_id, outcome)
|
||||||
_broadcast_public_outcome(attempt, &"catch", "")
|
_broadcast_public_outcome(
|
||||||
|
attempt,
|
||||||
|
&"catch",
|
||||||
|
"",
|
||||||
|
attempt.catch_payload,
|
||||||
|
)
|
||||||
_dispose_attempt(attempt.owner_peer_id)
|
_dispose_attempt(attempt.owner_peer_id)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -800,12 +819,16 @@ func _on_attempt_escaped(peer_id: int) -> void:
|
||||||
_dispose_attempt(peer_id)
|
_dispose_attempt(peer_id)
|
||||||
|
|
||||||
|
|
||||||
func _cancel_attempt(peer_id: int, message: String) -> void:
|
func _cancel_attempt(
|
||||||
|
peer_id: int,
|
||||||
|
message: String,
|
||||||
|
outcome: StringName = &"cancelled",
|
||||||
|
) -> void:
|
||||||
var attempt: NetworkFishingAttempt = _attempts.get(peer_id)
|
var attempt: NetworkFishingAttempt = _attempts.get(peer_id)
|
||||||
if attempt == null:
|
if attempt == null:
|
||||||
return
|
return
|
||||||
attempt.phase = NetworkFishingAttempt.Phase.CANCELLED
|
attempt.phase = NetworkFishingAttempt.Phase.CANCELLED
|
||||||
_broadcast_public_outcome(attempt, &"cancelled", message)
|
_broadcast_public_outcome(attempt, outcome, message)
|
||||||
_dispose_attempt(peer_id)
|
_dispose_attempt(peer_id)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -813,6 +836,7 @@ func _broadcast_public_outcome(
|
||||||
attempt: NetworkFishingAttempt,
|
attempt: NetworkFishingAttempt,
|
||||||
outcome: StringName,
|
outcome: StringName,
|
||||||
message: String,
|
message: String,
|
||||||
|
catch_payload: Dictionary = {},
|
||||||
) -> void:
|
) -> void:
|
||||||
var data: Dictionary = {
|
var data: Dictionary = {
|
||||||
"attempt_id": attempt.attempt_id,
|
"attempt_id": attempt.attempt_id,
|
||||||
|
|
@ -820,6 +844,8 @@ func _broadcast_public_outcome(
|
||||||
"outcome": str(outcome),
|
"outcome": str(outcome),
|
||||||
"message": message.left(128),
|
"message": message.left(128),
|
||||||
}
|
}
|
||||||
|
if outcome == &"catch" and not catch_payload.is_empty():
|
||||||
|
data["catch"] = catch_payload.duplicate(true)
|
||||||
_apply_public_outcome(data)
|
_apply_public_outcome(data)
|
||||||
receive_public_outcome.rpc(data)
|
receive_public_outcome.rpc(data)
|
||||||
|
|
||||||
|
|
@ -845,6 +871,10 @@ func _apply_public_outcome(data: Dictionary) -> void:
|
||||||
StringName(str(data["outcome"])),
|
StringName(str(data["outcome"])),
|
||||||
str(data["message"])
|
str(data["message"])
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
var outcome := StringName(str(data["outcome"]))
|
||||||
|
if outcome in [&"catch", &"escape", &"withdrawal"]:
|
||||||
|
_return_remote_presentation(peer_id, outcome, data)
|
||||||
else:
|
else:
|
||||||
_cleanup_remote_presentation(peer_id)
|
_cleanup_remote_presentation(peer_id)
|
||||||
|
|
||||||
|
|
@ -874,12 +904,16 @@ func _apply_cast_accepted(data: Dictionary) -> void:
|
||||||
if (
|
if (
|
||||||
typeof(data.get("attempt_id")) != TYPE_STRING
|
typeof(data.get("attempt_id")) != TYPE_STRING
|
||||||
or typeof(data.get("owner_peer_id")) != TYPE_INT
|
or typeof(data.get("owner_peer_id")) != TYPE_INT
|
||||||
|
or typeof(data.get("origin")) != TYPE_ARRAY
|
||||||
or typeof(data.get("target")) != TYPE_ARRAY
|
or typeof(data.get("target")) != TYPE_ARRAY
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
var peer_id: int = data["owner_peer_id"]
|
var peer_id: int = data["owner_peer_id"]
|
||||||
|
var origin: Vector3 = NetworkFishingProtocol.array_to_vector3(
|
||||||
|
data.get("origin", [])
|
||||||
|
)
|
||||||
var target: Vector3 = NetworkFishingProtocol.array_to_vector3(data["target"])
|
var target: Vector3 = NetworkFishingProtocol.array_to_vector3(data["target"])
|
||||||
if not target.is_finite():
|
if not origin.is_finite() or not target.is_finite():
|
||||||
return
|
return
|
||||||
if peer_id == _session.get_local_peer_id():
|
if peer_id == _session.get_local_peer_id():
|
||||||
var attempt := NetworkFishingAttempt.new()
|
var attempt := NetworkFishingAttempt.new()
|
||||||
|
|
@ -896,7 +930,7 @@ func _apply_cast_accepted(data: Dictionary) -> void:
|
||||||
else:
|
else:
|
||||||
var presentation := _get_remote_presentation(peer_id)
|
var presentation := _get_remote_presentation(peer_id)
|
||||||
if presentation != null:
|
if presentation != null:
|
||||||
presentation.show_cast(target)
|
presentation.show_cast(origin, target)
|
||||||
|
|
||||||
|
|
||||||
@rpc("authority", "call_remote", "reliable", 0)
|
@rpc("authority", "call_remote", "reliable", 0)
|
||||||
|
|
@ -1000,6 +1034,30 @@ func _cleanup_remote_presentation(peer_id: int) -> void:
|
||||||
presentation.queue_free()
|
presentation.queue_free()
|
||||||
|
|
||||||
|
|
||||||
|
func _return_remote_presentation(
|
||||||
|
peer_id: int,
|
||||||
|
outcome: StringName,
|
||||||
|
data: Dictionary,
|
||||||
|
) -> void:
|
||||||
|
var presentation: RemoteFishingPresentation = _remote_presentations.get(
|
||||||
|
peer_id
|
||||||
|
)
|
||||||
|
_remote_presentations.erase(peer_id)
|
||||||
|
if presentation == null or not is_instance_valid(presentation):
|
||||||
|
return
|
||||||
|
presentation.return_completed.connect(
|
||||||
|
presentation.queue_free,
|
||||||
|
CONNECT_ONE_SHOT,
|
||||||
|
)
|
||||||
|
var showcase_catch: FishCatch = null
|
||||||
|
if outcome == &"catch" and typeof(data.get("catch")) == TYPE_DICTIONARY:
|
||||||
|
var catch_data: Dictionary = data["catch"]
|
||||||
|
var fish_id := StringName(str(catch_data.get("fish_id", "")))
|
||||||
|
var fish: FishDataType = _fish_catalog.get_fish_by_id(fish_id)
|
||||||
|
showcase_catch = FishCatchType.from_network_dict(catch_data, fish)
|
||||||
|
presentation.play_return(showcase_catch)
|
||||||
|
|
||||||
|
|
||||||
func _dispose_attempt(peer_id: int) -> void:
|
func _dispose_attempt(peer_id: int) -> void:
|
||||||
var attempt: NetworkFishingAttempt = _attempts.get(peer_id)
|
var attempt: NetworkFishingAttempt = _attempts.get(peer_id)
|
||||||
_attempts.erase(peer_id)
|
_attempts.erase(peer_id)
|
||||||
|
|
@ -1007,7 +1065,12 @@ func _dispose_attempt(peer_id: int) -> void:
|
||||||
attempt.controller.reset()
|
attempt.controller.reset()
|
||||||
attempt.controller.queue_free()
|
attempt.controller.queue_free()
|
||||||
var avatar: Player = _spawn_service.get_avatar(peer_id)
|
var avatar: Player = _spawn_service.get_avatar(peer_id)
|
||||||
if avatar != null:
|
var local_return_is_active: bool = (
|
||||||
|
peer_id == _session.get_local_peer_id()
|
||||||
|
and _fishing_spot != null
|
||||||
|
and _fishing_spot.is_returning()
|
||||||
|
)
|
||||||
|
if avatar != null and not local_return_is_active:
|
||||||
avatar.set_movement_enabled(true)
|
avatar.set_movement_enabled(true)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -713,6 +713,22 @@ func begin_catch_showcase(fish_catch: FishCatchType) -> void:
|
||||||
_catch_display.visible = _catch_sprite.texture != null
|
_catch_display.visible = _catch_sprite.texture != null
|
||||||
|
|
||||||
|
|
||||||
|
func begin_remote_catch_showcase(fish_catch: FishCatchType) -> void:
|
||||||
|
if local_control_enabled or fish_catch == null or not fish_catch.is_valid():
|
||||||
|
return
|
||||||
|
end_catch_showcase(Callable(), true)
|
||||||
|
_showcase_rod_visibility = _fishing_rod.visible
|
||||||
|
_showcase_rod_state_stored = true
|
||||||
|
_fishing_rod.visible = false
|
||||||
|
_catch_sprite.texture = fish_catch.fish.display_texture
|
||||||
|
_catch_display.scale = (
|
||||||
|
Vector3.ONE
|
||||||
|
* fish_catch.display_scale
|
||||||
|
* catch_presentation_base_scale
|
||||||
|
)
|
||||||
|
_catch_display.visible = _catch_sprite.texture != null
|
||||||
|
|
||||||
|
|
||||||
func end_catch_showcase(
|
func end_catch_showcase(
|
||||||
restored_callback: Callable = Callable(),
|
restored_callback: Callable = Callable(),
|
||||||
immediate: bool = false,
|
immediate: bool = false,
|
||||||
|
|
|
||||||
104
tests/fishing_authority_validation.gd
Normal file
104
tests/fishing_authority_validation.gd
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
const MainScene = preload("res://main/main.tscn")
|
||||||
|
const FishingSpotType = preload("res://fishing/fishing_spot.gd")
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
call_deferred("_run")
|
||||||
|
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
root.size = Vector2i(1280, 720)
|
||||||
|
var main := MainScene.instantiate()
|
||||||
|
root.add_child(main)
|
||||||
|
for _frame: int in 4:
|
||||||
|
await process_frame
|
||||||
|
if not bool(main.get("_application_initialized")):
|
||||||
|
main.call("_activate_selected_data_path", "", true)
|
||||||
|
for _frame: int in 8:
|
||||||
|
await process_frame
|
||||||
|
assert(bool(main.get("_application_initialized")))
|
||||||
|
assert(bool(main.call("_prepare_private_host")))
|
||||||
|
var save_manager := main.get("_save_manager") as PlayerSaveManager
|
||||||
|
assert(save_manager.initialize_new_game())
|
||||||
|
main.call("_enter_gameplay")
|
||||||
|
for _frame: int in 8:
|
||||||
|
await process_frame
|
||||||
|
|
||||||
|
var session := main.get_node("%NetworkSession") as NetworkSession
|
||||||
|
var service := main.get_node(
|
||||||
|
"%NetworkFishingService"
|
||||||
|
) as NetworkFishingService
|
||||||
|
var fishing_spot := main.get_node("%FishingSpot") as FishingSpotType
|
||||||
|
var player := main.get("_player") as Player
|
||||||
|
assert(session.is_host())
|
||||||
|
assert(not session.is_open_host())
|
||||||
|
assert(service != null and fishing_spot != null and player != null)
|
||||||
|
assert(player.hotbar.get_selected_item_id() == &"basic_fishing_rod")
|
||||||
|
|
||||||
|
player.global_position = Vector3(-0.5, 3.95, 2.1)
|
||||||
|
var visuals := player.get_node("Visuals") as Node3D
|
||||||
|
visuals.rotation.y = PI * 0.5
|
||||||
|
for _frame: int in 4:
|
||||||
|
await physics_frame
|
||||||
|
assert(player.get_facing_direction().dot(Vector3.LEFT) > 0.99)
|
||||||
|
|
||||||
|
fishing_spot.call("_begin_aiming", player)
|
||||||
|
assert(fishing_spot.state == FishingSpotType.FishingState.AIMING_CAST)
|
||||||
|
assert(player.is_movement_enabled())
|
||||||
|
fishing_spot.set("_cast_charge", 0.32)
|
||||||
|
fishing_spot.call("_update_cast_charge", 0.0)
|
||||||
|
var aimed_target: Vector3 = fishing_spot.get("_cast_target")
|
||||||
|
assert(aimed_target.x < -1.35)
|
||||||
|
assert(is_equal_approx(aimed_target.y, 2.51))
|
||||||
|
assert(fishing_spot.is_target_fishable(aimed_target))
|
||||||
|
fishing_spot.call("_confirm_cast")
|
||||||
|
assert(fishing_spot.state == FishingSpotType.FishingState.CASTING)
|
||||||
|
assert(not player.is_movement_enabled())
|
||||||
|
|
||||||
|
var wait_deadline: int = Time.get_ticks_msec() + 4000
|
||||||
|
while (
|
||||||
|
Time.get_ticks_msec() < wait_deadline
|
||||||
|
and fishing_spot.state != FishingSpotType.FishingState.WAITING_FOR_BITE
|
||||||
|
):
|
||||||
|
await process_frame
|
||||||
|
assert(fishing_spot.state == FishingSpotType.FishingState.WAITING_FOR_BITE)
|
||||||
|
assert(service.has_local_attempt())
|
||||||
|
var attempts: Dictionary = service.get("_attempts")
|
||||||
|
var attempt: NetworkFishingAttempt = attempts.get(session.get_local_peer_id())
|
||||||
|
assert(attempt != null)
|
||||||
|
assert(is_equal_approx(attempt.target.y, 2.51))
|
||||||
|
assert(attempt.bobber_position.is_equal_approx(attempt.target))
|
||||||
|
|
||||||
|
fishing_spot.set("_withdrawal_input_held", true)
|
||||||
|
fishing_spot.set("_network_primary_input_held", true)
|
||||||
|
service.submit_local_input(true, true)
|
||||||
|
var return_deadline: int = Time.get_ticks_msec() + 4000
|
||||||
|
while (
|
||||||
|
Time.get_ticks_msec() < return_deadline
|
||||||
|
and fishing_spot.state != FishingSpotType.FishingState.RETURNING
|
||||||
|
):
|
||||||
|
await process_frame
|
||||||
|
assert(fishing_spot.state == FishingSpotType.FishingState.RETURNING)
|
||||||
|
assert(not service.has_local_attempt())
|
||||||
|
var bobber := fishing_spot.get_node(
|
||||||
|
"FishingPresentation/Bobber"
|
||||||
|
) as MeshInstance3D
|
||||||
|
assert(bobber.visible)
|
||||||
|
|
||||||
|
var ready_deadline: int = Time.get_ticks_msec() + 3000
|
||||||
|
while (
|
||||||
|
Time.get_ticks_msec() < ready_deadline
|
||||||
|
and fishing_spot.state != FishingSpotType.FishingState.READY
|
||||||
|
):
|
||||||
|
await process_frame
|
||||||
|
assert(fishing_spot.state == FishingSpotType.FishingState.READY)
|
||||||
|
assert(player.is_movement_enabled())
|
||||||
|
assert(not bobber.visible)
|
||||||
|
|
||||||
|
print("Fishing authority validation: PASS")
|
||||||
|
session.disconnect_session("")
|
||||||
|
main.queue_free()
|
||||||
|
await process_frame
|
||||||
|
quit()
|
||||||
1
tests/fishing_authority_validation.gd.uid
Normal file
1
tests/fishing_authority_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://cgqcyldad6dsj
|
||||||
194
tests/fishing_multiplayer_validation.gd
Normal file
194
tests/fishing_multiplayer_validation.gd
Normal file
|
|
@ -0,0 +1,194 @@
|
||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
const MainScene = preload("res://main/main.tscn")
|
||||||
|
const FishingSpotType = preload("res://fishing/fishing_spot.gd")
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
call_deferred("_run")
|
||||||
|
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
var arguments: PackedStringArray = OS.get_cmdline_user_args()
|
||||||
|
if arguments.has("host"):
|
||||||
|
await _run_host()
|
||||||
|
return
|
||||||
|
if arguments.has("client"):
|
||||||
|
await _run_client()
|
||||||
|
return
|
||||||
|
push_error("Fishing multiplayer validation requires host or client mode.")
|
||||||
|
quit(1)
|
||||||
|
|
||||||
|
|
||||||
|
func _run_host() -> void:
|
||||||
|
var main: Node = await _create_initialized_main()
|
||||||
|
assert(bool(main.call("_prepare_private_host")))
|
||||||
|
var save_manager := main.get("_save_manager") as PlayerSaveManager
|
||||||
|
assert(save_manager.initialize_new_game())
|
||||||
|
main.call("_enter_gameplay")
|
||||||
|
var session := main.get_node("%NetworkSession") as NetworkSession
|
||||||
|
assert(session.set_host_open(true))
|
||||||
|
var service := main.get_node(
|
||||||
|
"%NetworkFishingService"
|
||||||
|
) as NetworkFishingService
|
||||||
|
|
||||||
|
var join_deadline: int = Time.get_ticks_msec() + 20000
|
||||||
|
while (
|
||||||
|
Time.get_ticks_msec() < join_deadline
|
||||||
|
and session.get_authenticated_peer_ids().size() < 2
|
||||||
|
):
|
||||||
|
await process_frame
|
||||||
|
assert(session.get_authenticated_peer_ids().size() == 2)
|
||||||
|
var remote_peer_id: int = 0
|
||||||
|
for peer_id: int in session.get_authenticated_peer_ids():
|
||||||
|
if peer_id != session.get_local_peer_id():
|
||||||
|
remote_peer_id = peer_id
|
||||||
|
break
|
||||||
|
assert(remote_peer_id > 1)
|
||||||
|
var spawn_service := main.get_node(
|
||||||
|
"%PlayerSpawnService"
|
||||||
|
) as PlayerSpawnService
|
||||||
|
var remote_avatar: Player = spawn_service.get_avatar(remote_peer_id)
|
||||||
|
assert(remote_avatar != null)
|
||||||
|
remote_avatar.global_position = Vector3(-0.5, 3.95, 2.1)
|
||||||
|
var remote_visuals := remote_avatar.get_node("Visuals") as Node3D
|
||||||
|
remote_visuals.rotation.y = PI * 0.5
|
||||||
|
session.publish_authoritative_teleport(remote_peer_id)
|
||||||
|
|
||||||
|
var saw_remote_attempt: bool = false
|
||||||
|
var remote_presentation: RemoteFishingPresentation
|
||||||
|
var attempt_deadline: int = Time.get_ticks_msec() + 12000
|
||||||
|
while Time.get_ticks_msec() < attempt_deadline:
|
||||||
|
await process_frame
|
||||||
|
var attempts: Dictionary = service.get("_attempts")
|
||||||
|
for peer_id: int in attempts:
|
||||||
|
if peer_id == session.get_local_peer_id():
|
||||||
|
continue
|
||||||
|
var attempt: NetworkFishingAttempt = attempts[peer_id]
|
||||||
|
assert(is_equal_approx(attempt.target.y, 2.51))
|
||||||
|
assert(attempt.bobber_position.y <= 2.51 + 0.001)
|
||||||
|
var presentations: Dictionary = service.get("_remote_presentations")
|
||||||
|
remote_presentation = presentations.get(peer_id)
|
||||||
|
assert(remote_presentation != null)
|
||||||
|
var remote_bobber := remote_presentation.get(
|
||||||
|
"_bobber"
|
||||||
|
) as MeshInstance3D
|
||||||
|
assert(remote_bobber.visible)
|
||||||
|
saw_remote_attempt = true
|
||||||
|
if saw_remote_attempt:
|
||||||
|
break
|
||||||
|
assert(saw_remote_attempt)
|
||||||
|
var return_deadline: int = Time.get_ticks_msec() + 5000
|
||||||
|
while (
|
||||||
|
Time.get_ticks_msec() < return_deadline
|
||||||
|
and service.has_peer_attempt(remote_peer_id)
|
||||||
|
):
|
||||||
|
await process_frame
|
||||||
|
assert(not service.has_peer_attempt(remote_peer_id))
|
||||||
|
assert(is_instance_valid(remote_presentation))
|
||||||
|
assert(remote_presentation.get("_return_tween") != null)
|
||||||
|
|
||||||
|
var completion_deadline: int = Time.get_ticks_msec() + 12000
|
||||||
|
while (
|
||||||
|
Time.get_ticks_msec() < completion_deadline
|
||||||
|
and session.get_authenticated_peer_ids().size() == 2
|
||||||
|
):
|
||||||
|
await process_frame
|
||||||
|
print("Fishing multiplayer host validation: PASS")
|
||||||
|
session.disconnect_session("")
|
||||||
|
main.queue_free()
|
||||||
|
await process_frame
|
||||||
|
quit()
|
||||||
|
|
||||||
|
|
||||||
|
func _run_client() -> void:
|
||||||
|
var main: Node = await _create_initialized_main()
|
||||||
|
main.call("_on_title_join_game_requested", "127.0.0.1:7777")
|
||||||
|
var session := main.get_node("%NetworkSession") as NetworkSession
|
||||||
|
var joined: bool = false
|
||||||
|
var join_deadline: int = Time.get_ticks_msec() + 20000
|
||||||
|
while Time.get_ticks_msec() < join_deadline:
|
||||||
|
await process_frame
|
||||||
|
if session.state == NetworkSession.State.VERIFYING_SERVER_IDENTITY:
|
||||||
|
main.call("_confirm_server_trust")
|
||||||
|
if session.is_joined_client() and bool(main.get("_gameplay_started")):
|
||||||
|
joined = true
|
||||||
|
break
|
||||||
|
assert(joined)
|
||||||
|
|
||||||
|
var service := main.get_node(
|
||||||
|
"%NetworkFishingService"
|
||||||
|
) as NetworkFishingService
|
||||||
|
var fishing_spot := main.get_node("%FishingSpot") as FishingSpotType
|
||||||
|
var player := main.get("_player") as Player
|
||||||
|
var placement_deadline: int = Time.get_ticks_msec() + 5000
|
||||||
|
while (
|
||||||
|
Time.get_ticks_msec() < placement_deadline
|
||||||
|
and player.global_position.distance_to(Vector3(-0.5, 3.95, 2.1))
|
||||||
|
> 0.25
|
||||||
|
):
|
||||||
|
await physics_frame
|
||||||
|
assert(player.global_position.distance_to(Vector3(-0.5, 3.95, 2.1)) <= 0.25)
|
||||||
|
assert(player.get_facing_direction().dot(Vector3.LEFT) > 0.99)
|
||||||
|
|
||||||
|
fishing_spot.call("_begin_aiming", player)
|
||||||
|
assert(player.is_movement_enabled())
|
||||||
|
fishing_spot.set("_cast_charge", 0.32)
|
||||||
|
fishing_spot.call("_update_cast_charge", 0.0)
|
||||||
|
var target: Vector3 = fishing_spot.get("_cast_target")
|
||||||
|
assert(fishing_spot.is_target_fishable(target))
|
||||||
|
assert(is_equal_approx(target.y, 2.51))
|
||||||
|
fishing_spot.call("_confirm_cast")
|
||||||
|
|
||||||
|
var accepted_deadline: int = Time.get_ticks_msec() + 6000
|
||||||
|
while (
|
||||||
|
Time.get_ticks_msec() < accepted_deadline
|
||||||
|
and fishing_spot.state != FishingSpotType.FishingState.WAITING_FOR_BITE
|
||||||
|
):
|
||||||
|
await process_frame
|
||||||
|
assert(fishing_spot.state == FishingSpotType.FishingState.WAITING_FOR_BITE)
|
||||||
|
assert(service.has_local_attempt())
|
||||||
|
fishing_spot.set("_withdrawal_input_held", true)
|
||||||
|
fishing_spot.set("_network_primary_input_held", true)
|
||||||
|
service.submit_local_input(true, true)
|
||||||
|
|
||||||
|
var return_deadline: int = Time.get_ticks_msec() + 5000
|
||||||
|
while (
|
||||||
|
Time.get_ticks_msec() < return_deadline
|
||||||
|
and fishing_spot.state != FishingSpotType.FishingState.RETURNING
|
||||||
|
):
|
||||||
|
await process_frame
|
||||||
|
assert(fishing_spot.state == FishingSpotType.FishingState.RETURNING)
|
||||||
|
var bobber := fishing_spot.get_node(
|
||||||
|
"FishingPresentation/Bobber"
|
||||||
|
) as MeshInstance3D
|
||||||
|
assert(bobber.visible)
|
||||||
|
|
||||||
|
var ready_deadline: int = Time.get_ticks_msec() + 3000
|
||||||
|
while (
|
||||||
|
Time.get_ticks_msec() < ready_deadline
|
||||||
|
and fishing_spot.state != FishingSpotType.FishingState.READY
|
||||||
|
):
|
||||||
|
await process_frame
|
||||||
|
assert(fishing_spot.state == FishingSpotType.FishingState.READY)
|
||||||
|
assert(player.is_movement_enabled())
|
||||||
|
assert(not bobber.visible)
|
||||||
|
print("Fishing multiplayer client validation: PASS")
|
||||||
|
session.disconnect_session("")
|
||||||
|
main.queue_free()
|
||||||
|
await process_frame
|
||||||
|
quit()
|
||||||
|
|
||||||
|
|
||||||
|
func _create_initialized_main() -> Node:
|
||||||
|
root.size = Vector2i(1280, 720)
|
||||||
|
var main := MainScene.instantiate()
|
||||||
|
root.add_child(main)
|
||||||
|
for _frame: int in 4:
|
||||||
|
await process_frame
|
||||||
|
if not bool(main.get("_application_initialized")):
|
||||||
|
main.call("_activate_selected_data_path", "", true)
|
||||||
|
for _frame: int in 8:
|
||||||
|
await process_frame
|
||||||
|
assert(bool(main.get("_application_initialized")))
|
||||||
|
return main
|
||||||
1
tests/fishing_multiplayer_validation.gd.uid
Normal file
1
tests/fishing_multiplayer_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://chyq7wjhpko7d
|
||||||
337
tests/fishing_surface_validation.gd
Normal file
337
tests/fishing_surface_validation.gd
Normal file
|
|
@ -0,0 +1,337 @@
|
||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
const FishingSpotScene = preload("res://fishing/fishing_spot.tscn")
|
||||||
|
const FishingSpotType = preload("res://fishing/fishing_spot.gd")
|
||||||
|
const FishingPresentationType = preload(
|
||||||
|
"res://fishing/fishing_presentation.gd"
|
||||||
|
)
|
||||||
|
const RemoteFishingPresentationType = preload(
|
||||||
|
"res://fishing/remote_fishing_presentation.gd"
|
||||||
|
)
|
||||||
|
const PlayerScene = preload("res://player/player.tscn")
|
||||||
|
const FishCatchType = preload("res://fish/fish_catch.gd")
|
||||||
|
const FishingSurfaceResolverType = preload(
|
||||||
|
"res://fishing/fishing_surface_resolver.gd"
|
||||||
|
)
|
||||||
|
const FishingSurfaceSampleType = preload(
|
||||||
|
"res://fishing/fishing_surface_sample.gd"
|
||||||
|
)
|
||||||
|
const FishableWaterRegionType = preload(
|
||||||
|
"res://world/fishable_water_region.gd"
|
||||||
|
)
|
||||||
|
const FishPoolType = preload("res://fish/fish_pool.gd")
|
||||||
|
const WaterBodyScene = preload("res://world/water_body.tscn")
|
||||||
|
const StarterRegionScene = preload(
|
||||||
|
"res://world/regions/starter_island_region.tscn"
|
||||||
|
)
|
||||||
|
const PondPool: FishPoolType = preload(
|
||||||
|
"res://fish/pools/starter_pond_pool.tres"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
call_deferred("_run")
|
||||||
|
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
await _validate_resolver_layers()
|
||||||
|
await _validate_portable_water_body()
|
||||||
|
await _validate_starter_region_surfaces()
|
||||||
|
await _validate_presentation()
|
||||||
|
await _validate_remote_presentation()
|
||||||
|
await _validate_bite_wait_distribution()
|
||||||
|
print("Fishing surface validation: PASS")
|
||||||
|
quit()
|
||||||
|
|
||||||
|
|
||||||
|
func _validate_resolver_layers() -> void:
|
||||||
|
var world := Node3D.new()
|
||||||
|
root.add_child(world)
|
||||||
|
_add_solid_box(world, Vector3(40.0, 2.0, 12.0), Vector3(10.0, -1.0, 0.0))
|
||||||
|
_add_water_region(world, Vector3(0.0, 2.0, 0.0), Vector2(8.0, 8.0), PondPool)
|
||||||
|
_add_solid_box(world, Vector3(1.0, 1.0, 1.0), Vector3(2.0, 2.25, 0.0))
|
||||||
|
_add_water_region(world, Vector3(10.0, 5.0, 0.0), Vector2(6.0, 6.0), PondPool)
|
||||||
|
_add_water_region(world, Vector3(20.0, 3.0, 0.0), Vector2(6.0, 6.0), null)
|
||||||
|
await physics_frame
|
||||||
|
await physics_frame
|
||||||
|
|
||||||
|
var resolver := FishingSurfaceResolverType.new()
|
||||||
|
var space_state: PhysicsDirectSpaceState3D = world.get_world_3d().direct_space_state
|
||||||
|
var water: FishingSurfaceSampleType = resolver.resolve_surface(
|
||||||
|
space_state,
|
||||||
|
Vector3.ZERO,
|
||||||
|
4.0,
|
||||||
|
)
|
||||||
|
assert(water.is_fishable())
|
||||||
|
assert(is_equal_approx(water.position.y, 2.0))
|
||||||
|
|
||||||
|
var covered_water: FishingSurfaceSampleType = resolver.resolve_surface(
|
||||||
|
space_state,
|
||||||
|
Vector3(2.0, 2.0, 0.0),
|
||||||
|
4.0,
|
||||||
|
)
|
||||||
|
assert(covered_water.has_surface)
|
||||||
|
assert(not covered_water.is_water_surface)
|
||||||
|
assert(is_equal_approx(covered_water.position.y, 2.75))
|
||||||
|
|
||||||
|
var elevated_water: FishingSurfaceSampleType = resolver.resolve_surface(
|
||||||
|
space_state,
|
||||||
|
Vector3(10.0, 0.0, 0.0),
|
||||||
|
8.0,
|
||||||
|
)
|
||||||
|
assert(elevated_water.is_fishable())
|
||||||
|
assert(is_equal_approx(elevated_water.position.y, 5.0))
|
||||||
|
|
||||||
|
var unstocked_water: FishingSurfaceSampleType = resolver.resolve_surface(
|
||||||
|
space_state,
|
||||||
|
Vector3(20.0, 0.0, 0.0),
|
||||||
|
6.0,
|
||||||
|
)
|
||||||
|
assert(unstocked_water.has_surface)
|
||||||
|
assert(unstocked_water.is_water_surface)
|
||||||
|
assert(not unstocked_water.is_fishable())
|
||||||
|
assert(is_equal_approx(unstocked_water.position.y, 3.0))
|
||||||
|
|
||||||
|
var safe_reel: FishingSurfaceSampleType = resolver.resolve_withdrawal_surface(
|
||||||
|
space_state,
|
||||||
|
Vector3(0.0, 2.0, 0.0),
|
||||||
|
Vector3(3.0, 2.0, 0.0),
|
||||||
|
Vector3.RIGHT,
|
||||||
|
4.0,
|
||||||
|
0.4,
|
||||||
|
)
|
||||||
|
assert(safe_reel.is_fishable())
|
||||||
|
assert(is_equal_approx(safe_reel.position.y, 2.0))
|
||||||
|
var shoreline_reel: FishingSurfaceSampleType = (
|
||||||
|
resolver.resolve_withdrawal_surface(
|
||||||
|
space_state,
|
||||||
|
Vector3(3.0, 2.0, 0.0),
|
||||||
|
Vector3(3.7, 2.0, 0.0),
|
||||||
|
Vector3.RIGHT,
|
||||||
|
4.0,
|
||||||
|
0.4,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert(not shoreline_reel.is_fishable())
|
||||||
|
assert(shoreline_reel.position.is_equal_approx(Vector3(3.0, 2.0, 0.0)))
|
||||||
|
|
||||||
|
world.queue_free()
|
||||||
|
await process_frame
|
||||||
|
|
||||||
|
|
||||||
|
func _validate_portable_water_body() -> void:
|
||||||
|
var water_body := WaterBodyScene.instantiate() as Node3D
|
||||||
|
water_body.position = Vector3(3.0, 7.0, -2.0)
|
||||||
|
water_body.set("surface_size", Vector2(12.0, 8.0))
|
||||||
|
water_body.set("fishing_depth", 6.0)
|
||||||
|
water_body.set("recovery_depth", 7.0)
|
||||||
|
water_body.set("fish_pool", PondPool)
|
||||||
|
root.add_child(water_body)
|
||||||
|
await process_frame
|
||||||
|
|
||||||
|
var visual := water_body.get_node("VisualWater") as MeshInstance3D
|
||||||
|
var visual_mesh := visual.mesh as PlaneMesh
|
||||||
|
assert(visual_mesh.size.is_equal_approx(Vector2(12.0, 8.0)))
|
||||||
|
var fishing_region := water_body.get_node(
|
||||||
|
"FishingRegion"
|
||||||
|
) as FishableWaterRegionType
|
||||||
|
var fishing_shape_node := water_body.get_node(
|
||||||
|
"FishingRegion/Shape"
|
||||||
|
) as CollisionShape3D
|
||||||
|
var fishing_shape := fishing_shape_node.shape as BoxShape3D
|
||||||
|
assert(fishing_region.fish_pool == PondPool)
|
||||||
|
assert(is_equal_approx(fishing_region.get_surface_height(), 7.0))
|
||||||
|
assert(is_equal_approx(fishing_shape_node.position.y, -3.0))
|
||||||
|
assert(fishing_shape.size.is_equal_approx(Vector3(12.0, 6.0, 8.0)))
|
||||||
|
var recovery_region := water_body.get_node("RecoveryRegion") as Area3D
|
||||||
|
assert(is_equal_approx(recovery_region.position.y, -3.5))
|
||||||
|
|
||||||
|
water_body.queue_free()
|
||||||
|
await process_frame
|
||||||
|
|
||||||
|
|
||||||
|
func _validate_starter_region_surfaces() -> void:
|
||||||
|
var region := StarterRegionScene.instantiate() as Node3D
|
||||||
|
root.add_child(region)
|
||||||
|
var fishing_spot := FishingSpotScene.instantiate() as FishingSpotType
|
||||||
|
root.add_child(fishing_spot)
|
||||||
|
await physics_frame
|
||||||
|
await physics_frame
|
||||||
|
|
||||||
|
var pond: FishingSurfaceSampleType = fishing_spot.resolve_fishing_surface(
|
||||||
|
Vector3(-9.4, 2.51, 2.1),
|
||||||
|
4.0,
|
||||||
|
)
|
||||||
|
assert(pond.is_fishable())
|
||||||
|
assert(is_equal_approx(pond.position.y, 2.51))
|
||||||
|
var ocean: FishingSurfaceSampleType = fishing_spot.resolve_fishing_surface(
|
||||||
|
Vector3(50.0, -0.45, 0.0),
|
||||||
|
4.0,
|
||||||
|
)
|
||||||
|
assert(ocean.is_fishable())
|
||||||
|
assert(is_equal_approx(ocean.position.y, -0.45))
|
||||||
|
var covered_ocean: FishingSurfaceSampleType = (
|
||||||
|
fishing_spot.resolve_fishing_surface(
|
||||||
|
Vector3(20.0, -0.45, 0.0),
|
||||||
|
4.0,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert(covered_ocean.has_surface)
|
||||||
|
assert(not covered_ocean.is_fishable())
|
||||||
|
assert(covered_ocean.position.y > -0.45)
|
||||||
|
|
||||||
|
fishing_spot.queue_free()
|
||||||
|
region.queue_free()
|
||||||
|
await process_frame
|
||||||
|
|
||||||
|
|
||||||
|
func _validate_presentation() -> void:
|
||||||
|
var fishing_spot := FishingSpotScene.instantiate() as FishingSpotType
|
||||||
|
root.add_child(fishing_spot)
|
||||||
|
var presentation := fishing_spot.get_node(
|
||||||
|
"FishingPresentation"
|
||||||
|
) as FishingPresentationType
|
||||||
|
var rod := Node3D.new()
|
||||||
|
var rod_tip := Marker3D.new()
|
||||||
|
rod.add_child(rod_tip)
|
||||||
|
root.add_child(rod)
|
||||||
|
rod_tip.position = Vector3(0.0, 1.0, 0.0)
|
||||||
|
await process_frame
|
||||||
|
|
||||||
|
var slope_normal := Vector3(0.0, 1.0, 1.0).normalized()
|
||||||
|
presentation.begin_aim(
|
||||||
|
rod_tip,
|
||||||
|
rod,
|
||||||
|
Vector3(0.0, 0.1, -1.0),
|
||||||
|
slope_normal,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
var marker := presentation.get_node("CastTargetMarker") as Node3D
|
||||||
|
var valid_marker := presentation.get_node(
|
||||||
|
"CastTargetMarker/ValidTargetMarker"
|
||||||
|
) as MeshInstance3D
|
||||||
|
var invalid_marker := presentation.get_node(
|
||||||
|
"CastTargetMarker/InvalidTargetMarker"
|
||||||
|
) as Node3D
|
||||||
|
assert(not valid_marker.visible)
|
||||||
|
assert(invalid_marker.visible)
|
||||||
|
assert(invalid_marker.get_child_count() == 2)
|
||||||
|
assert(marker.global_basis.y.normalized().dot(slope_normal) > 0.999)
|
||||||
|
|
||||||
|
var landing := Vector3(0.0, 0.13, -4.0)
|
||||||
|
presentation.begin_cast(landing, landing, true)
|
||||||
|
await presentation.cast_completed
|
||||||
|
var bobber := presentation.get_node("Bobber") as MeshInstance3D
|
||||||
|
assert(bobber.visible)
|
||||||
|
assert(bobber.global_position.is_equal_approx(landing))
|
||||||
|
presentation.play_outcome(&"invalid")
|
||||||
|
await process_frame
|
||||||
|
assert(bobber.visible)
|
||||||
|
await presentation.outcome_completed
|
||||||
|
assert(not bobber.visible)
|
||||||
|
|
||||||
|
rod.queue_free()
|
||||||
|
fishing_spot.queue_free()
|
||||||
|
await process_frame
|
||||||
|
|
||||||
|
|
||||||
|
func _validate_bite_wait_distribution() -> void:
|
||||||
|
var fishing_spot := FishingSpotScene.instantiate() as FishingSpotType
|
||||||
|
root.add_child(fishing_spot)
|
||||||
|
await process_frame
|
||||||
|
var quick_count: int = 0
|
||||||
|
var typical_or_long_count: int = 0
|
||||||
|
for _sample_index: int in 10000:
|
||||||
|
var wait_seconds: float = fishing_spot.roll_bite_wait_time()
|
||||||
|
assert(wait_seconds >= 10.0)
|
||||||
|
assert(wait_seconds <= 240.0)
|
||||||
|
if wait_seconds < 30.0:
|
||||||
|
quick_count += 1
|
||||||
|
else:
|
||||||
|
typical_or_long_count += 1
|
||||||
|
assert(quick_count > 0)
|
||||||
|
assert(typical_or_long_count > quick_count)
|
||||||
|
fishing_spot.queue_free()
|
||||||
|
await process_frame
|
||||||
|
|
||||||
|
|
||||||
|
func _validate_remote_presentation() -> void:
|
||||||
|
var player := PlayerScene.instantiate() as Player
|
||||||
|
root.add_child(player)
|
||||||
|
player.set_local_control(false)
|
||||||
|
var presentation := RemoteFishingPresentationType.new()
|
||||||
|
root.add_child(presentation)
|
||||||
|
presentation.setup(player)
|
||||||
|
await process_frame
|
||||||
|
var origin: Vector3 = player.get_fishing_rod_tip().global_position
|
||||||
|
var target: Vector3 = origin + Vector3(-4.0, -0.5, 0.0)
|
||||||
|
presentation.show_cast(origin, target)
|
||||||
|
var bobber := presentation.get("_bobber") as MeshInstance3D
|
||||||
|
assert(bobber.visible)
|
||||||
|
assert(presentation.get("_cast_tween") != null)
|
||||||
|
await create_timer(0.7).timeout
|
||||||
|
assert(bobber.global_position.distance_to(target) < 0.1)
|
||||||
|
var first_bob_y: float = bobber.global_position.y
|
||||||
|
await create_timer(0.2).timeout
|
||||||
|
assert(not is_equal_approx(bobber.global_position.y, first_bob_y))
|
||||||
|
|
||||||
|
var fish_catch := FishCatchType.new()
|
||||||
|
var fish: FishData = PondPool.candidates.front()
|
||||||
|
fish_catch.fish = fish
|
||||||
|
fish_catch.fish_id = fish.id
|
||||||
|
fish_catch.catch_id = &"remote-presentation-test"
|
||||||
|
fish_catch.weight_lb = 1.0
|
||||||
|
fish_catch.display_scale = 1.0
|
||||||
|
fish_catch.sale_value = 1
|
||||||
|
assert(fish_catch.is_valid())
|
||||||
|
presentation.play_return(fish_catch)
|
||||||
|
await create_timer(0.6).timeout
|
||||||
|
var catch_display := player.get_node("%CatchDisplay") as Node3D
|
||||||
|
assert(catch_display.visible)
|
||||||
|
await presentation.return_completed
|
||||||
|
assert(not catch_display.visible)
|
||||||
|
|
||||||
|
presentation.queue_free()
|
||||||
|
player.queue_free()
|
||||||
|
await process_frame
|
||||||
|
|
||||||
|
|
||||||
|
func _add_water_region(
|
||||||
|
parent: Node3D,
|
||||||
|
position: Vector3,
|
||||||
|
surface_size: Vector2,
|
||||||
|
fish_pool: FishPoolType,
|
||||||
|
) -> void:
|
||||||
|
var water_root := Node3D.new()
|
||||||
|
water_root.position = position
|
||||||
|
parent.add_child(water_root)
|
||||||
|
var region := FishableWaterRegionType.new()
|
||||||
|
region.collision_layer = 4
|
||||||
|
region.collision_mask = 0
|
||||||
|
region.monitoring = false
|
||||||
|
region.surface_height_mode = FishableWaterRegionType.SurfaceHeightMode.PARENT_GLOBAL_Y
|
||||||
|
region.fish_pool = fish_pool
|
||||||
|
water_root.add_child(region)
|
||||||
|
var shape_node := CollisionShape3D.new()
|
||||||
|
shape_node.position.y = -2.0
|
||||||
|
var shape := BoxShape3D.new()
|
||||||
|
shape.size = Vector3(surface_size.x, 4.0, surface_size.y)
|
||||||
|
shape_node.shape = shape
|
||||||
|
region.add_child(shape_node)
|
||||||
|
|
||||||
|
|
||||||
|
func _add_solid_box(
|
||||||
|
parent: Node3D,
|
||||||
|
size: Vector3,
|
||||||
|
position: Vector3,
|
||||||
|
) -> void:
|
||||||
|
var body := StaticBody3D.new()
|
||||||
|
body.position = position
|
||||||
|
body.collision_layer = 1
|
||||||
|
body.collision_mask = 0
|
||||||
|
parent.add_child(body)
|
||||||
|
var shape_node := CollisionShape3D.new()
|
||||||
|
var shape := BoxShape3D.new()
|
||||||
|
shape.size = size
|
||||||
|
shape_node.shape = shape
|
||||||
|
body.add_child(shape_node)
|
||||||
1
tests/fishing_surface_validation.gd.uid
Normal file
1
tests/fishing_surface_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://bh8g62bagphbd
|
||||||
|
|
@ -124,6 +124,9 @@ position = Vector3(-9.4, 2.51, 2.1)
|
||||||
script = ExtResource("10_water_body")
|
script = ExtResource("10_water_body")
|
||||||
surface_size = Vector2(16.1, 12.3675)
|
surface_size = Vector2(16.1, 12.3675)
|
||||||
water_material = ExtResource("19_fresh_water")
|
water_material = ExtResource("19_fresh_water")
|
||||||
|
fish_pool = ExtResource("12_pond_pool")
|
||||||
|
location_tags = Array[StringName]([&"starter_pond"])
|
||||||
|
selection_priority = 1
|
||||||
|
|
||||||
[node name="VisualWater" type="MeshInstance3D" parent="WaterBodies/Pond" unique_id=1034265960]
|
[node name="VisualWater" type="MeshInstance3D" parent="WaterBodies/Pond" unique_id=1034265960]
|
||||||
material_override = ExtResource("19_fresh_water")
|
material_override = ExtResource("19_fresh_water")
|
||||||
|
|
@ -173,19 +176,19 @@ fish_pool = ExtResource("4_pool")
|
||||||
surface_height_mode = 1
|
surface_height_mode = 1
|
||||||
|
|
||||||
[node name="WestShape" type="CollisionShape3D" parent="WaterBodies/Ocean/FishingRegions/OceanFishingRegion" unique_id=357406062]
|
[node name="WestShape" type="CollisionShape3D" parent="WaterBodies/Ocean/FishingRegions/OceanFishingRegion" unique_id=357406062]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -45.5, 0, 0)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -45.5, -1, 0)
|
||||||
shape = SubResource("OceanWestShape")
|
shape = SubResource("OceanWestShape")
|
||||||
|
|
||||||
[node name="EastShape" type="CollisionShape3D" parent="WaterBodies/Ocean/FishingRegions/OceanFishingRegion" unique_id=772224738]
|
[node name="EastShape" type="CollisionShape3D" parent="WaterBodies/Ocean/FishingRegions/OceanFishingRegion" unique_id=772224738]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 32, 0, 0)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 32, -1, 0)
|
||||||
shape = SubResource("OceanEastShape")
|
shape = SubResource("OceanEastShape")
|
||||||
|
|
||||||
[node name="NorthShape" type="CollisionShape3D" parent="WaterBodies/Ocean/FishingRegions/OceanFishingRegion" unique_id=646147646]
|
[node name="NorthShape" type="CollisionShape3D" parent="WaterBodies/Ocean/FishingRegions/OceanFishingRegion" unique_id=646147646]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -13, 0, 31)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -13, -1, 31)
|
||||||
shape = SubResource("OceanNorthShape")
|
shape = SubResource("OceanNorthShape")
|
||||||
|
|
||||||
[node name="SouthShape" type="CollisionShape3D" parent="WaterBodies/Ocean/FishingRegions/OceanFishingRegion" unique_id=894083854]
|
[node name="SouthShape" type="CollisionShape3D" parent="WaterBodies/Ocean/FishingRegions/OceanFishingRegion" unique_id=894083854]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -13, 0, -31)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -13, -1, -31)
|
||||||
shape = SubResource("OceanSouthShape")
|
shape = SubResource("OceanSouthShape")
|
||||||
|
|
||||||
[node name="RecoveryRegions" type="Node3D" parent="WaterBodies/Ocean" unique_id=463706703]
|
[node name="RecoveryRegions" type="Node3D" parent="WaterBodies/Ocean" unique_id=463706703]
|
||||||
|
|
|
||||||
48
world/water_body.tscn
Normal file
48
world/water_body.tscn
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
[gd_scene load_steps=8 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://world/water_body_authoring.gd" id="1_authoring"]
|
||||||
|
[ext_resource type="Script" path="res://world/fishable_water_region.gd" id="2_fishing"]
|
||||||
|
[ext_resource type="Script" path="res://world/player_water_trigger.gd" id="3_recovery"]
|
||||||
|
[ext_resource type="Material" path="res://world/materials/stylized_water.tres" id="4_water"]
|
||||||
|
|
||||||
|
[sub_resource type="PlaneMesh" id="WaterSurfaceMesh"]
|
||||||
|
resource_local_to_scene = true
|
||||||
|
size = Vector2(10, 10)
|
||||||
|
|
||||||
|
[sub_resource type="BoxShape3D" id="FishingVolumeShape"]
|
||||||
|
resource_local_to_scene = true
|
||||||
|
size = Vector3(10, 4, 10)
|
||||||
|
|
||||||
|
[sub_resource type="BoxShape3D" id="RecoveryVolumeShape"]
|
||||||
|
resource_local_to_scene = true
|
||||||
|
size = Vector3(10, 4.8, 10)
|
||||||
|
|
||||||
|
[node name="WaterBody" type="Node3D"]
|
||||||
|
script = ExtResource("1_authoring")
|
||||||
|
water_material = ExtResource("4_water")
|
||||||
|
|
||||||
|
[node name="VisualWater" type="MeshInstance3D" parent="."]
|
||||||
|
cast_shadow = 0
|
||||||
|
material_override = ExtResource("4_water")
|
||||||
|
mesh = SubResource("WaterSurfaceMesh")
|
||||||
|
|
||||||
|
[node name="FishingRegion" type="Area3D" parent="."]
|
||||||
|
collision_layer = 4
|
||||||
|
collision_mask = 0
|
||||||
|
monitoring = false
|
||||||
|
script = ExtResource("2_fishing")
|
||||||
|
surface_height_mode = 1
|
||||||
|
|
||||||
|
[node name="Shape" type="CollisionShape3D" parent="FishingRegion"]
|
||||||
|
position = Vector3(0, -2, 0)
|
||||||
|
shape = SubResource("FishingVolumeShape")
|
||||||
|
|
||||||
|
[node name="RecoveryRegion" type="Area3D" parent="."]
|
||||||
|
position = Vector3(0, -2.4, 0)
|
||||||
|
collision_layer = 8
|
||||||
|
collision_mask = 2
|
||||||
|
script = ExtResource("3_recovery")
|
||||||
|
surface_height_mode = 1
|
||||||
|
|
||||||
|
[node name="Shape" type="CollisionShape3D" parent="RecoveryRegion"]
|
||||||
|
shape = SubResource("RecoveryVolumeShape")
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
class_name WaterBodyAuthoring
|
class_name WaterBodyAuthoring
|
||||||
extends Node3D
|
extends Node3D
|
||||||
|
|
||||||
|
const FishPoolType = preload("res://fish/fish_pool.gd")
|
||||||
|
|
||||||
@export_group("Surface")
|
@export_group("Surface")
|
||||||
## Visible water footprint in meters.
|
## Visible water footprint in meters.
|
||||||
@export_custom(
|
@export_custom(
|
||||||
|
|
@ -19,31 +21,29 @@ var surface_size: Vector2 = Vector2(10.0, 10.0):
|
||||||
_sync_owned_nodes()
|
_sync_owned_nodes()
|
||||||
|
|
||||||
@export_group("Fishing Coverage")
|
@export_group("Fishing Coverage")
|
||||||
## Extra fishable coverage beyond each edge of the visible surface.
|
|
||||||
@export_custom(
|
|
||||||
PROPERTY_HINT_RANGE,
|
|
||||||
"0.0,20.0,0.1,or_greater,suffix:m",
|
|
||||||
)
|
|
||||||
var fishing_padding: Vector2 = Vector2.ZERO:
|
|
||||||
set(value):
|
|
||||||
fishing_padding = Vector2(maxf(value.x, 0.0), maxf(value.y, 0.0))
|
|
||||||
_sync_owned_nodes()
|
|
||||||
@export_range(0.1, 20.0, 0.1, "or_greater", "suffix:m")
|
@export_range(0.1, 20.0, 0.1, "or_greater", "suffix:m")
|
||||||
var fishing_depth: float = 4.0:
|
var fishing_depth: float = 4.0:
|
||||||
set(value):
|
set(value):
|
||||||
fishing_depth = maxf(value, 0.1)
|
fishing_depth = maxf(value, 0.1)
|
||||||
_sync_owned_nodes()
|
_sync_owned_nodes()
|
||||||
|
@export var fish_pool: FishPoolType:
|
||||||
|
set(value):
|
||||||
|
fish_pool = value
|
||||||
|
_sync_owned_nodes()
|
||||||
|
@export var water_type: WaterType.Type = WaterType.Type.FRESH_WATER:
|
||||||
|
set(value):
|
||||||
|
water_type = value
|
||||||
|
_sync_owned_nodes()
|
||||||
|
@export var location_tags: Array[StringName] = []:
|
||||||
|
set(value):
|
||||||
|
location_tags = value
|
||||||
|
_sync_owned_nodes()
|
||||||
|
@export var selection_priority: int = 0:
|
||||||
|
set(value):
|
||||||
|
selection_priority = value
|
||||||
|
_sync_owned_nodes()
|
||||||
|
|
||||||
@export_group("Recovery Coverage")
|
@export_group("Recovery Coverage")
|
||||||
## Extra recovery coverage beyond each edge of the visible surface.
|
|
||||||
@export_custom(
|
|
||||||
PROPERTY_HINT_RANGE,
|
|
||||||
"0.0,20.0,0.1,or_greater,suffix:m",
|
|
||||||
)
|
|
||||||
var recovery_padding: Vector2 = Vector2.ZERO:
|
|
||||||
set(value):
|
|
||||||
recovery_padding = Vector2(maxf(value.x, 0.0), maxf(value.y, 0.0))
|
|
||||||
_sync_owned_nodes()
|
|
||||||
@export_range(0.1, 20.0, 0.1, "or_greater", "suffix:m")
|
@export_range(0.1, 20.0, 0.1, "or_greater", "suffix:m")
|
||||||
var recovery_depth: float = 4.8:
|
var recovery_depth: float = 4.8:
|
||||||
set(value):
|
set(value):
|
||||||
|
|
@ -56,6 +56,8 @@ var visual_water_path: NodePath = ^"VisualWater"
|
||||||
@export_node_path("CollisionShape3D")
|
@export_node_path("CollisionShape3D")
|
||||||
var fishing_shape_path: NodePath = ^"FishingRegion/Shape"
|
var fishing_shape_path: NodePath = ^"FishingRegion/Shape"
|
||||||
@export_node_path("Area3D")
|
@export_node_path("Area3D")
|
||||||
|
var fishing_region_path: NodePath = ^"FishingRegion"
|
||||||
|
@export_node_path("Area3D")
|
||||||
var recovery_region_path: NodePath = ^"RecoveryRegion"
|
var recovery_region_path: NodePath = ^"RecoveryRegion"
|
||||||
@export_node_path("CollisionShape3D")
|
@export_node_path("CollisionShape3D")
|
||||||
var recovery_shape_path: NodePath = ^"RecoveryRegion/Shape"
|
var recovery_shape_path: NodePath = ^"RecoveryRegion/Shape"
|
||||||
|
|
@ -80,13 +82,28 @@ func _sync_owned_nodes() -> void:
|
||||||
var fishing_shape_node := (
|
var fishing_shape_node := (
|
||||||
get_node_or_null(fishing_shape_path) as CollisionShape3D
|
get_node_or_null(fishing_shape_path) as CollisionShape3D
|
||||||
)
|
)
|
||||||
|
var fishing_region := (
|
||||||
|
get_node_or_null(fishing_region_path) as FishableWaterRegion
|
||||||
|
)
|
||||||
|
if fishing_region != null:
|
||||||
|
fishing_region.fish_pool = fish_pool
|
||||||
|
fishing_region.water_type = water_type
|
||||||
|
fishing_region.location_tags = location_tags.duplicate()
|
||||||
|
fishing_region.selection_priority = selection_priority
|
||||||
|
fishing_region.surface_height_mode = (
|
||||||
|
FishableWaterRegion.SurfaceHeightMode.PARENT_GLOBAL_Y
|
||||||
|
)
|
||||||
if fishing_shape_node != null:
|
if fishing_shape_node != null:
|
||||||
var fishing_shape := fishing_shape_node.shape as BoxShape3D
|
var fishing_shape := fishing_shape_node.shape as BoxShape3D
|
||||||
if fishing_shape != null:
|
if fishing_shape != null:
|
||||||
|
# The authored root is the one water-surface height. Keep the fishable
|
||||||
|
# volume entirely below that plane so visible water and interaction
|
||||||
|
# can be moved together without a second height to tune.
|
||||||
|
fishing_shape_node.position.y = -fishing_depth * 0.5
|
||||||
fishing_shape.size = Vector3(
|
fishing_shape.size = Vector3(
|
||||||
surface_size.x + fishing_padding.x * 2.0,
|
surface_size.x,
|
||||||
fishing_depth,
|
fishing_depth,
|
||||||
surface_size.y + fishing_padding.y * 2.0
|
surface_size.y
|
||||||
)
|
)
|
||||||
|
|
||||||
var recovery_region := (
|
var recovery_region := (
|
||||||
|
|
@ -101,9 +118,9 @@ func _sync_owned_nodes() -> void:
|
||||||
var recovery_shape := recovery_shape_node.shape as BoxShape3D
|
var recovery_shape := recovery_shape_node.shape as BoxShape3D
|
||||||
if recovery_shape != null:
|
if recovery_shape != null:
|
||||||
recovery_shape.size = Vector3(
|
recovery_shape.size = Vector3(
|
||||||
surface_size.x + recovery_padding.x * 2.0,
|
surface_size.x,
|
||||||
recovery_depth,
|
recovery_depth,
|
||||||
surface_size.y + recovery_padding.y * 2.0
|
surface_size.y
|
||||||
)
|
)
|
||||||
|
|
||||||
if Engine.is_editor_hint():
|
if Engine.is_editor_hint():
|
||||||
|
|
@ -139,11 +156,9 @@ func _get_configuration_warnings() -> PackedStringArray:
|
||||||
)
|
)
|
||||||
if recovery_shape == null or not recovery_shape.shape is BoxShape3D:
|
if recovery_shape == null or not recovery_shape.shape is BoxShape3D:
|
||||||
warnings.append("RecoveryRegion must provide a BoxShape3D.")
|
warnings.append("RecoveryRegion must provide a BoxShape3D.")
|
||||||
var fishing_region := (
|
var fishing_region := get_node_or_null(
|
||||||
fishing_shape.get_parent() as FishableWaterRegion
|
fishing_region_path
|
||||||
if fishing_shape != null
|
) as FishableWaterRegion
|
||||||
else null
|
|
||||||
)
|
|
||||||
if fishing_region == null:
|
if fishing_region == null:
|
||||||
warnings.append("FishingRegion must use FishableWaterRegion.")
|
warnings.append("FishingRegion must use FishableWaterRegion.")
|
||||||
elif (
|
elif (
|
||||||
|
|
@ -155,4 +170,6 @@ func _get_configuration_warnings() -> PackedStringArray:
|
||||||
)
|
)
|
||||||
if water_material == null:
|
if water_material == null:
|
||||||
warnings.append("Assign a water material.")
|
warnings.append("Assign a water material.")
|
||||||
|
if fish_pool == null:
|
||||||
|
warnings.append("Assign a fish pool.")
|
||||||
return warnings
|
return warnings
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue