feat: Implement the core player entity with state management, network synchronization, and game system integration.

This commit is contained in:
Yogi Wiguna
2026-03-25 16:42:31 +08:00
parent a0da22ff5c
commit 0a8e2b9702
+62 -93
View File
@@ -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):