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
+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