feat: Implement the core player entity with state management, network synchronization, and game system integration.
This commit is contained in:
+62
-93
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user