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
|
return multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED
|
||||||
|
|
||||||
# Special effect states
|
# Special effect states
|
||||||
var is_frozen: bool = false
|
var is_frozen: bool = false:
|
||||||
var is_stop_frozen: bool = false # Special freeze for Stop n Go phase
|
set(value):
|
||||||
var is_invisible: bool = false
|
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
|
var original_movement_range: int = 1
|
||||||
|
|
||||||
# Tekton Interaction
|
# Tekton Interaction
|
||||||
var carried_tekton: Node3D = null
|
var carried_tekton: Node3D = null
|
||||||
var is_carrying_tekton: bool:
|
var is_carrying_tekton: bool = false:
|
||||||
get: return is_carried_tekton
|
|
||||||
set(value):
|
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)
|
emit_signal("tekton_carried_changed", value)
|
||||||
# Visual/Logic side effects if any
|
# Visual/Logic side effects if any
|
||||||
|
|
||||||
var is_carried_tekton: bool = false
|
|
||||||
var is_attack_mode: bool = false:
|
var is_attack_mode: bool = false:
|
||||||
set(value):
|
set(value):
|
||||||
if is_attack_mode == value:
|
if is_attack_mode == value:
|
||||||
return # Prevent infinite recursion / redundant updates
|
return # Prevent infinite recursion / redundant updates
|
||||||
|
|
||||||
is_attack_mode = value
|
is_attack_mode = value
|
||||||
# Visual feedback for attack mode (Red Tint)
|
_refresh_player_visuals()
|
||||||
if is_attack_mode:
|
|
||||||
_apply_tint_recursive(self , Color(1.0, 0.5, 0.5))
|
|
||||||
else:
|
|
||||||
_apply_tint_recursive(self , Color.WHITE)
|
|
||||||
|
|
||||||
# Sync to others if we are the authority
|
# Sync to others if we are the authority
|
||||||
if is_multiplayer_authority() and can_rpc():
|
if is_multiplayer_authority() and can_rpc():
|
||||||
@@ -73,7 +81,7 @@ var is_attack_mode: bool = false:
|
|||||||
func sync_attack_mode(state: bool):
|
func sync_attack_mode(state: bool):
|
||||||
# We WANT to trigger the setter to apply visuals on clients
|
# We WANT to trigger the setter to apply visuals on clients
|
||||||
# Using self.var triggers setter in GDScript
|
# Using self.var triggers setter in GDScript
|
||||||
self.is_attack_mode = state
|
is_attack_mode = state
|
||||||
|
|
||||||
@export var is_bot: bool = false
|
@export var is_bot: bool = false
|
||||||
|
|
||||||
@@ -676,20 +684,26 @@ func sync_display_name(new_name: String) -> void:
|
|||||||
if has_node("Name"):
|
if has_node("Name"):
|
||||||
$Name.text = _display_name
|
$Name.text = _display_name
|
||||||
|
|
||||||
@rpc("any_peer", "call_local", "reliable")
|
func _refresh_player_visuals():
|
||||||
func sync_modulate(color: Color) -> void:
|
var color_to_apply = Color.WHITE
|
||||||
"""Sync visual modulation (e.g. freeze effect) across network."""
|
var alpha_to_apply = 1.0
|
||||||
# Node3D doesn't have modulate. We must tint the active character meshes.
|
|
||||||
var active_char_node = null
|
|
||||||
|
|
||||||
match selected_character:
|
if is_frozen:
|
||||||
"Bob": active_char_node = character_bob
|
color_to_apply = Color.BLUE # Frozen (Staggered)
|
||||||
"Masbro": active_char_node = character_masbro
|
elif is_stop_frozen:
|
||||||
"Gatot": active_char_node = character_gatot
|
color_to_apply = Color.CYAN # Stop n Go Freeze
|
||||||
"Oldpop": active_char_node = character_oldpop
|
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 the determined color and alpha
|
||||||
_apply_tint_recursive(active_char_node, color)
|
_apply_tint_recursive(self, color_to_apply, alpha_to_apply)
|
||||||
|
|
||||||
func update_rank_visuals(rank: int):
|
func update_rank_visuals(rank: int):
|
||||||
var pos_label = get_node_or_null("Position")
|
var pos_label = get_node_or_null("Position")
|
||||||
@@ -710,47 +724,22 @@ func update_rank_visuals(rank: int):
|
|||||||
else:
|
else:
|
||||||
pos_label.visible = false
|
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:
|
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()
|
var mat = StandardMaterial3D.new()
|
||||||
mat.albedo_color = color
|
mat.albedo_color = color
|
||||||
|
mat.albedo_color.a = alpha
|
||||||
mat.blend_mode = BaseMaterial3D.BLEND_MODE_MIX
|
mat.blend_mode = BaseMaterial3D.BLEND_MODE_MIX
|
||||||
mat.cull_mode = BaseMaterial3D.CULL_DISABLED
|
mat.cull_mode = BaseMaterial3D.CULL_DISABLED
|
||||||
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||||
|
|
||||||
# If color is WHITE (reset), clear the overlay
|
if color == Color.WHITE and alpha == 1.0:
|
||||||
if color == Color.WHITE:
|
|
||||||
node.material_overlay = null
|
node.material_overlay = null
|
||||||
node.transparency = 0.0 # Reset
|
|
||||||
else:
|
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
|
node.material_overlay = mat
|
||||||
|
|
||||||
for child in node.get_children():
|
for child in node.get_children():
|
||||||
_apply_tint_recursive(child, color)
|
_apply_tint_recursive(child, color, alpha)
|
||||||
|
|
||||||
var immunity_timer: float = 0.0
|
var immunity_timer: float = 0.0
|
||||||
var tekton_carry_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
|
return # Already staggered
|
||||||
|
|
||||||
is_frozen = true
|
is_frozen = true
|
||||||
_apply_tint_recursive(self , Color.BLUE) # Visual feedback
|
|
||||||
|
|
||||||
# Play knock VFX sequence on the receiver
|
# Play knock VFX sequence on the receiver
|
||||||
_play_knock_vfx()
|
_play_knock_vfx()
|
||||||
@@ -789,14 +777,7 @@ func apply_stagger(duration: float = 1.5):
|
|||||||
await get_tree().create_timer(duration).timeout
|
await get_tree().create_timer(duration).timeout
|
||||||
|
|
||||||
is_frozen = false
|
is_frozen = false
|
||||||
# If still immune, show immunity tint (Green?), otherwise White
|
_refresh_player_visuals()
|
||||||
# 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
|
|
||||||
|
|
||||||
func _play_knock_vfx() -> void:
|
func _play_knock_vfx() -> void:
|
||||||
"""Plays the three knock receiver VFX concurrently.
|
"""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("attack_mode_bot")
|
||||||
play_skill_vfx("receiver_skill_stunned")
|
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")
|
@rpc("any_peer", "call_local", "reliable")
|
||||||
func sync_stop_freeze(enabled: bool):
|
func sync_stop_freeze(enabled: bool):
|
||||||
# Security: Only allow server (peer 1) or local calls (peer 0)
|
# 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:
|
if sender != 1 and sender != 0:
|
||||||
return
|
return
|
||||||
|
|
||||||
is_stop_frozen = enabled
|
set_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)
|
|
||||||
|
|
||||||
@rpc("any_peer", "call_local")
|
@rpc("any_peer", "call_local")
|
||||||
func apply_slow_effect(duration: float = 3.0):
|
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)
|
var tekton = get_node_or_null(tekton_path)
|
||||||
if tekton:
|
if tekton:
|
||||||
carried_tekton = tekton
|
carried_tekton = tekton
|
||||||
is_carrying_tekton = true
|
self.is_carrying_tekton = true
|
||||||
tekton.set_carried(true, self )
|
tekton.set_carried(true, self )
|
||||||
|
|
||||||
# Disposed of AttackMode upon grab
|
# Disposed of AttackMode upon grab
|
||||||
@@ -2250,7 +2226,7 @@ func sync_throw_tekton(target_pos: Vector2i):
|
|||||||
if carried_tekton:
|
if carried_tekton:
|
||||||
var tekton = carried_tekton
|
var tekton = carried_tekton
|
||||||
carried_tekton = null
|
carried_tekton = null
|
||||||
is_carrying_tekton = false
|
self.is_carrying_tekton = false
|
||||||
if tekton.has_method("set_thrown"):
|
if tekton.has_method("set_thrown"):
|
||||||
tekton.set_thrown(true)
|
tekton.set_thrown(true)
|
||||||
else:
|
else:
|
||||||
@@ -2333,7 +2309,7 @@ func sync_drop_tekton():
|
|||||||
if carried_tekton:
|
if carried_tekton:
|
||||||
var tekton = carried_tekton
|
var tekton = carried_tekton
|
||||||
carried_tekton = null
|
carried_tekton = null
|
||||||
is_carrying_tekton = false
|
self.is_carrying_tekton = false
|
||||||
|
|
||||||
# Set its position to player's current position (but on ground)
|
# Set its position to player's current position (but on ground)
|
||||||
var drop_pos = grid_to_world(current_position)
|
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?)
|
# 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.
|
# 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():
|
func enter_attack_mode():
|
||||||
if not is_multiplayer_authority(): return
|
if not is_multiplayer_authority(): return
|
||||||
@@ -2377,18 +2357,7 @@ func enter_knock_mode():
|
|||||||
update_active_player_indicator()
|
update_active_player_indicator()
|
||||||
|
|
||||||
func update_active_player_indicator():
|
func update_active_player_indicator():
|
||||||
var color = Color.WHITE
|
_refresh_player_visuals()
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
@rpc("any_peer", "call_local", "unreliable")
|
@rpc("any_peer", "call_local", "unreliable")
|
||||||
func sync_bump(target_pos: Vector2i, is_soft: bool = false):
|
func sync_bump(target_pos: Vector2i, is_soft: bool = false):
|
||||||
|
|||||||
Reference in New Issue
Block a user