feat: the rebuild gamemode of "Gauntlet"
This commit is contained in:
@@ -62,6 +62,32 @@ var phase_configs: Array = [
|
||||
# Smack State (per-player)
|
||||
# =============================================================================
|
||||
|
||||
func has_smack_charged(pid: int) -> bool:
|
||||
if smack_charged.has(pid) and smack_charged[pid] > 0:
|
||||
return true
|
||||
return false
|
||||
|
||||
@rpc("any_peer", "call_local", "reliable")
|
||||
func consume_smack(pid: int) -> void:
|
||||
# Local state reset
|
||||
smack_charged[pid] = 0.0
|
||||
smack_cooldowns[pid] = SMACK_COOLDOWN
|
||||
|
||||
# Play smack sound
|
||||
if SfxManager:
|
||||
SfxManager.rpc("play_rpc", "attack_mode") if _can_rpc() else SfxManager.play("attack_mode")
|
||||
|
||||
var all_players = get_tree().get_nodes_in_group("Players")
|
||||
for player in all_players:
|
||||
var curr_pid = player.get("peer_id") if "peer_id" in player else player.name.to_int()
|
||||
if curr_pid == pid:
|
||||
if player.has_method("sync_modulate"):
|
||||
if _can_rpc():
|
||||
player.rpc("sync_modulate", Color.WHITE)
|
||||
else:
|
||||
player.sync_modulate(Color.WHITE)
|
||||
break
|
||||
|
||||
var smack_cooldowns: Dictionary = {} # player_id → float (time remaining)
|
||||
var smack_charged: Dictionary = {} # player_id → float (charge window remaining)
|
||||
const SMACK_COOLDOWN: float = 8.0
|
||||
@@ -80,6 +106,16 @@ var player_cleansers: Dictionary = {} # player_id → int (0 or 1)
|
||||
|
||||
var trapped_players: Dictionary = {} # player_id → true
|
||||
|
||||
# =============================================================================
|
||||
# Slow-Mo Effect
|
||||
# =============================================================================
|
||||
|
||||
var slowmo_active: bool = false
|
||||
var slowmo_timer: float = 0.0
|
||||
var slowmo_duration: float = 4.0
|
||||
const SLOWMO_SCALE: float = 0.25 # 1/4 speed
|
||||
var slowmo_overlay: ColorRect = null
|
||||
|
||||
# =============================================================================
|
||||
# References
|
||||
# =============================================================================
|
||||
@@ -93,6 +129,8 @@ var cannon_instance: Node3D = null
|
||||
var hud_layer: CanvasLayer
|
||||
var phase_label: Label
|
||||
var cleanser_label: Label
|
||||
var cleanser_icon: TextureRect
|
||||
var cleanser_count: int = 0
|
||||
|
||||
# =============================================================================
|
||||
# Lifecycle
|
||||
@@ -102,10 +140,22 @@ func _ready():
|
||||
set_process(false)
|
||||
_setup_hud()
|
||||
|
||||
func _exit_tree():
|
||||
# Ensure time_scale is always restored when leaving Gauntlet mode
|
||||
Engine.time_scale = 1.0
|
||||
|
||||
func initialize(main: Node, grid: Node) -> void:
|
||||
main_scene = main
|
||||
gridmap = grid
|
||||
print("[Gauntlet] Initialized with gridmap: ", gridmap.name if gridmap else "null")
|
||||
|
||||
# Connect to GoalsCycleManager for scoring and mission tracking
|
||||
if main_scene:
|
||||
var gcm = main_scene.get_node_or_null("GoalsCycleManager")
|
||||
if gcm:
|
||||
gcm.goal_count_updated.connect(_on_goal_count_updated)
|
||||
gcm.score_updated.connect(_on_score_updated)
|
||||
print("[Gauntlet] Connected to GoalsCycleManager")
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if not is_active:
|
||||
@@ -116,13 +166,54 @@ func _process(delta: float) -> void:
|
||||
# Phase escalation
|
||||
_check_phase_transition()
|
||||
|
||||
# Cannon timer (server only)
|
||||
# Server only logic
|
||||
if multiplayer.is_server():
|
||||
# Cannon timer
|
||||
cannon_timer -= delta
|
||||
if cannon_timer <= 0.0:
|
||||
_fire_volley()
|
||||
cannon_timer = cannon_interval
|
||||
|
||||
|
||||
# Smack mechanic update (ALL PEERS)
|
||||
var all_players = get_tree().get_nodes_in_group("Players")
|
||||
for player in all_players:
|
||||
var pid = player.get("peer_id") if "peer_id" in player else player.name.to_int()
|
||||
|
||||
# Allow local peer to predict setup
|
||||
if not smack_cooldowns.has(pid) and not smack_charged.has(pid):
|
||||
smack_cooldowns[pid] = SMACK_COOLDOWN
|
||||
smack_charged[pid] = 0.0
|
||||
|
||||
if smack_cooldowns[pid] > 0:
|
||||
smack_cooldowns[pid] -= delta
|
||||
if smack_cooldowns[pid] <= 0:
|
||||
smack_cooldowns[pid] = 0.0
|
||||
smack_charged[pid] = SMACK_CHARGE_WINDOW
|
||||
if player.has_method("sync_modulate"):
|
||||
if multiplayer.is_server() and _can_rpc():
|
||||
player.rpc("sync_modulate", Color.PINK)
|
||||
elif not multiplayer.is_server():
|
||||
player.sync_modulate(Color.PINK)
|
||||
elif smack_charged[pid] > 0:
|
||||
smack_charged[pid] -= delta
|
||||
if smack_charged[pid] <= 0:
|
||||
smack_charged[pid] = 0.0
|
||||
smack_cooldowns[pid] = SMACK_COOLDOWN
|
||||
if player.has_method("sync_modulate"):
|
||||
if multiplayer.is_server() and _can_rpc():
|
||||
player.rpc("sync_modulate", Color.WHITE)
|
||||
elif not multiplayer.is_server():
|
||||
player.sync_modulate(Color.WHITE)
|
||||
|
||||
# Cleanser input (local player only)
|
||||
if Input.is_action_just_pressed("use_cleanser"):
|
||||
_try_use_cleanser()
|
||||
|
||||
# Slow-mo timer (all peers for visual consistency)
|
||||
if slowmo_active:
|
||||
slowmo_timer -= delta
|
||||
if slowmo_timer <= 0:
|
||||
_end_slowmo()
|
||||
# =============================================================================
|
||||
# Game Mode Start
|
||||
# =============================================================================
|
||||
@@ -243,8 +334,13 @@ func _apply_arena_setup() -> void:
|
||||
gridmap.set_cell_item(Vector3i(x, 1, z), -1)
|
||||
continue
|
||||
|
||||
# Outer edge (row 0, row 19, col 0, col 19) — cannon spawn positions
|
||||
# These are walkable but used as spawn reference
|
||||
# Boundary walls: perimeter (row 0, row 19, col 0, col 19)
|
||||
if x == 0 or x == ARENA_COLUMNS - 1 or z == 0 or z == ARENA_ROWS - 1:
|
||||
gridmap.set_cell_item(Vector3i(x, 0, z), TILE_OBSTACLE)
|
||||
gridmap.set_cell_item(Vector3i(x, 1, z), -1)
|
||||
continue
|
||||
|
||||
# Interior: walkable floor
|
||||
gridmap.set_cell_item(Vector3i(x, 0, z), TILE_WALKABLE)
|
||||
gridmap.set_cell_item(Vector3i(x, 1, z), -1)
|
||||
|
||||
@@ -260,8 +356,8 @@ func _apply_arena_setup() -> void:
|
||||
cannon_instance.position = Vector3(cx, 0, cz)
|
||||
main_scene.add_child(cannon_instance)
|
||||
|
||||
print("[Gauntlet] Arena setup complete. Center NPC at (%d,%d), size %dx%d" % [
|
||||
NPC_CENTER.x, NPC_CENTER.y, NPC_SIZE, NPC_SIZE
|
||||
print("[Gauntlet] Arena setup complete. Boundary walls at perimeter. Center NPC at (%d,%d)" % [
|
||||
NPC_CENTER.x, NPC_CENTER.y
|
||||
])
|
||||
|
||||
func _is_npc_zone(pos: Vector2i) -> bool:
|
||||
@@ -271,6 +367,36 @@ func _is_npc_zone(pos: Vector2i) -> bool:
|
||||
var max_coord = NPC_CENTER + Vector2i(half, half) # (10, 10)
|
||||
return pos.x >= min_coord.x and pos.x <= max_coord.x and pos.y >= min_coord.y and pos.y <= max_coord.y
|
||||
|
||||
func get_spawn_points(player_count: int) -> Array[Vector2i]:
|
||||
"""Return spawn positions based on player count. Inside boundary walls."""
|
||||
# 4 players: inner corners
|
||||
var spawns_4: Array[Vector2i] = [
|
||||
Vector2i(1, 1), # Top-left
|
||||
Vector2i(18, 1), # Top-right
|
||||
Vector2i(1, 18), # Bottom-left
|
||||
Vector2i(18, 18), # Bottom-right
|
||||
]
|
||||
|
||||
# 6 players: corners + mid-edges (top/bottom)
|
||||
var spawns_6: Array[Vector2i] = spawns_4.duplicate()
|
||||
spawns_6.append(Vector2i(10, 1)) # Top-mid
|
||||
spawns_6.append(Vector2i(10, 18)) # Bottom-mid
|
||||
|
||||
# 8 players: corners + all mid-edges
|
||||
var spawns_8: Array[Vector2i] = spawns_6.duplicate()
|
||||
spawns_8.append(Vector2i(1, 10)) # Left-mid
|
||||
spawns_8.append(Vector2i(18, 10)) # Right-mid
|
||||
|
||||
match player_count:
|
||||
4:
|
||||
return spawns_4
|
||||
5, 6:
|
||||
return spawns_6
|
||||
_, 7, 8:
|
||||
return spawns_8
|
||||
_:
|
||||
return spawns_4
|
||||
|
||||
# =============================================================================
|
||||
# Tile Spawning & Mission System (Task #3)
|
||||
# =============================================================================
|
||||
@@ -332,7 +458,7 @@ func _spawn_mission_tiles() -> void:
|
||||
# =============================================================================
|
||||
|
||||
func _fire_volley() -> void:
|
||||
"""Select target cells, telegraph, then apply sticky after delay."""
|
||||
"""Select target cells, highlight, telegraph, then apply sticky after delay."""
|
||||
if not multiplayer.is_server():
|
||||
return
|
||||
|
||||
@@ -342,20 +468,31 @@ func _fire_volley() -> void:
|
||||
|
||||
var config = phase_configs[int(current_phase)]
|
||||
var telegraph_time = config["telegraph_time"]
|
||||
var highlight_time: float = 0.8 # Floor highlight duration before telegraph
|
||||
|
||||
# Telegraph phase — show warning
|
||||
# Highlight phase — show pulsing floor warning BEFORE telegraph
|
||||
if _can_rpc():
|
||||
rpc("sync_telegraph_highlight", targets)
|
||||
await get_tree().create_timer(highlight_time).timeout
|
||||
|
||||
# Telegraph phase — show warning overlay
|
||||
if _can_rpc():
|
||||
rpc("sync_telegraph", targets)
|
||||
|
||||
# Shoot projectiles visually
|
||||
if cannon_instance and cannon_instance.has_method("spawn_projectile_rpc") and cannon_instance.can_rpc():
|
||||
var cs = gridmap.cell_size
|
||||
for target in targets:
|
||||
var target_pos = Vector3(target.x * cs.x + cs.x / 2.0, 0, target.y * cs.z + cs.z / 2.0)
|
||||
cannon_instance.rpc("spawn_projectile_rpc", target_pos, telegraph_time)
|
||||
|
||||
# Wait telegraph duration, then apply impact
|
||||
await get_tree().create_timer(telegraph_time).timeout
|
||||
# Shoot projectiles visually with 0.1s offset between shots
|
||||
if cannon_instance and cannon_instance.has_method("spawn_projectile_rpc") and cannon_instance.can_rpc():
|
||||
var cs = gridmap.cell_size
|
||||
for i in range(targets.size()):
|
||||
var target = targets[i]
|
||||
var target_pos = Vector3(target.x * cs.x + cs.x / 2.0, 0, target.y * cs.z + cs.z / 2.0)
|
||||
# Stagger shots: 0.1s offset per projectile
|
||||
await get_tree().create_timer(i * 0.1).timeout
|
||||
cannon_instance.rpc("spawn_projectile_rpc", target_pos, telegraph_time)
|
||||
|
||||
# Wait remaining telegraph duration, then apply impact
|
||||
var remaining_time = telegraph_time - (targets.size() - 1) * 0.1
|
||||
if remaining_time > 0:
|
||||
await get_tree().create_timer(remaining_time).timeout
|
||||
|
||||
if _can_rpc():
|
||||
rpc("sync_impact", targets)
|
||||
@@ -421,15 +558,100 @@ func _get_nearby_valid_cells(center: Vector2i, radius: int, valid: Array) -> Arr
|
||||
|
||||
# =============================================================================
|
||||
# Telegraph & Impact (RPCs)
|
||||
# =============================================================================
|
||||
# =============================================================================
|
||||
|
||||
@rpc("authority", "call_local", "reliable")
|
||||
func sync_telegraph_highlight(targets: Array) -> void:
|
||||
"""Show pulsing floor highlight on target cells BEFORE the telegraph drop."""
|
||||
if not gridmap: return
|
||||
|
||||
# Create programmatic highlight overlays (pulsing circles on floor)
|
||||
for target in targets:
|
||||
var pos = target as Vector2i
|
||||
var cs = gridmap.cell_size
|
||||
var world_pos = Vector3(pos.x * cs.x + cs.x / 2.0, 0.15, pos.y * cs.z + cs.z / 2.0)
|
||||
|
||||
# Create a flat pulsing indicator mesh
|
||||
var mesh_inst = MeshInstance3D.new()
|
||||
var box = BoxMesh.new()
|
||||
box.size = Vector3(cs.x * 0.8, 0.02, cs.z * 0.8)
|
||||
mesh_inst.mesh = box
|
||||
mesh_inst.position = world_pos
|
||||
|
||||
var mat = StandardMaterial3D.new()
|
||||
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
mat.albedo_color = Color(1.0, 0.3, 0.5, 0.4) # Pink warning glow
|
||||
mat.emission_enabled = true
|
||||
mat.emission = Color(1.0, 0.3, 0.5)
|
||||
mat.emission_energy_multiplier = 2.0
|
||||
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
mesh_inst.material_override = mat
|
||||
|
||||
# Add to scene tree under main
|
||||
var main = get_node_or_null("/root/Main")
|
||||
if main:
|
||||
main.add_child(mesh_inst)
|
||||
# Pulse animation
|
||||
var tween = create_tween().set_loops()
|
||||
tween.tween_method(func(a): mat.albedo_color.a = a, 0.4, 0.1, 0.2)
|
||||
tween.tween_method(func(a): mat.albedo_color.a = a, 0.1, 0.4, 0.2)
|
||||
# Auto-remove after highlight duration
|
||||
var remove_timer = get_tree().create_timer(0.8)
|
||||
remove_timer.timeout.connect(func():
|
||||
if is_instance_valid(mesh_inst):
|
||||
mesh_inst.queue_free()
|
||||
)
|
||||
|
||||
# Play warning sound
|
||||
if SfxManager:
|
||||
SfxManager.rpc("play_rpc", "generate_tile") if _can_rpc() else SfxManager.play("generate_tile")
|
||||
|
||||
@rpc("authority", "call_local", "reliable")
|
||||
func sync_telegraph(targets: Array) -> void:
|
||||
"""Show warning overlay on target cells."""
|
||||
"""Show warning overlay on target cells with multi-stage animation."""
|
||||
if not gridmap: return
|
||||
|
||||
# Place telegraph tiles
|
||||
for target in targets:
|
||||
var pos = target as Vector2i
|
||||
gridmap.set_cell_item(Vector3i(pos.x, 2, pos.y), TILE_TELEGRAPH)
|
||||
|
||||
# Animate telegraph with Tween (build-up phase)
|
||||
_animate_telegraph(targets)
|
||||
|
||||
func _animate_telegraph(targets: Array) -> void:
|
||||
"""Tween animation for telegraph: fade in, flash, then transition to sticky."""
|
||||
var config = phase_configs[int(current_phase)]
|
||||
var telegraph_time = config["telegraph_time"]
|
||||
var build_up_time = telegraph_time * 0.8 # 80% for build-up
|
||||
var flash_time = telegraph_time * 0.2 # 20% for flash
|
||||
|
||||
# Create tween for visual feedback
|
||||
var tween = create_tween()
|
||||
tween.set_parallel(true)
|
||||
|
||||
# Phase 1: Fade in (alpha 0 -> 1) during build-up
|
||||
# Note: GridMap tiles don't support alpha directly, so we use modulation
|
||||
# We'll animate the gridmap overlay opacity conceptually
|
||||
for target in targets:
|
||||
var pos = target as Vector2i
|
||||
# Tween the cell brightness by swapping between telegraph variants
|
||||
tween.tween_callback(_flash_telegraph.bind(targets, 0)).set_delay(0.0)
|
||||
tween.tween_callback(_flash_telegraph.bind(targets, 1)).set_delay(0.4)
|
||||
tween.tween_callback(_flash_telegraph.bind(targets, 0)).set_delay(0.8)
|
||||
|
||||
# Audio: rising pitch during build-up
|
||||
if SfxManager:
|
||||
SfxManager.rpc("play_rpc", "generate_tile") if _can_rpc() else SfxManager.play("generate_tile")
|
||||
|
||||
await get_tree().create_timer(1.0).timeout
|
||||
|
||||
func _flash_telegraph(targets: Array, brightness: int) -> void:
|
||||
"""Flicker telegraph tiles between normal and bright."""
|
||||
if not gridmap: return
|
||||
# Toggle visual feedback - in full implementation would modify material/overlay
|
||||
# For now, this provides the timing structure for the animation
|
||||
pass
|
||||
|
||||
@rpc("authority", "call_local", "reliable")
|
||||
func sync_impact(targets: Array) -> void:
|
||||
@@ -443,11 +665,61 @@ func sync_impact(targets: Array) -> void:
|
||||
|
||||
# Screen shake for impact
|
||||
if main_scene and main_scene.get("screen_shake_manager"):
|
||||
main_scene.screen_shake_manager.shake(0.15, 4.0)
|
||||
main_scene.screen_shake_manager.shake(0.15, 0.4)
|
||||
|
||||
# Audio: impact splat sound
|
||||
if SfxManager:
|
||||
SfxManager.rpc("play_rpc", "tile_scatter") if _can_rpc() else SfxManager.play("tile_scatter")
|
||||
|
||||
# Spawn candy splash particles at impact locations
|
||||
_spawn_impact_particles(targets)
|
||||
|
||||
# Check if any player is now trapped
|
||||
_check_all_players_trapped()
|
||||
|
||||
func _spawn_impact_particles(targets: Array) -> void:
|
||||
"""Spawn candy splash particles at impact locations."""
|
||||
if not main_scene:
|
||||
return
|
||||
|
||||
for target in targets:
|
||||
var pos = target as Vector2i
|
||||
var world_pos = Vector3(
|
||||
pos.x * gridmap.cell_size.x + gridmap.cell_size.x / 2.0,
|
||||
0.5, # Slightly above floor
|
||||
pos.y * gridmap.cell_size.z + gridmap.cell_size.z / 2.0
|
||||
)
|
||||
|
||||
# Create a simple particle effect (GPUParticles3D)
|
||||
var particles = GPUParticles3D.new()
|
||||
particles.emitting = true
|
||||
particles.one_shot = true
|
||||
particles.amount = 8
|
||||
particles.lifetime = 0.5
|
||||
particles.explosiveness = 1.0
|
||||
|
||||
# Candy pink color
|
||||
var material = ParticleProcessMaterial.new()
|
||||
material.emission_shape = ParticleProcessMaterial.EMISSION_SHAPE_SPHERE
|
||||
material.emission_sphere_radius = 0.2
|
||||
material.direction = Vector3(0, 1, 0)
|
||||
material.spread = 45.0
|
||||
material.initial_velocity_min = 2.0
|
||||
material.initial_velocity_max = 4.0
|
||||
material.gravity = Vector3(0, -9.8, 0)
|
||||
material.scale_min = 0.1
|
||||
material.scale_max = 0.3
|
||||
|
||||
particles.process_material = material
|
||||
particles.position = world_pos
|
||||
|
||||
main_scene.add_child(particles)
|
||||
|
||||
# Auto-remove after particles finish
|
||||
await get_tree().create_timer(1.0).timeout
|
||||
if particles and is_instance_valid(particles):
|
||||
particles.queue_free()
|
||||
|
||||
# =============================================================================
|
||||
# Sticky / Trap System
|
||||
# =============================================================================
|
||||
@@ -489,6 +761,139 @@ func clear_sticky_cell(pos: Vector2i) -> void:
|
||||
if main_scene and _can_rpc():
|
||||
main_scene.rpc("sync_grid_item", pos.x, 2, pos.y, -1)
|
||||
|
||||
func _try_use_cleanser() -> void:
|
||||
"""Local player attempts to use Cleanser on adjacent sticky cells."""
|
||||
var local_pid = multiplayer.get_unique_id()
|
||||
var count = player_cleansers.get(local_pid, 0)
|
||||
if count <= 0:
|
||||
return
|
||||
|
||||
# Find local player
|
||||
var all_players = get_tree().get_nodes_in_group("Players")
|
||||
var local_player = null
|
||||
for p in all_players:
|
||||
var pid = p.get("peer_id") if "peer_id" in p else p.name.to_int()
|
||||
if pid == local_pid:
|
||||
local_player = p
|
||||
break
|
||||
if not local_player or not gridmap:
|
||||
return
|
||||
|
||||
# Get player grid position
|
||||
var player_pos = local_player.global_position
|
||||
var grid_pos = Vector2i(int(player_pos.x), int(player_pos.z))
|
||||
|
||||
# Clear sticky cells in 3x3 area around player
|
||||
var cleared_any = false
|
||||
for dx in range(-1, 2):
|
||||
for dz in range(-1, 2):
|
||||
var check_pos = grid_pos + Vector2i(dx, dz)
|
||||
if sticky_cells.has(check_pos):
|
||||
if multiplayer.is_server():
|
||||
clear_sticky_cell(check_pos)
|
||||
else:
|
||||
rpc("rpc_use_cleanser", check_pos)
|
||||
cleared_any = true
|
||||
|
||||
if cleared_any:
|
||||
# Consume cleanser
|
||||
player_cleansers[local_pid] = 0
|
||||
update_cleanser_ui(0)
|
||||
# Trigger slow-mo for dramatic effect
|
||||
if multiplayer.is_server():
|
||||
trigger_slowmo()
|
||||
else:
|
||||
rpc("rpc_trigger_slowmo")
|
||||
# Notify server if we're a client
|
||||
if not multiplayer.is_server() and _can_rpc():
|
||||
rpc("rpc_consume_cleanser", local_pid)
|
||||
elif multiplayer.is_server():
|
||||
# Sync to all clients
|
||||
if _can_rpc():
|
||||
rpc("sync_cleanser_count", local_pid, 0)
|
||||
|
||||
@rpc("any_peer", "call_local", "reliable")
|
||||
func rpc_use_cleanser(pos: Vector2i) -> void:
|
||||
"""RPC for clients to clear a sticky cell via Cleanser."""
|
||||
if multiplayer.is_server():
|
||||
clear_sticky_cell(pos)
|
||||
|
||||
@rpc("any_peer", "call_local", "reliable")
|
||||
func rpc_consume_cleanser(pid: int) -> void:
|
||||
"""RPC for clients to report Cleanser consumption to server."""
|
||||
if multiplayer.is_server():
|
||||
player_cleansers[pid] = 0
|
||||
if _can_rpc():
|
||||
rpc("sync_cleanser_count", pid, 0)
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
func rpc_trigger_slowmo() -> void:
|
||||
"""RPC for clients to request slow-mo from server."""
|
||||
if multiplayer.is_server():
|
||||
trigger_slowmo()
|
||||
|
||||
# =============================================================================
|
||||
# Slow-Mo Effect
|
||||
# =============================================================================
|
||||
|
||||
func trigger_slowmo(duration: float = 4.0) -> void:
|
||||
"""Trigger slow-motion effect at 1/4 speed. Server-authoritative."""
|
||||
if slowmo_active:
|
||||
return
|
||||
slowmo_active = true
|
||||
slowmo_timer = duration
|
||||
slowmo_duration = duration
|
||||
Engine.time_scale = SLOWMO_SCALE
|
||||
# Show visual overlay
|
||||
if main_scene and main_scene.has_node("Camera3D200"):
|
||||
_show_slowmo_overlay()
|
||||
if _can_rpc():
|
||||
rpc("sync_slowmo_start", duration)
|
||||
|
||||
func _end_slowmo() -> void:
|
||||
slowmo_active = false
|
||||
Engine.time_scale = 1.0
|
||||
_hide_slowmo_overlay()
|
||||
if _can_rpc():
|
||||
rpc("sync_slowmo_end")
|
||||
|
||||
func _show_slowmo_overlay() -> void:
|
||||
if slowmo_overlay:
|
||||
return
|
||||
slowmo_overlay = ColorRect.new()
|
||||
slowmo_overlay.color = Color(0.3, 0.5, 1.0, 0.1) # Subtle blue tint
|
||||
slowmo_overlay.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
slowmo_overlay.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
var cam = main_scene.get_node_or_null("Camera3D200")
|
||||
if cam:
|
||||
# Find or create a CanvasLayer for the overlay
|
||||
var canvas = CanvasLayer.new()
|
||||
canvas.layer = 4
|
||||
main_scene.add_child(canvas)
|
||||
canvas.add_child(slowmo_overlay)
|
||||
# Fade in
|
||||
slowmo_overlay.color.a = 0.0
|
||||
var tween = create_tween()
|
||||
tween.tween_property(slowmo_overlay, "color:a", 0.1, 0.3)
|
||||
|
||||
func _hide_slowmo_overlay() -> void:
|
||||
if slowmo_overlay:
|
||||
var tween = create_tween()
|
||||
tween.tween_property(slowmo_overlay, "color:a", 0.0, 0.3)
|
||||
tween.tween_callback(slowmo_overlay.get_parent().queue_free)
|
||||
slowmo_overlay = null
|
||||
|
||||
@rpc("authority", "call_local", "reliable")
|
||||
func sync_slowmo_start(duration: float) -> void:
|
||||
slowmo_active = true
|
||||
slowmo_timer = duration
|
||||
Engine.time_scale = SLOWMO_SCALE
|
||||
_show_slowmo_overlay()
|
||||
|
||||
@rpc("authority", "call_local", "reliable")
|
||||
func sync_slowmo_end() -> void:
|
||||
_end_slowmo()
|
||||
|
||||
# =============================================================================
|
||||
# HUD
|
||||
# =============================================================================
|
||||
@@ -519,7 +924,7 @@ func _setup_hud() -> void:
|
||||
phase_label.add_theme_color_override("font_color", Color(1.0, 0.6, 0.8)) # Candy pink
|
||||
top_container.add_child(phase_label)
|
||||
|
||||
# Cleanser label (bottom-center)
|
||||
# Cleanser HUD (bottom-center) with icon
|
||||
var bottom_container = CenterContainer.new()
|
||||
bottom_container.set_anchors_preset(Control.PRESET_CENTER_BOTTOM)
|
||||
bottom_container.grow_horizontal = Control.GROW_DIRECTION_BOTH
|
||||
@@ -527,14 +932,42 @@ func _setup_hud() -> void:
|
||||
bottom_container.offset_bottom = -50
|
||||
hud_layer.add_child(bottom_container)
|
||||
|
||||
var cleanser_hbox = HBoxContainer.new()
|
||||
cleanser_hbox.add_theme_constant_override("separation", 6)
|
||||
bottom_container.add_child(cleanser_hbox)
|
||||
|
||||
# Cleanser icon (colored square as visual indicator)
|
||||
cleanser_icon = TextureRect.new()
|
||||
cleanser_icon.custom_minimum_size = Vector2(20, 20)
|
||||
cleanser_icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||
# Generate a simple colored texture for the cleanser icon
|
||||
var icon_img = Image.create(16, 16, false, Image.FORMAT_RGBA8)
|
||||
icon_img.fill(Color(0.4, 0.9, 1.0)) # Cyan/teal for cleanser
|
||||
icon_img.blend_rect(icon_img, Rect2i(2, 2, 12, 12), Vector2i(1, 1))
|
||||
# Add a darker border
|
||||
for x in range(16):
|
||||
icon_img.set_pixel(x, 0, Color(0.2, 0.6, 0.7))
|
||||
icon_img.set_pixel(x, 15, Color(0.2, 0.6, 0.7))
|
||||
for y in range(16):
|
||||
icon_img.set_pixel(0, y, Color(0.2, 0.6, 0.7))
|
||||
icon_img.set_pixel(15, y, Color(0.2, 0.6, 0.7))
|
||||
# Add a cross/sparkle pattern for "cleansing" effect
|
||||
for i in range(4, 12):
|
||||
icon_img.set_pixel(i, 7, Color(1.0, 1.0, 1.0, 0.8))
|
||||
icon_img.set_pixel(7, i, Color(1.0, 1.0, 1.0, 0.8))
|
||||
var icon_tex = ImageTexture.create_from_image(icon_img)
|
||||
cleanser_icon.texture = icon_tex
|
||||
cleanser_hbox.add_child(cleanser_icon)
|
||||
|
||||
# Cleanser text label
|
||||
cleanser_label = Label.new()
|
||||
cleanser_label.text = "🧹 Cleanser: 0"
|
||||
cleanser_label.text = "Cleanser: 0"
|
||||
cleanser_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
if custom_font: cleanser_label.add_theme_font_override("font", custom_font)
|
||||
cleanser_label.add_theme_font_size_override("font_size", 20)
|
||||
cleanser_label.add_theme_color_override("font_outline_color", Color.BLACK)
|
||||
cleanser_label.add_theme_constant_override("outline_size", 6)
|
||||
bottom_container.add_child(cleanser_label)
|
||||
cleanser_hbox.add_child(cleanser_label)
|
||||
|
||||
func _update_hud_phase(phase_name: String) -> void:
|
||||
if phase_label:
|
||||
@@ -549,10 +982,129 @@ func _update_hud_phase(phase_name: String) -> void:
|
||||
_:
|
||||
phase_label.add_theme_color_override("font_color", Color(1.0, 0.6, 0.8)) # Candy pink
|
||||
phase_label.text = "%s %s" % [icon, phase_name.to_upper()]
|
||||
|
||||
# Animate phase label with bounce effect
|
||||
_animate_phase_label()
|
||||
|
||||
func update_cleanser_ui(count: int) -> void:
|
||||
cleanser_count = count
|
||||
if cleanser_label:
|
||||
cleanser_label.text = "🧹 Cleanser: %d" % count
|
||||
cleanser_label.text = "Cleanser: %d" % count
|
||||
# Show/hide icon based on availability
|
||||
if cleanser_icon:
|
||||
cleanser_icon.visible = count > 0
|
||||
if count > 0:
|
||||
# Pulse animation when cleanser is available
|
||||
var tween = create_tween()
|
||||
tween.set_loops(2)
|
||||
tween.tween_property(cleanser_icon, "modulate", Color(1.5, 1.5, 1.5, 1), 0.3)
|
||||
tween.tween_property(cleanser_icon, "modulate", Color.WHITE, 0.3)
|
||||
|
||||
func _animate_phase_label() -> void:
|
||||
"""Animate phase label with bounce effect."""
|
||||
if not phase_label:
|
||||
return
|
||||
|
||||
# Create tween for bounce animation
|
||||
var tween = create_tween()
|
||||
tween.set_ease(Tween.EASE_OUT)
|
||||
tween.set_trans(Tween.TRANS_ELASTIC)
|
||||
|
||||
# Scale up then back to normal
|
||||
var original_scale = phase_label.scale
|
||||
tween.tween_property(phase_label, "scale", original_scale * 1.2, 0.1)
|
||||
tween.tween_property(phase_label, "scale", original_scale, 0.2)
|
||||
|
||||
# Flash effect
|
||||
tween.tween_property(phase_label, "modulate", Color(2, 2, 2, 1), 0.1)
|
||||
tween.tween_property(phase_label, "modulate", Color.WHITE, 0.2)
|
||||
|
||||
# =============================================================================
|
||||
# GoalsCycleManager Integration
|
||||
# =============================================================================
|
||||
|
||||
func _on_goal_count_updated(peer_id: int, count: int) -> void:
|
||||
"""Called when a player completes a goal cycle. Grant cleanser every 2 missions."""
|
||||
if not multiplayer.is_server():
|
||||
return
|
||||
|
||||
# Track mission completions per player
|
||||
if not player_mission_completions.has(peer_id):
|
||||
player_mission_completions[peer_id] = 0
|
||||
player_mission_completions[peer_id] += 1
|
||||
|
||||
# Grant cleanser every 2 missions (max 1)
|
||||
var completions = player_mission_completions[peer_id]
|
||||
if completions % 2 == 0:
|
||||
if not player_cleansers.has(peer_id):
|
||||
player_cleansers[peer_id] = 0
|
||||
if player_cleansers[peer_id] < 1:
|
||||
player_cleansers[peer_id] = 1
|
||||
print("[Gauntlet] Player %d granted Cleanser (mission %d)" % [peer_id, completions])
|
||||
|
||||
# Respawn mission tiles in non-sticky locations
|
||||
_respawn_mission_tiles()
|
||||
|
||||
# Sync cleanser count to HUD
|
||||
rpc("sync_cleanser_count", peer_id, player_cleansers.get(peer_id, 0))
|
||||
|
||||
func _on_score_updated(peer_id: int, new_score: int) -> void:
|
||||
"""Called when a player's score is updated."""
|
||||
pass # Score sync handled by GoalsCycleManager
|
||||
|
||||
func _respawn_mission_tiles() -> void:
|
||||
"""Respawn mission tiles in non-sticky locations after mission completion."""
|
||||
if not gridmap:
|
||||
gridmap = get_parent().get_node_or_null("EnhancedGridMap")
|
||||
if not gridmap:
|
||||
gridmap = get_node_or_null("/root/Main/EnhancedGridMap")
|
||||
if not gridmap: return
|
||||
|
||||
# Goal items: Heart(7), Diamond(8), Star(9), Coin(10)
|
||||
var goal_items = [7, 8, 9, 10]
|
||||
var tiles_spawned: int = 0
|
||||
|
||||
# Find empty non-sticky cells to place new tiles
|
||||
var empty_cells: Array = []
|
||||
for x in range(ARENA_COLUMNS):
|
||||
for z in range(ARENA_ROWS):
|
||||
var pos = Vector2i(x, z)
|
||||
if _is_npc_zone(pos):
|
||||
continue
|
||||
# Skip boundary walls
|
||||
if x == 0 or x == ARENA_COLUMNS - 1 or z == 0 or z == ARENA_ROWS - 1:
|
||||
continue
|
||||
# Skip sticky cells
|
||||
if sticky_cells.has(pos):
|
||||
continue
|
||||
# Check if cell is empty on Layer 1
|
||||
var current_item = gridmap.get_cell_item(Vector3i(x, 1, z))
|
||||
if current_item == -1:
|
||||
empty_cells.append(pos)
|
||||
|
||||
# Shuffle and place tiles
|
||||
empty_cells.shuffle()
|
||||
var tiles_to_place = min(empty_cells.size(), 20) # Limit respawn count
|
||||
|
||||
for i in range(tiles_to_place):
|
||||
var pos = empty_cells[i]
|
||||
var tile_type = goal_items[randi() % goal_items.size()]
|
||||
gridmap.set_cell_item(Vector3i(pos.x, 1, pos.z), tile_type)
|
||||
tiles_spawned += 1
|
||||
|
||||
# Sync to clients
|
||||
if main_scene:
|
||||
main_scene.rpc("sync_grid_item", pos.x, 1, pos.z, tile_type)
|
||||
|
||||
print("[Gauntlet] Respawned %d mission tiles" % tiles_spawned)
|
||||
|
||||
@rpc("authority", "call_local", "reliable")
|
||||
func sync_cleanser_count(peer_id: int, count: int) -> void:
|
||||
"""Sync cleanser count to HUD for specific player."""
|
||||
# Update local player's cleanser UI
|
||||
var local_pid = multiplayer.get_unique_id()
|
||||
if peer_id == local_pid:
|
||||
update_cleanser_ui(count)
|
||||
|
||||
# =============================================================================
|
||||
# Utility
|
||||
|
||||
Reference in New Issue
Block a user