feat: Introduce an EnhancedGridMap with advanced generation, randomization, pathfinding, and data serialization, along with new player, powerup, and portal managers.

This commit is contained in:
Yogi Wiguna
2026-03-04 17:40:10 +08:00
parent 8f03cc15c5
commit cd7881bc3f
12 changed files with 128 additions and 107 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ signal grid_updated
@export var hover_item: int = 1
@export var start_item: int = 2
@export var end_item: int = 3
@export var immutable_items: Array[int] = [4] # Items that cannot be randomized/reset
@export var immutable_items: Array[int] = [1, 2, 3, 4] # Items that cannot be randomized/reset (Start, Safe, Finish, Wall)
var current_mesh_library: MeshLibrary
var grid_data: Array = [] # 3D array [floor][row][column]
+16 -13
View File
@@ -1581,12 +1581,15 @@ func request_randomize_item(grid_position: Vector2i):
func sync_grid_item(x: int, y: int, z: int, item: int):
var enhanced_gridmap = $EnhancedGridMap
if enhanced_gridmap:
# WALL-SAFETY CHECK: Block tiles (7-20) from being placed on walls (4) or void (-1)
# PROTECTED FLOOR CHECK: Block tiles (7-20) from being placed on walls (4) or void (-1)
# Note: We allow spawning on Safe Zones, Start, and Finish as it's on Layer 1.
if y == 1 and item >= 7 and item <= 20:
var f0 = enhanced_gridmap.get_cell_item(Vector3i(x, 0, z))
if f0 == 4 or f0 == -1:
# Log and block illegal placement
print("[Main] Blocked illegal tile (%d) placement on wall/void at (%d, %d)" % [item, x, z])
var f1 = enhanced_gridmap.get_cell_item(Vector3i(x, 1, z))
# Block if Layer 0 is Wall (4) or Void (-1)
# OR Layer 1 is already a wall (4 or 13)
if f0 in [4, -1] or f1 == 4 or f1 == 13:
return
enhanced_gridmap.set_cell_item(Vector3i(x, y, z), item)
@@ -1607,10 +1610,11 @@ func sync_grid_items_batch(data: Array):
var z = entry.get("z", 0)
var item = entry.get("item", -1)
# WALL-SAFETY CHECK
# PROTECTED FLOOR CHECK
if y == 1 and item >= 7 and item <= 20:
var f0 = enhanced_gridmap.get_cell_item(Vector3i(x, 0, z))
if f0 == 4 or f0 == -1:
var f1 = enhanced_gridmap.get_cell_item(Vector3i(x, 1, z))
if f0 in [4, -1] or f1 == 4 or f1 == 13:
continue
enhanced_gridmap.set_cell_item(Vector3i(x, y, z), item)
@@ -1625,13 +1629,12 @@ func randomize_game_grid():
var enhanced_gridmap = $EnhancedGridMap
if enhanced_gridmap:
# Randomize Floor 1 using ScarcityController
enhanced_gridmap.randomize_floor(1, ScarcityController.get_random_tile_id)
# Sync to clients if needed (usually handled by initial state sync or explicit item syncs)
# Since Main.gd doesn't have a "Sync Floor" RPC, we rely on clients running the same seed or syncing individual cells.
# For now, let's assume server authority + sync on connect handles it, or add sync loop if critical.
pass
# Use density-aware callable: 60% chance for a real tile, 40% for none
var density_callable = func():
if randf() > 0.6: return -1
return ScarcityController.get_random_tile_id()
enhanced_gridmap.randomize_floor(1, density_callable)
@rpc("authority", "call_local", "reliable")
func sync_full_grid_data_stop_n_go(floor0: PackedInt32Array, floor1: PackedInt32Array, cols: int, rows: int):
+2 -11
View File
@@ -32,7 +32,6 @@
[ext_resource type="Texture2D" uid="uid://3up2su2e0lfa" path="res://assets/graphics/touch_control/freeze_area.png" id="28_fv21b"]
[ext_resource type="Texture2D" uid="uid://ckhdyxnho6sjp" path="res://assets/graphics/touch_control/spawn_tile.png" id="28_j8jky"]
[ext_resource type="Texture2D" uid="uid://b2vhatfmufn3d" path="res://assets/graphics/touch_control/ghost.png" id="33_5q0nq"]
[ext_resource type="Texture2D" uid="uid://cdwk17moidkj2" path="res://assets/graphics/touch_control/knock_tekton.png" id="35_fuf3a"]
[ext_resource type="Texture2D" uid="uid://biun2yvglxgij" path="res://assets/graphics/touch_control/grab_tekton.png" id="36_pibwh"]
[ext_resource type="Script" uid="uid://86ikh0wuqk7v" path="res://scripts/ui/powerup_inventory_ui.gd" id="powerup_ui_script"]
[ext_resource type="Script" uid="uid://b54tfa0n6kogi" path="res://scripts/managers/touch_controls.gd" id="touch_manager"]
@@ -99,6 +98,7 @@ columns = 14
rows = 14
floors = 2
auto_randomize = true
immutable_items = Array[int]([4])
metadata/_editor_floor_ = Vector3(0, 1, 0)
[node name="Camera3D" type="Camera3D" parent="." unique_id=1200003163]
@@ -9808,16 +9808,7 @@ flat = true
icon_alignment = 1
expand_icon = true
[node name="TektonKnockBtn" type="Button" parent="TouchControls/TouchControls/ActionsBtn" unique_id=2133168886]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
icon = ExtResource("35_fuf3a")
flat = true
icon_alignment = 1
expand_icon = true
[node name="TektonThrowBtn" type="Button" parent="TouchControls/TouchControls/ActionsBtn" unique_id=2097928368]
[node name="TektonGrabBtn" type="Button" parent="TouchControls/TouchControls/ActionsBtn" unique_id=2097928368]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
+31
View File
@@ -707,6 +707,9 @@ var immunity_timer: float = 0.0
@rpc("any_peer", "call_local")
func apply_stagger(duration: float = 1.5):
if is_carrying_tekton:
return # Cannot be staggered while carrying a Tekton
if immunity_timer > 0:
return # Immune!
@@ -2191,6 +2194,34 @@ func sync_throw_tekton(target_pos: Vector2i):
print("[Player %s] Threw Tekton to %s (Dist: %s)" % [name, target_pos, target_pos.distance_to(tekton.current_position)])
func drop_tekton():
"""Drops the Tekton at the current player position immediately."""
if not is_multiplayer_authority() or not is_carrying_tekton:
return
if is_multiplayer_authority() and can_rpc():
rpc("sync_drop_tekton")
@rpc("any_peer", "call_local", "reliable")
func sync_drop_tekton():
if carried_tekton:
var tekton = carried_tekton
carried_tekton = null
is_carrying_tekton = false
# Set its position to player's current position (but on ground)
var drop_pos = grid_to_world(current_position)
tekton.global_position = drop_pos
if tekton.has_method("set_carried"):
tekton.set_carried(false)
# Trigger landing effects (minimal scale)
if tekton.has_method("on_thrown_landing"):
tekton.on_thrown_landing(self, 1.0) # Minimal scale impact
print("[Player %s] Dropped Tekton at %s" % [name, current_position])
# 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
+3 -16
View File
@@ -120,26 +120,13 @@ func handle_unhandled_input(event):
player.enter_attack_mode()
KEY_E:
if player.powerup_manager:
# Spawn Boost
if player.is_carrying_tekton and player.powerup_manager:
# Spawn Boost (Now uses Tekton)
if player.powerup_manager.has_method("spawn_boost_reward"):
player.powerup_manager.spawn_boost_reward()
else:
# Fallback if method missing
player.powerup_manager.use_special_effect()
KEY_G:
if player.is_carrying_tekton:
if player.powerup_manager and player.powerup_manager.can_use_special():
player.throw_tekton()
player.powerup_manager.reset_boost()
else:
if not player.is_carrying_tekton:
player.grab_tekton()
KEY_B:
if player.powerup_manager and player.powerup_manager.can_use_special():
if player.has_method("_find_nearby_tekton") and player._find_nearby_tekton():
player.knock_tekton()
elif player.has_method("enter_knock_mode"):
player.enter_knock_mode()
# Handle spawn point selection if not yet selected
@@ -160,6 +160,12 @@ func try_push(target_pos: Vector2i, direction: Vector2i) -> bool:
if not other_player:
return false
# === INVULNERABILITY CHECK ===
if other_player.get("is_carrying_tekton"):
print("[Move] Push blocked: Target is carrying a Tekton and is invulnerable.")
NotificationManager.send_message(player, "Target is Immune!", NotificationManager.MessageType.WARNING)
return false
# === NEW LOGIC: Only allow push if in ATTACK MODE ===
if not player.get("is_attack_mode"):
# Standard bumping effect or nothing?
+2 -2
View File
@@ -479,9 +479,9 @@ func _refresh_tiles():
for x in range(GRID_SIZE):
for z in range(GRID_SIZE):
# 1. Check if Floor 0 is a wall or empty (non-walkable)
# 1. Check if Floor 0 is a wall or void
var floor_0_item = gridmap.get_cell_item(Vector3i(x, 0, z))
if floor_0_item == 4 or floor_0_item == -1:
if floor_0_item in [4, -1]:
continue
# 1.5. Prevent spawning directly under portal doors
+11 -5
View File
@@ -177,14 +177,20 @@ func consume_boost(amount: float):
rpc("sync_boost", current_boost)
func spawn_boost_reward() -> bool:
"""Alternative Boost Usage: Spawn a PowerUp Tile properly."""
if not can_use_special():
"""Alternative Boost Usage: Spawn a PowerUp Tile properly.
Now requires carrying a Tekton (E shortcut)."""
if not player.is_carrying_tekton:
return false
if player.special_tiles_manager and player.special_tiles_manager.has_method("spawn_powerups_around"):
player.special_tiles_manager.spawn_powerups_around(player.current_position)
reset_boost() # Consumes full bar
print("[PowerUp] %s used Boost to SPAWN ITEM." % player.name)
# Spawn only common tiles (7-10) with 100% density
player.special_tiles_manager.spawn_powerups_around(player.current_position, true, true, true)
# Drop the Tekton after spawning
if player.has_method("drop_tekton"):
player.drop_tekton()
print("[PowerUp] %s used Tekton to SPAWN TILES (100%% Density Common)." % player.name)
return true
return false
+26 -14
View File
@@ -377,20 +377,21 @@ func _execute_block_floor(target_pos: Vector2i = Vector2i.ZERO):
if _is_position_blocked_by_stand(pos): continue
var block_pos = Vector3i(pos.x, 0, pos.y)
var current_item = enhanced_gridmap.get_cell_item(block_pos)
var original_item = enhanced_gridmap.get_cell_item(block_pos)
# PROTECTED FLOOR CHECK: avoid overwriting Start (1), Safe (2), Finish (3), or Wall (4)
var is_immutable = false
if "immutable_items" in enhanced_gridmap:
if current_item in enhanced_gridmap.immutable_items:
if original_item in enhanced_gridmap.immutable_items:
is_immutable = true
if current_item == 4 or is_immutable: continue
if original_item in [1, 2, 3, 4] or is_immutable: continue
batch_data.append({"x": block_pos.x, "y": 0, "z": block_pos.z, "item": 4})
# Record for restoration
blocked_tiles.append({
"position": block_pos,
"original_item": current_item,
"original_item": original_item,
"timer": BLOCK_DURATION
})
@@ -415,7 +416,7 @@ func _execute_invisible_mode(target: Node3D):
# Helper: Spawn Powerups (For Super Push)
# =============================================================================
func spawn_powerups_around(center: Vector2i, force_powerups: bool = true):
func spawn_powerups_around(center: Vector2i, force_powerups: bool = true, only_common: bool = false, full_density: bool = false):
# "spawn / replace your nearby tiles into power up ( special tiles )"
# New PowerUp Tiles are 11, 12, 13, 14
var radius = 2
@@ -428,20 +429,31 @@ func spawn_powerups_around(center: Vector2i, force_powerups: bool = true):
if enhanced_gridmap.is_position_valid(pos):
# Random chance to spawn ANYTHING at this spot (keep density reasonable)
if rng.randf() > 0.5: continue
if not full_density and rng.randf() > 0.5: continue
# PROTECTED FLOOR CHECK: Don't spawn on existing walls or void
var f0 = enhanced_gridmap.get_cell_item(Vector3i(pos.x, 0, pos.y))
var f1 = enhanced_gridmap.get_cell_item(Vector3i(pos.x, 1, pos.y))
if f0 in [4, -1] or f1 == 4 or f1 == 13:
continue
var item_id: int
# 70% Chance for Normal Tile (7-10)
if rng.randf() < 0.7:
var mode = LobbyManager.get_game_mode()
if only_common or LobbyManager.is_game_mode(GameMode.Mode.STOP_N_GO):
# Spawn ONLY goals (7-10) for Stop n Go or if forced
item_id = rng.randi_range(7, 10)
else:
# 30% Chance for PowerUp (Speed 11, Freeze 12, Ghost 14 - Exclude Wall 13 in restricted modes)
var mode = LobbyManager.get_game_mode()
var is_restricted = GameMode.is_restricted(mode)
if is_restricted:
item_id = [11, 14].pick_random()
# Free mode: 60% Chance for Normal Tile (7-10), 40% for PowerUp
if rng.randf() < 0.6:
item_id = rng.randi_range(7, 10)
else:
item_id = rng.randi_range(11, 14)
# 30% Chance for PowerUp (Speed 11, Freeze 12, Ghost 14 - Exclude Wall 13 in restricted modes)
var is_restricted = GameMode.is_restricted(mode)
if is_restricted:
item_id = [11, 14].pick_random()
else:
item_id = rng.randi_range(11, 14)
var cell = Vector3i(pos.x, 1, pos.y)
+9 -2
View File
@@ -319,10 +319,17 @@ func _spawn_mission_tiles():
for z in range(gridmap.rows):
# Ensure we don't spawn on obstacles
var base_tile = gridmap.get_cell_item(Vector3i(x, 0, z))
if base_tile == TILE_OBSTACLE:
var current_item = gridmap.get_cell_item(Vector3i(x, 1, z))
# PROTECTED FLOOR CHECK: Don't spawn on walls or void
if base_tile in [TILE_OBSTACLE, -1] or current_item == TILE_OBSTACLE or current_item == 13:
continue
# Spawn tiles with 60% density
if randf() > 0.6:
gridmap.set_cell_item(Vector3i(x, 1, z), -1)
continue
# Spawn tiles on all floors (100% density)
var tile_type = goal_items[randi() % goal_items.size()]
gridmap.set_cell_item(Vector3i(x, 1, z), tile_type)
+17 -40
View File
@@ -14,8 +14,7 @@ var put_button: Button
var attack_mode_button: Button # Renamed from special_button
var spawn_boost_button: Button
var settings_button: Button
var tekton_knock_button: Button
var tekton_throw_button: Button
var tekton_grab_button: Button
# Settings - persisted to config file
const CONFIG_PATH = "user://touch_controls_settings.cfg"
@@ -137,15 +136,13 @@ func _create_touch_ui():
grab_button = _find_or_create_action_button(actions_container, "Grab", "👋", button_positions.grab)
put_button = _find_or_create_action_button(actions_container, "Put", "📦", button_positions.put)
tekton_knock_button = _find_or_create_action_button(actions_container, "TektonKnock", "👊", Vector2(-280, -160))
tekton_throw_button = _find_or_create_action_button(actions_container, "TektonThrow", "🎯", Vector2(-280, -80))
tekton_grab_button = _find_or_create_action_button(actions_container, "TektonGrab", "👋", Vector2(-280, -80))
# Order: AttackMode, SpawnBoost, Grab
if attack_mode_button: actions_container.move_child(attack_mode_button, 0)
if spawn_boost_button: actions_container.move_child(spawn_boost_button, 1)
if grab_button: actions_container.move_child(grab_button, 2)
if tekton_knock_button: actions_container.move_child(tekton_knock_button, 3)
if tekton_throw_button: actions_container.move_child(tekton_throw_button, 4)
if tekton_grab_button: actions_container.move_child(tekton_grab_button, 3)
# Hide Put Button
if put_button:
@@ -263,8 +260,7 @@ func _ensure_shortcut_label(btn: Button, button_name: String):
"Put": existing_lbl.text = ""
"AttackMode": existing_lbl.text = "Q" if is_sng else ""
"SpawnBoost": existing_lbl.text = "E" if is_sng else ""
"TektonKnock": existing_lbl.text = "B"
"TektonThrow": existing_lbl.text = "G"
"TektonGrab": existing_lbl.text = "G"
# Ensure correct placement (Top Right)
existing_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
@@ -294,8 +290,7 @@ func _ensure_shortcut_label(btn: Button, button_name: String):
"Put": shortcut_lbl.text = "" # Disabled shortcut
"AttackMode": shortcut_lbl.text = "Q"
"SpawnBoost": shortcut_lbl.text = "E"
"TektonKnock": shortcut_lbl.text = "B"
"TektonThrow": shortcut_lbl.text = "G"
"TektonGrab": shortcut_lbl.text = "G"
btn.add_child(shortcut_lbl)
@@ -315,8 +310,7 @@ func _on_button_pressed(button_name: String):
"Put": btn = put_button
"AttackMode": btn = attack_mode_button
"SpawnBoost": btn = spawn_boost_button
"TektonKnock": btn = tekton_knock_button
"TektonThrow": btn = tekton_throw_button
"TektonGrab": btn = tekton_grab_button
if btn:
var tween = create_tween()
@@ -347,27 +341,11 @@ func _on_button_pressed(button_name: String):
else:
print("[TouchControls] PowerUpManager missing on player")
"SpawnBoost":
var powerup_mgr = local_player.get_node_or_null("PowerUpManager")
if powerup_mgr and powerup_mgr.can_use_special():
if local_player.special_tiles_manager and local_player.special_tiles_manager.has_method("spawn_powerups_around"):
local_player.special_tiles_manager.spawn_powerups_around(local_player.current_position)
powerup_mgr.reset_boost()
"TektonKnock":
var powerup_mgr = local_player.get_node_or_null("PowerUpManager")
if powerup_mgr and powerup_mgr.can_use_special():
# Proactive: If nearby, knock immediately. Else enter mode.
if local_player.has_method("_find_nearby_tekton") and local_player._find_nearby_tekton():
local_player.knock_tekton()
elif local_player.has_method("enter_knock_mode"):
local_player.enter_knock_mode()
"TektonThrow":
if local_player.is_carrying_tekton:
var powerup_mgr = local_player.get_node_or_null("PowerUpManager")
if powerup_mgr and powerup_mgr.can_use_special():
if local_player.has_method("throw_tekton"):
local_player.throw_tekton()
powerup_mgr.reset_boost()
else:
if local_player and local_player.is_carrying_tekton:
if local_player.powerup_manager and local_player.powerup_manager.has_method("spawn_boost_reward"):
local_player.powerup_manager.spawn_boost_reward()
"TektonGrab":
if not local_player.is_carrying_tekton:
if local_player.has_method("grab_tekton"):
local_player.grab_tekton()
@@ -378,8 +356,7 @@ func _on_button_released(button_name: String):
"Put": btn = put_button
"AttackMode": btn = attack_mode_button
"SpawnBoost": btn = spawn_boost_button
"TektonKnock": btn = tekton_knock_button
"TektonThrow": btn = tekton_throw_button
"TektonGrab": btn = tekton_grab_button
if btn:
var tween = create_tween()
@@ -548,12 +525,12 @@ func _on_boost_points_changed(current_points: int, max_points: int):
var is_full = current_points >= (max_points - 1) # Tolerance
_update_boost_button_state(attack_mode_button, is_full)
_update_boost_button_state(spawn_boost_button, is_full)
_update_boost_button_state(tekton_knock_button, is_full)
# TektonThrow can be used for "Grab" even without full boost
var can_throw_or_grab = is_full or (local_player and not local_player.is_carrying_tekton)
_update_boost_button_state(tekton_throw_button, can_throw_or_grab)
# SpawnBoost depends on carrying a Tekton, not boost points
var can_spawn = local_player and local_player.is_carrying_tekton
_update_boost_button_state(spawn_boost_button, can_spawn)
_update_boost_button_state(tekton_grab_button, true) # Always enabled (logic handles grab-ability)
func _on_tekton_carried_changed(_is_carrying: bool):
# Refresh button states when player grabs/throws a tekton
+4 -3
View File
@@ -299,7 +299,8 @@ func temporarily_change_floor(center: Vector2i, radius: int, new_id: int, durati
var original = enhanced_gridmap.get_cell_item(cell_3d)
# Only change if not already the new ID (avoid redundant updates or overriding existing freeze)
if original != new_id:
# PROTECTED FLOOR CHECK: avoid overwriting Start (1), Safe (2), Finish (3), or Wall (4)
if original != new_id and not original in [1, 2, 3, 4]:
# PRE-FIX: If we capture a "Hole" (Void or Pickup 7-14) here, we must record it as Floor (0)
# so that we restore a valid floor later.
if original == -1 or (original >= 7 and original <= 14):
@@ -370,10 +371,10 @@ func spawn_tiles_around(count: int = 4):
if _is_position_blocked_by_stand(pos):
continue
if enhanced_gridmap.is_position_valid(pos):
# EXTRA CHECK: Do not spawn tiles on walls (ID 4) or empty void (ID -1) on Floor 0
# Note: We allow spawning on Safe Zones, Start, and Finish as it's on Layer 1.
var floor_0_item = enhanced_gridmap.get_cell_item(Vector3i(pos.x, 0, pos.y))
if floor_0_item == 4 or floor_0_item == -1:
if floor_0_item in [4, -1]:
continue
# 50% chance to spawn something