@@ -11,6 +11,11 @@ extends CharacterBody3D
1111@export var is_shiny = false
1212@export var shiny_particles : GPUParticles3D
1313
14+ # Debug flag - set to true to enable detailed orientation debugging
15+ @export var debug_orientation = false
16+ # Debug flag for shader animation - set to true to track animation rate issues
17+ @export var debug_shader_animation = false
18+
1419@export var mesh_instance_path : NodePath
1520var mesh_instance : MeshInstance3D
1621
@@ -19,6 +24,9 @@ var speed = 0
1924var home : int
2025var type : int
2126var accumulated_shader_time : float = 0.0 # Custom time accumulator for shader
27+ var debug_birth_time : float = 0.0 # Track when this fish was created for debugging
28+ var debug_last_accum_time : float = 0.0 # Track previous accumulated time for rate calculation
29+ var debug_last_real_time : float = 0.0 # Track real time for rate calculation
2230
2331# --- Shader Animation Speed Control (names describe rate of accumulation) ---
2432@export var base_anim_rate : float = 0.5 # Base rate for accumulated_shader_time
@@ -49,6 +57,9 @@ func _ready():
4957 printerr (" ERROR: mesh_instance_path is not set for node: " , name )
5058
5159 accumulated_shader_time = randf () * 2.0 * PI
60+ debug_birth_time = Time .get_time_dict_from_system ()["second" ] + Time .get_time_dict_from_system ()["minute" ] * 60 + Time .get_time_dict_from_system ()["hour" ] * 3600
61+ debug_last_accum_time = accumulated_shader_time
62+ debug_last_real_time = debug_birth_time
5263
5364 # Disable shiny particles on WebGL for performance
5465 if is_webgl_build and shiny_particles :
@@ -77,9 +88,15 @@ func _physics_process(delta: float) -> void:
7788
7889 if get_slide_collision_count () > 0 && rotation_cooldown_left <= 0 :
7990 rotation_cooldown_left = rotation_cooldown
91+ # Flip direction first
8092 rotate_y (deg_to_rad (180 ))
93+ # Normalize rotation to prevent accumulation errors
94+ normalize_y_rotation ()
95+ # Then set new velocity with random angle
8196 var deg = randf_range (min_angle , max_angle )
8297 set_z_rotation_and_velocity (deg )
98+ # Extra validation after collision handling
99+ validate_orientation_velocity_match ()
83100
84101 if global_position .y >= - 0.5 :
85102 record_surface_achievement ()
@@ -97,12 +114,15 @@ func _physics_process(delta: float) -> void:
97114 set_z_rotation_and_velocity (randf_range (min_angle , max_angle ))
98115
99116 move_and_slide ()
100- # Update shader animation with timer-based optimization
117+
118+ # Validate orientation matches velocity (only check occasionally for performance)
119+ if randf () < 0.005 : # Check ~0.5% of frames to reduce spam
120+ validate_orientation_velocity_match ()
121+
122+ # Update shader animation every frame (removed timer optimization)
123+ # The timer-based optimization was causing inconsistent animation rates
101124 if type == 0 or type == 1 : # Only update animation for fish_a (0) and fish_b (1)
102- shader_update_timer += delta
103- if shader_update_timer >= shader_update_interval :
104- update_shader_animation (shader_update_timer )
105- shader_update_timer = 0.0
125+ update_shader_animation (delta )
106126
107127func update_shader_animation (delta_time : float ):
108128 if ! mesh_instance : return
@@ -123,9 +143,40 @@ func update_shader_animation(delta_time: float):
123143 var effective_anim_rate = base_anim_rate + (current_speed * speed_to_anim_rate_factor )
124144 effective_anim_rate = clamp (effective_anim_rate , min_effective_anim_rate , max_effective_anim_rate )
125145
146+ # Add the accumulated time since last update, scaled by animation rate
126147 accumulated_shader_time += delta_time * effective_anim_rate
127148
128- # print("Fish ", name, ": current_speed: ", current_speed, ", effective_anim_rate: ", effective_anim_rate, ", new_accum_time: ", accumulated_shader_time) # Uncomment for verbose logging
149+ # Keep the accumulated time in a reasonable range to prevent floating point precision issues
150+ # Wrap around every ~6.28 seconds (2π) since most shader animations are cyclical
151+ var old_accum_time = accumulated_shader_time
152+ var two_pi = 2.0 * PI
153+ accumulated_shader_time = fmod (accumulated_shader_time , two_pi )
154+
155+ # Debug: Track animation rate changes if enabled
156+ if debug_shader_animation and randf () < 0.05 : # Increased sampling rate for better accuracy
157+ var current_time = Time .get_time_dict_from_system ()["second" ] + Time .get_time_dict_from_system ()["minute" ] * 60 + Time .get_time_dict_from_system ()["hour" ] * 3600
158+ var fish_age = current_time - debug_birth_time
159+ var time_increment = delta_time * effective_anim_rate
160+
161+ # Calculate actual animation rate
162+ var real_time_elapsed = current_time - debug_last_real_time
163+ var accum_time_change = accumulated_shader_time - debug_last_accum_time
164+ # Handle wrapping
165+ if accum_time_change < - 3.0 : # Wrapped backwards
166+ accum_time_change += 2 * PI
167+ elif accum_time_change > 3.0 : # Wrapped forwards
168+ accum_time_change -= 2 * PI
169+ var actual_rate = accum_time_change / real_time_elapsed if real_time_elapsed > 0 else 0
170+
171+ print ("Fish " , name , ": age=" , fish_age , "s anim_rate=" , effective_anim_rate , " actual_rate=" , actual_rate , " accum_time=" , accumulated_shader_time , " delta=" , delta_time )
172+
173+ debug_last_accum_time = accumulated_shader_time
174+ debug_last_real_time = current_time
175+
176+ if delta_time > 0.05 : # Flag unusually large delta times
177+ print (" -> WARNING: Large delta_time: " , delta_time )
178+ if abs (old_accum_time - accumulated_shader_time ) > 0.1 :
179+ print (" -> Wrapped from " , old_accum_time , " to " , accumulated_shader_time )
129180 material .set_shader_parameter ("animation_time_input" , accumulated_shader_time )
130181
131182func initialize (mStart_position , mHome , mMin_speed , mMax_speed ,
@@ -167,8 +218,12 @@ mDifficulty, mMin_weight, mMax_weight, price_weight_multiplier, mType, weight_mu
167218func set_z_rotation_and_velocity (deg : float ) -> void :
168219 rotation [2 ] = 0
169220 var rad = deg_to_rad (deg )
221+ var facing_left = is_facing_left ()
222+
223+ # Debug: Uncomment to track direction changes
224+ # print("Fish ", name, ": set_z_rotation_and_velocity deg=", deg, " facing_left=", facing_left, " rotation.y=", rotation.y)
170225
171- if is_facing_left () :
226+ if facing_left :
172227 rotate_z (- rad )
173228 velocity = Vector3 .MODEL_RIGHT * speed
174229 velocity = velocity .rotated (Vector3 .FORWARD , rotation .z )
@@ -177,23 +232,98 @@ func set_z_rotation_and_velocity(deg: float) -> void:
177232 velocity = Vector3 .MODEL_LEFT * speed
178233 velocity = velocity .rotated (Vector3 .BACK , rotation .z )
179234
235+ # Validate that orientation matches velocity direction
236+ validate_orientation_velocity_match ()
237+
238+
239+ # Validate that the fish's visual orientation matches its velocity direction
240+ func validate_orientation_velocity_match ():
241+ if velocity .length () < 0.1 : # Skip validation if fish is barely moving
242+ return
243+
244+ var facing_left = is_facing_left ()
245+ var moving_left = velocity .x < 0
246+ var moving_right = velocity .x > 0
247+
248+ # CORRECT coordinate system based on debug output:
249+ # - Vector3.MODEL_RIGHT = (-1, 0, 0) → negative X direction
250+ # - Vector3.MODEL_LEFT = (1, 0, 0) → positive X direction
251+ # Therefore:
252+ # - facing_left = true → velocity = Vector3.MODEL_RIGHT → moves LEFT (negative X)
253+ # - facing_left = false → velocity = Vector3.MODEL_LEFT → moves RIGHT (positive X)
254+
255+ var orientation_matches = (facing_left && moving_left ) || (! facing_left && moving_right )
256+
257+ if ! orientation_matches :
258+ print ("WARNING: Fish " , name , " orientation mismatch!" )
259+ print (" - facing_left: " , facing_left )
260+ print (" - rotation.y: " , rotation .y )
261+ print (" - velocity.x: " , velocity .x )
262+ print (" - Expected: facing_left=" , facing_left , " should move " , "LEFT" if facing_left else "RIGHT" )
263+ print (" - Actual: moving " , "LEFT" if moving_left else "RIGHT" )
264+
265+ # Fix the mismatch by flipping the fish to match velocity direction
266+ print (" - Attempting to fix orientation..." )
267+ var old_facing_left = facing_left
268+ rotate_y (deg_to_rad (180 ))
269+ normalize_y_rotation ()
270+ var new_facing_left = is_facing_left ()
271+ print (" - Fixed. New rotation.y: " , rotation .y , " facing_left: " , old_facing_left , " → " , new_facing_left )
272+
273+ # Verify the fix worked
274+ var new_moving_left = velocity .x < 0
275+ var new_moving_right = velocity .x > 0
276+ var fixed_orientation_matches = (new_facing_left && new_moving_left ) || (! new_facing_left && new_moving_right )
277+ if ! fixed_orientation_matches :
278+ print (" - ERROR: Fix failed! Still mismatched after rotation." )
279+ elif debug_orientation :
280+ # Optional detailed logging when debug mode is enabled
281+ print ("Fish " , name , " orientation OK: facing_left=" , facing_left ,
282+ " moving=" , "LEFT" if moving_left else "RIGHT" )
283+
284+ # Helper function to normalize Y rotation and prevent accumulation errors
285+ func normalize_y_rotation ():
286+ var old_rotation = rotation .y
287+ rotation .y = fmod (rotation .y , 2 * PI )
288+ if rotation .y > PI :
289+ rotation .y -= 2 * PI
290+ elif rotation .y < - PI :
291+ rotation .y += 2 * PI
180292
293+ # Debug: Uncomment to track rotation normalization
294+ # if abs(old_rotation - rotation.y) > 0.1:
295+ # print("Fish ", name, ": Normalized rotation.y from ", old_rotation, " to ", rotation.y)
296+
181297func is_facing_left () -> bool :
182- return rotation [1 ] < 0
298+ # Normalize rotation.y to handle edge cases and floating point precision
299+ var normalized_y = fmod (rotation .y , 2 * PI )
300+ if normalized_y > PI :
301+ normalized_y -= 2 * PI
302+ elif normalized_y < - PI :
303+ normalized_y += 2 * PI
304+
305+ # Fish faces left when rotated around 180° (π radians)
306+ # This includes both positive π and negative π values
307+ # Range: roughly between π/2 and 3π/2, or in normalized form: |normalized_y| > π/2
308+ return abs (normalized_y ) > (PI / 2 + 0.01 ) # Small epsilon to avoid floating point issues
183309
184310func is_looking_up () -> bool :
185311 return rotation [2 ] > 0
186312
187313func scatter (body : Node3D ) -> void :
188314 if (body .global_position .x < global_position .x && is_facing_left () or
189- body .global_position .x > global_position .x && ! is_facing_left ()):
315+ body .global_position .x > global_position .x && ! is_facing_left ()):
190316 rotate_y (deg_to_rad (180 ))
317+ # Normalize rotation to prevent accumulation errors
318+ normalize_y_rotation ()
191319 if (body .global_position .y < global_position .y ):
192320 set_z_rotation_and_velocity (randf_range (35 , 55 ))
193321 else :
194322 set_z_rotation_and_velocity (randf_range (- 35 , - 55 ))
195323
196-
324+ # Extra validation after scattering
325+ validate_orientation_velocity_match ()
326+
197327 pass
198328
199329func get_scale_for_weight (max_weight , min_weight , mWeight ) -> Vector3 :
@@ -210,5 +340,4 @@ func record_surface_achievement():
210340 var achievement_manager = get_node_or_null ("/root/AchievementManager" )
211341 if achievement_manager and achievement_manager .has_method ("get_achievement_system" ):
212342 var achievement_system = achievement_manager .get_achievement_system ()
213- if achievement_system :
214- achievement_system .record_fish_surface (self .type )
343+ achievement_system .record_fish_surface (self .type )
0 commit comments