From 0a8e2b9702becbef90a4ffb6d2a16c3ff3281848 Mon Sep 17 00:00:00 2001 From: Yogi Wiguna Date: Wed, 25 Mar 2026 16:42:31 +0800 Subject: [PATCH] feat: Implement the core player entity with state management, network synchronization, and game system integration. --- scenes/player.gd | 155 +++++++++++++++++++---------------------------- 1 file changed, 62 insertions(+), 93 deletions(-) diff --git a/scenes/player.gd b/scenes/player.gd index 9e3e3d5..9637dcb 100644 --- a/scenes/player.gd +++ b/scenes/player.gd @@ -38,32 +38,40 @@ func can_rpc() -> bool: return multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED # Special effect states -var is_frozen: bool = false -var is_stop_frozen: bool = false # Special freeze for Stop n Go phase -var is_invisible: bool = false +var is_frozen: bool = false: + set(value): + if is_frozen == value: return + is_frozen = value + _refresh_player_visuals() +var is_stop_frozen: bool = false: # Special freeze for Stop n Go phase + set(value): + if is_stop_frozen == value: return + is_stop_frozen = value + _refresh_player_visuals() +var is_invisible: bool = false: + set(value): + if is_invisible == value: return + is_invisible = value + _refresh_player_visuals() var original_movement_range: int = 1 # Tekton Interaction var carried_tekton: Node3D = null -var is_carrying_tekton: bool: - get: return is_carried_tekton +var is_carrying_tekton: bool = false: set(value): - is_carried_tekton = value + if is_carrying_tekton == value: return + is_carrying_tekton = value + _refresh_player_visuals() emit_signal("tekton_carried_changed", value) # Visual/Logic side effects if any -var is_carried_tekton: bool = false var is_attack_mode: bool = false: set(value): if is_attack_mode == value: return # Prevent infinite recursion / redundant updates is_attack_mode = value - # Visual feedback for attack mode (Red Tint) - if is_attack_mode: - _apply_tint_recursive(self , Color(1.0, 0.5, 0.5)) - else: - _apply_tint_recursive(self , Color.WHITE) + _refresh_player_visuals() # Sync to others if we are the authority if is_multiplayer_authority() and can_rpc(): @@ -73,7 +81,7 @@ var is_attack_mode: bool = false: func sync_attack_mode(state: bool): # We WANT to trigger the setter to apply visuals on clients # Using self.var triggers setter in GDScript - self.is_attack_mode = state + is_attack_mode = state @export var is_bot: bool = false @@ -676,20 +684,26 @@ func sync_display_name(new_name: String) -> void: if has_node("Name"): $Name.text = _display_name -@rpc("any_peer", "call_local", "reliable") -func sync_modulate(color: Color) -> void: - """Sync visual modulation (e.g. freeze effect) across network.""" - # Node3D doesn't have modulate. We must tint the active character meshes. - var active_char_node = null +func _refresh_player_visuals(): + var color_to_apply = Color.WHITE + var alpha_to_apply = 1.0 - match selected_character: - "Bob": active_char_node = character_bob - "Masbro": active_char_node = character_masbro - "Gatot": active_char_node = character_gatot - "Oldpop": active_char_node = character_oldpop + if is_frozen: + color_to_apply = Color.BLUE # Frozen (Staggered) + elif is_stop_frozen: + color_to_apply = Color.CYAN # Stop n Go Freeze + elif is_attack_mode: + color_to_apply = Color(1.0, 0.5, 0.5) # Attack Mode (Red Tint) + elif is_carrying_tekton or is_knock_mode: + color_to_apply = Color(1.0, 1.0, 0.0) # Carrying or Knocking (Yellow) + elif immunity_timer > 0: + color_to_apply = Color(0.5, 1.0, 0.5) # Immunity (Light Green) + elif is_invisible: + color_to_apply = Color.WHITE + alpha_to_apply = 0.5 # Invisible (Semi-transparent) - if active_char_node: - _apply_tint_recursive(active_char_node, color) + # Apply the determined color and alpha + _apply_tint_recursive(self, color_to_apply, alpha_to_apply) func update_rank_visuals(rank: int): var pos_label = get_node_or_null("Position") @@ -710,47 +724,22 @@ func update_rank_visuals(rank: int): else: pos_label.visible = false -func _apply_tint_recursive(node: Node, color: Color): +func _apply_tint_recursive(node: Node, color: Color, alpha: float = 1.0): if node is MeshInstance3D: - # Simple way to tint: use material_overlay with an unshaded material if possible, - # or transparency. But changing albedo on shared materials affects all instances! - # Using transparency/modulate equivalent in 3D is tricky without unique materials. - # A common trick is to use `instance_shader_parameter` if supported, or modifying visibility range? No. - # Best safe visual cue for "Help me create an ice effect" without shaders: - # Set `transparency` (alpha) if we want ghost, or `material_overlay`. - # For this quick fix, let's create a standard material overlay on the fly if needed - # or just rely on a debug geometry. - # Actually, geometry_instance_3d has `material_overlay`. - # Better approach for "Freeze": Just add a visible "Ice Block" mesh to the player - # instead of trying to tint the gltf model which might have complex materials. - # But user asked for "modulate". The closest 3D equivalent is material_overlay with a color. var mat = StandardMaterial3D.new() mat.albedo_color = color + mat.albedo_color.a = alpha mat.blend_mode = BaseMaterial3D.BLEND_MODE_MIX mat.cull_mode = BaseMaterial3D.CULL_DISABLED mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED - # If color is WHITE (reset), clear the overlay - if color == Color.WHITE: + if color == Color.WHITE and alpha == 1.0: node.material_overlay = null - node.transparency = 0.0 # Reset else: - # If color is Blue (frozen), make it semi-transparent overlay - mat.albedo_color = color - - # Special Ghost Effect (Invisible Mode) - if color.a < 1.0: - mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA - node.transparency = 0.5 # Make base mesh transparent too if possible, depending on material - else: - # Frozen - mat.albedo_color.a = 0.5 # Semi-transparent overlay - mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA - node.material_overlay = mat for child in node.get_children(): - _apply_tint_recursive(child, color) + _apply_tint_recursive(child, color, alpha) var immunity_timer: float = 0.0 var tekton_carry_timer: float = 0.0 @@ -769,7 +758,6 @@ func apply_stagger(duration: float = 1.5): return # Already staggered is_frozen = true - _apply_tint_recursive(self , Color.BLUE) # Visual feedback # Play knock VFX sequence on the receiver _play_knock_vfx() @@ -789,14 +777,7 @@ func apply_stagger(duration: float = 1.5): await get_tree().create_timer(duration).timeout is_frozen = false - # If still immune, show immunity tint (Green?), otherwise White - # UNLESS we are still stop-frozen (Cyan) - if is_stop_frozen: - _apply_tint_recursive(self , Color.CYAN) - elif immunity_timer > 0: - _apply_tint_recursive(self , Color(0.5, 1.0, 0.5)) # Light Green for immunity - else: - _apply_tint_recursive(self , Color.WHITE) # Remove tint + _refresh_player_visuals() func _play_knock_vfx() -> void: """Plays the three knock receiver VFX concurrently. @@ -805,6 +786,14 @@ func _play_knock_vfx() -> void: play_skill_vfx("attack_mode_bot") play_skill_vfx("receiver_skill_stunned") +func set_stop_frozen(enabled: bool): + self.is_stop_frozen = enabled + + if enabled: + print("[STOP n GO] Player %s FROZEN until GO phase" % name) + else: + print("[STOP n GO] Player %s UNFROZEN" % name) + @rpc("any_peer", "call_local", "reliable") func sync_stop_freeze(enabled: bool): # Security: Only allow server (peer 1) or local calls (peer 0) @@ -812,20 +801,7 @@ func sync_stop_freeze(enabled: bool): if sender != 1 and sender != 0: return - is_stop_frozen = enabled - - if enabled: - _apply_tint_recursive(self , Color.CYAN) - print("[STOP n GO] Player %s FROZEN until GO phase" % name) - else: - # Restore appropriate tint - if is_frozen: - _apply_tint_recursive(self , Color.BLUE) - elif immunity_timer > 0: - _apply_tint_recursive(self , Color(0.5, 1.0, 0.5)) - else: - _apply_tint_recursive(self , Color.WHITE) - print("[STOP n GO] Player %s UNFROZEN" % name) + set_stop_frozen(enabled) @rpc("any_peer", "call_local") func apply_slow_effect(duration: float = 3.0): @@ -2202,7 +2178,7 @@ func sync_grab_tekton(tekton_path: NodePath): var tekton = get_node_or_null(tekton_path) if tekton: carried_tekton = tekton - is_carrying_tekton = true + self.is_carrying_tekton = true tekton.set_carried(true, self ) # Disposed of AttackMode upon grab @@ -2250,7 +2226,7 @@ func sync_throw_tekton(target_pos: Vector2i): if carried_tekton: var tekton = carried_tekton carried_tekton = null - is_carrying_tekton = false + self.is_carrying_tekton = false if tekton.has_method("set_thrown"): tekton.set_thrown(true) else: @@ -2333,7 +2309,7 @@ func sync_drop_tekton(): if carried_tekton: var tekton = carried_tekton carried_tekton = null - is_carrying_tekton = false + self.is_carrying_tekton = false # Set its position to player's current position (but on ground) var drop_pos = grid_to_world(current_position) @@ -2350,7 +2326,11 @@ func sync_drop_tekton(): # is_attack_mode is already declared at top of file (or inherited?) # Keeping is_knock_mode here for now or moving it up would be better, but let's just fix the error first. -var is_knock_mode: bool = false # Yellow mode for knocking Tekton +var is_knock_mode: bool = false: + set(value): + if is_knock_mode == value: return + is_knock_mode = value + _refresh_player_visuals() func enter_attack_mode(): if not is_multiplayer_authority(): return @@ -2377,18 +2357,7 @@ func enter_knock_mode(): update_active_player_indicator() func update_active_player_indicator(): - var color = Color.WHITE - - if is_attack_mode: - color = Color.RED - elif is_knock_mode: - color = Color.YELLOW - - # Apply visual tint to character model across network - if can_rpc(): - rpc("sync_modulate", color) - else: - sync_modulate(color) # Apply locally if offline/not ready + _refresh_player_visuals() @rpc("any_peer", "call_local", "unreliable") func sync_bump(target_pos: Vector2i, is_soft: bool = false):