feat: Implement Tekton Doors game mode with portal mechanics, add an in-game message bar, and introduce a pre-game countdown.

This commit is contained in:
Yogi Wiguna
2026-02-25 16:58:59 +08:00
parent a157c1efc4
commit 6d41f9dbc6
9 changed files with 513 additions and 8 deletions
+1 -1
View File
@@ -41,7 +41,7 @@ signal scarcity_mode_changed(mode: String)
# Character and area selection
var available_characters: Array[String] = ["Copper", "Dabro", "Gatot", "Pip", "Random"]
var available_areas: Array[String] = ["Desert", "Forest", "City", "Factory"]
var available_game_modes: Array[String] = ["Freemode", "Stop n Go"]
var available_game_modes: Array[String] = ["Freemode", "Stop n Go", "Tekton Doors"]
var selected_area: String = "Desert" # Host-controlled
var game_mode: String = "Freemode" # Host-controlled
var local_character_index: int = 0 # Local player's character index
+259
View File
@@ -0,0 +1,259 @@
extends Node
# PortalModeManager - Handles "Tekton Doors" mode logic
# Manages room partitioning, portal connections, and mode-specific timers.
var main: Node
var gridmap: Node
# Room layout config
const ROOM_COUNT = 4
const GRID_SIZE = 14
const ROOM_DIM = 7
# State
var connections = {} # room_id -> {door_id -> {target_room, target_door}}
var doors = [] # List of PortalDoor nodes
var swap_timer: Timer
var tile_refresh_timer: Timer
var finish_spawned: bool = false
var missions_required: int = 3
func initialize(p_main: Node, p_gridmap: Node):
main = p_main
gridmap = p_gridmap
print("[PortalModeManager] Initialized")
# Connection Swap Timer (15s)
swap_timer = Timer.new()
swap_timer.name = "PortalSwapTimer"
swap_timer.wait_time = 15.0
swap_timer.timeout.connect(_on_swap_timer_timeout)
add_child(swap_timer)
# Tile Refresh Timer (25s)
tile_refresh_timer = Timer.new()
tile_refresh_timer.name = "TileRefreshTimer"
tile_refresh_timer.wait_time = 25.0
tile_refresh_timer.timeout.connect(_on_tile_refresh_timer_timeout)
add_child(tile_refresh_timer)
# Connect to mission tracking
var gcm = main.get_node_or_null("GoalsCycleManager")
if gcm:
gcm.goal_count_updated.connect(_on_goal_count_updated)
func start_game_mode():
if not multiplayer.is_server(): return
print("[PortalModeManager] Starting Portal Game Mode...")
# 1. Setup Arena Size
_setup_arena_size()
# 2. Setup Room Partitions (visual/physical walls between rooms)
_setup_room_partitions()
# 3. Spawn Portal Doors
_spawn_portal_doors()
# 4. Initialize Connections
_randomize_connections()
# 5. Start Timers
swap_timer.start()
tile_refresh_timer.start()
# 6. Initial Tile Spawn
_refresh_tiles()
func _setup_arena_size():
if not gridmap: return
gridmap.columns = GRID_SIZE
gridmap.rows = GRID_SIZE
gridmap.clear()
# Fill floor
for x in range(GRID_SIZE):
for z in range(GRID_SIZE):
gridmap.set_cell_item(Vector3i(x, 0, z), 0) # Normal floor
func get_spawn_points() -> Array[Vector2i]:
# One point per quadrant
return [
Vector2i(3, 3), # Room 0
Vector2i(10, 3), # Room 1
Vector2i(3, 10), # Room 2
Vector2i(10, 10) # Room 3
]
func _setup_room_partitions():
for i in range(GRID_SIZE):
# Vertical wall (middle columns)
gridmap.set_cell_item(Vector3i(6, 0, i), 4) # Wall item
gridmap.set_cell_item(Vector3i(7, 0, i), 4)
# Horizontal wall (middle rows)
gridmap.set_cell_item(Vector3i(i, 0, 6), 4)
gridmap.set_cell_item(Vector3i(i, 0, 7), 4)
func _spawn_portal_doors():
var portal_scene = load("res://scenes/portal_door.tscn")
var stands_container = main.get_node_or_null("Stands")
if not stands_container: return
var door_configs = [
# Room 0
{"room": 0, "pos": Vector2i(6, 2), "rot": PI/2, "offset": Vector2i(-1, 0)}, # East
{"room": 0, "pos": Vector2i(2, 6), "rot": 0, "offset": Vector2i(0, -1)}, # South
# Room 1
{"room": 1, "pos": Vector2i(7, 2), "rot": PI/2, "offset": Vector2i(1, 0)}, # West
{"room": 1, "pos": Vector2i(11, 6), "rot": 0, "offset": Vector2i(0, -1)}, # South
# Room 2
{"room": 2, "pos": Vector2i(2, 7), "rot": 0, "offset": Vector2i(0, 1)}, # North
{"room": 2, "pos": Vector2i(6, 11), "rot": PI/2, "offset": Vector2i(-1, 0)},# East
# Room 3
{"room": 3, "pos": Vector2i(11, 7), "rot": 0, "offset": Vector2i(0, 1)}, # North
{"room": 3, "pos": Vector2i(7, 11), "rot": PI/2, "offset": Vector2i(1, 0)} # West
]
for i in range(door_configs.size()):
var cfg = door_configs[i]
var door = portal_scene.instantiate()
door.name = "Portal_%d" % i
door.room_id = cfg["room"]
door.door_id = i
door.set_meta("spawn_offset", cfg["offset"]) # Store offset for teleport
# Position
var world_pos = gridmap.map_to_local(Vector3i(cfg["pos"].x, 0, cfg["pos"].y))
door.transform.origin = world_pos
door.rotation.y = cfg["rot"]
stands_container.add_child(door, true)
doors.append(door)
door.player_entered_portal.connect(handle_portal_interaction)
gridmap.set_cell_item(Vector3i(cfg["pos"].x, 0, cfg["pos"].y), 0) # Normal floor
const PORTAL_COLORS = [
Color(0, 1, 1), # Cyan
Color(1, 0, 1), # Magenta
Color(1, 1, 0), # Yellow
Color(0, 1, 0) # Green
]
func _randomize_connections():
if not multiplayer.is_server(): return
print("[PortalModeManager] Swapping portal connections...")
connections.clear()
var door_indices = []
for i in range(doors.size()):
door_indices.append(i)
# Shuffle and Validate: ensure no pairs are in the same room
var valid_pairing = false
var attempts = 0
while not valid_pairing and attempts < 100:
attempts += 1
door_indices.shuffle()
valid_pairing = true
for i in range(0, door_indices.size(), 2):
var a = door_indices[i]
var b = door_indices[i+1]
if doors[a].room_id == doors[b].room_id:
valid_pairing = false
break
# Pair them up and assign colors
for i in range(0, door_indices.size(), 2):
var a = door_indices[i]
var b = door_indices[i+1]
connections[a] = b
connections[b] = a
var color = PORTAL_COLORS[i/2 % PORTAL_COLORS.size()]
doors[a].target_door_id = b
doors[a].portal_color = color
doors[b].target_door_id = a
doors[b].portal_color = color
main.rpc("display_message", "PORTALS SWITCHED!")
func _on_goal_count_updated(peer_id: int, count: int):
if not multiplayer.is_server(): return
if count >= missions_required and not finish_spawned:
_spawn_finish_room()
func _spawn_finish_room():
print("[PortalModeManager] Missions complete! Spawning Finish Room...")
finish_spawned = true
# Choose a random center room tile (X=3, Z=10 or similar in any room)
var room_centers = get_spawn_points()
var center = room_centers[randi() % room_centers.size()]
# Place finish tile (ID 3)
gridmap.set_cell_item(Vector3i(center.x, 0, center.y), 3)
main.get_node("EnhancedGridMap").update_grid_data()
main.rpc("display_message", "FINISH ROOM REVEALED!")
func _on_swap_timer_timeout():
_randomize_connections()
func _on_tile_refresh_timer_timeout():
_refresh_tiles()
main.rpc("display_message", "TILES REPLENISHED!")
func _refresh_tiles():
# Simple tile fill for each quadrant
for x in range(GRID_SIZE):
for z in range(GRID_SIZE):
# Skip walls
if gridmap.get_cell_item(Vector3i(x, 0, z)) == 4: continue
# Low chance to spawn a tile if empty
if randf() < 0.1:
var weights = ScarcityModel.get_tile_weights()
var tile_id = _pick_weighted_tile(weights)
# 1. Update GridMap
gridmap.set_cell_item(Vector3i(x, 0, z), tile_id)
func _pick_weighted_tile(weights: Dictionary) -> int:
var total_weight = 0
for w in weights.values(): total_weight += w
var r = randi() % total_weight
var cumulative = 0
for tile in weights:
cumulative += weights[tile]
if r < cumulative:
return tile
return 7 # Default Heart
func handle_portal_interaction(player, door):
if not multiplayer.is_server(): return
var source_id = door.door_id
if not connections.has(source_id): return
var target_id = connections[source_id]
var target_door = doors[target_id]
# Use stored offset to avoid infinite loop (spawn inside the target room)
var offset = target_door.get_meta("spawn_offset") if target_door.has_meta("spawn_offset") else Vector2i(0,0)
# Convert world pos back to grid
var target_world = target_door.global_position
var target_grid_3d = gridmap.local_to_map(target_world)
var target_grid = Vector2i(target_grid_3d.x, target_grid_3d.z) + offset
print("[Portal] Teleporting %s to Room %d, Pos %s (via Door %d)" % [player.name, target_door.room_id, target_grid, target_id])
# Snap player
if player.has_method("set_spawn_position"):
player.rpc("set_spawn_position", target_grid)
@@ -0,0 +1 @@
uid://cf0a3uwnf23j7
+88
View File
@@ -0,0 +1,88 @@
extends StaticBody3D
# PortalDoor.gd
# Specialized door for "Tekton Doors" mode.
# Teleports players to a target room/door when they step into it.
signal player_entered_portal(player_node, door_node)
@export var room_id: int = 0
@export var door_id: int = 0 # 0: North, 1: South, 2: East, 3: West
# State synced by PortalModeManager
var target_room_id: int = -1
var target_door_id: int = -1
var is_active: bool = true
var portal_color: Color = Color.WHITE: set = set_portal_color
func set_portal_color(value: Color):
portal_color = value
_update_visuals()
@onready var detection_area: Area3D = $Area3D
func _ready():
add_to_group("PortalDoors")
if detection_area:
detection_area.body_entered.connect(_on_body_entered)
# Visual feedback: indicate door is active
_update_visuals()
func _on_body_entered(body: Node3D):
if not is_active: return
if body.is_in_group("Players") or body.get("is_bot"):
print("[PortalDoor] Player %s entered Door %d in Room %d" % [body.name, door_id, room_id])
emit_signal("player_entered_portal", body, self)
var _materials_initialized: bool = false
func _update_visuals():
if not is_node_ready() or not is_inside_tree(): return
if not _materials_initialized:
_initialize_unique_materials()
_materials_initialized = true
var vortex = get_node_or_null("Vortex")
if vortex:
var mat = vortex.get_surface_override_material(0)
if mat:
mat.albedo_color = portal_color
mat.albedo_color.a = 0.5
if mat.has_method("set_emission"):
mat.emission = portal_color
for part_name in ["Frame_Left", "Frame_Right", "Frame_Top"]:
var frame = get_node_or_null(part_name)
if frame:
var mat = frame.get_surface_override_material(0)
if mat:
mat.albedo_color = portal_color.lerp(Color.BLACK, 0.4)
func _initialize_unique_materials():
var vortex = get_node_or_null("Vortex")
if vortex:
var mat = vortex.get_surface_override_material(0)
if not mat:
mat = vortex.mesh.surface_get_material(0)
if mat:
vortex.set_surface_override_material(0, mat.duplicate())
for part_name in ["Frame_Left", "Frame_Right", "Frame_Top"]:
var frame = get_node_or_null(part_name)
if frame:
var mat = frame.get_surface_override_material(0)
if not mat:
mat = frame.mesh.surface_get_material(0)
if mat:
frame.set_surface_override_material(0, mat.duplicate())
func get_teleport_target_position() -> Vector2i:
# This function will be called by the manager to determine WHERE the player spawns
# usually just outside the target door's position.
# For now, let's just return a placeholder that the manager will override.
return Vector2i.ZERO
+1
View File
@@ -0,0 +1 @@
uid://bitnhtgysi3b7