// personal reference · 3D scene navigation & world mapping
Two separate systems handle all scene navigation in the game. They share the same GameState autoload for condition checking.
GameState autoload. Never hardcode conditions locally inside scenes. One source of truth for everything.
Each row in the connection table represents one bidirectional connection between two doors. This prevents missing return doors.
Each door node in your scene only needs one exported variable — its own door_id. Everything else is looked up from the table at runtime.
# Door.gd — attach to each door node (Area3D)
extends Area3D
@export var door_id: String = ""
func _on_body_entered(body):
if body.is_in_group("player"):
GameState.enter_door(door_id)
Maintain this as a Google Sheet. Use dropdown validation on door_id_x and door_id_y columns to prevent typos. Export as CSV → convert to JSON.
| connection_id | door_id_x | scene_x | door_id_y | scene_y | condition_x_y | condition_y_x |
|---|---|---|---|---|---|---|
| 001 | town_01_door_A | town_01.tscn | dungeon_01_door_B | dungeon_01.tscn | (empty) | (empty) |
| 002 | town_01_door_C | town_01.tscn | dungeon_02_door_A | dungeon_02.tscn | quest_B_done | (empty) |
| 003 | town_02_door_A | town_02.tscn | dungeon_03_door_A | dungeon_03.tscn | key_obtained | key_obtained |
extends Node
var door_database = {}
var spawn_door_id: String = ""
# Load door table from JSON on game start
func _ready():
var file = FileAccess.open("res://data/doors.json", FileAccess.READ)
var data = JSON.parse_string(file.get_as_text())
for row in data["connections"]:
door_database[row["door_id_x"]] = row
door_database[row["door_id_y"]] = row
# Called when player touches a door
func enter_door(player_door_id: String):
if not door_database.has(player_door_id):
print("ERROR: door_id not found → ", player_door_id)
return
var row = door_database[player_door_id]
var is_x_side = (player_door_id == row["door_id_x"])
var condition = row["condition_x_y"] if is_x_side else row["condition_y_x"]
var dest_scene = row["scene_y"] if is_x_side else row["scene_x"]
var dest_door = row["door_id_y"] if is_x_side else row["door_id_x"]
# Check condition
if condition != "" and not get(condition):
show_locked_message()
return
# Store destination and load scene
spawn_door_id = dest_door
get_tree().change_scene_to_file("res://maps/" + dest_scene)
# In each map scene _ready()
func _ready():
var spawn_id = GameState.spawn_door_id
if spawn_id != "":
var door_node = get_node_or_null(spawn_id)
if door_node:
$Player.global_position = door_node.global_position # Vector3 in 3D
Teleport is one direction only. Used by guild NPCs, items, or magic circles. One trigger can show a menu of multiple destinations.
| teleport_id | label | destination_scene | destination_spawn | condition | teleport_type |
|---|---|---|---|---|---|
| guild_to_dungeon_01 | Forest Dungeon | dungeon_01.tscn | dungeon_01_spawn_A | quest_A_done | guild |
| guild_to_dungeon_02 | Ice Cave | dungeon_02.tscn | dungeon_02_spawn_A | quest_B_done | guild |
| item_return_town | Return to Town | town_01.tscn | town_01_center | (empty) | item |
| circle_dungeon_03 | Ancient Ruins | dungeon_03.tscn | dungeon_03_spawn_A | relic_obtained | magic_circle |
var teleport_database = {}
func load_teleports():
var file = FileAccess.open("res://data/teleports.json", FileAccess.READ)
var data = JSON.parse_string(file.get_as_text())
for entry in data["teleports"]:
teleport_database[entry["teleport_id"]] = entry
# Get all available teleports for a specific type
func get_available_teleports(type: String) -> Array:
var result = []
for id in teleport_database:
var entry = teleport_database[id]
if entry["teleport_type"] == type:
var cond = entry["condition"]
if cond == "" or get(cond):
result.append(entry)
return result
# Execute a teleport
func use_teleport(teleport_id: String):
var entry = teleport_database[teleport_id]
spawn_door_id = entry["destination_spawn"]
get_tree().change_scene_to_file("res://maps/" + entry["destination_scene"])
# GuildNPC.gd
func talk_to_player():
var options = GameState.get_available_teleports("guild")
# pass options to your dialog/menu system
$TeleportMenu.show_options(options)
# When player selects a destination
func _on_option_selected(teleport_id: String):
GameState.use_teleport(teleport_id)
Run this script to catch missing or broken connections before they become runtime bugs.
# Run this in _ready() during development only
func validate_doors():
for door_id in door_database:
var row = door_database[door_id]
# Check destination door exists
var dest = row["door_id_y"] if door_id == row["door_id_x"] else row["door_id_x"]
if not door_database.has(dest):
push_error("DOOR ERROR: " + door_id + " → destination not found: " + dest)
# Check condition variable exists in GameState
for cond_key in ["condition_x_y", "condition_y_x"]:
var cond = row[cond_key]
if cond != "" and not (cond in GameState.get_property_list().map(func(p): return p["name"])):
push_warning("CONDITION WARNING: " + cond + " not found in GameState")
print("Door validation complete.")