From 2850c266571e803a6bdea55067d85e9e44679c28 Mon Sep 17 00:00:00 2001 From: Zeeshaun Date: Fri, 6 Mar 2026 12:04:02 -0600 Subject: [PATCH] Adding location based tiling --- game/scenes/Levels/location_level.gd | 230 ++++++++++++++++++++++++++- game/scenes/UI/login_screen.gd | 33 ++-- 2 files changed, 246 insertions(+), 17 deletions(-) diff --git a/game/scenes/Levels/location_level.gd b/game/scenes/Levels/location_level.gd index fa67264..a6c979e 100644 --- a/game/scenes/Levels/location_level.gd +++ b/game/scenes/Levels/location_level.gd @@ -1,15 +1,235 @@ extends Node3D +const LOCATIONS_API_URL := "https://ploc.ranaze.com/api/Locations" + @export var tile_size := 4.0 @export var block_height := 1.0 +@export_range(1, 8, 1) var tile_radius := 3 +@export var tracked_node_path: NodePath +@export var border_color: Color = Color(0.05, 0.05, 0.05, 1.0) +@export var border_height_bias := 0.005 +@export var show_tile_labels := true +@export var tile_label_height := 0.01 +@export var tile_label_color: Color = Color(1, 1, 1, 1) @onready var _block: MeshInstance3D = $TerrainBlock @onready var _camera: Camera3D = $Camera3D +var _center_coord := Vector2i.ZERO +var _tiles_root: Node3D +var _tracked_node: Node3D +var _tile_nodes: Dictionary = {} +var _camera_start_offset := Vector3(0.0, 6.0, 10.0) +var _border_material: StandardMaterial3D +var _known_location_coords: Dictionary = {} +var _locations_loaded := false + func _ready() -> void: - var coord := SelectedCharacter.get_coord() - var block_pos := Vector3(coord.x * tile_size, block_height * 0.5, coord.y * tile_size) - _block.position = block_pos - _block.scale = Vector3(tile_size, block_height, tile_size) + _tiles_root = Node3D.new() + _tiles_root.name = "GeneratedTiles" + add_child(_tiles_root) + if _camera: - _camera.look_at(block_pos, Vector3.UP) + _camera_start_offset = _camera.global_position + + _tracked_node = get_node_or_null(tracked_node_path) as Node3D + if _tracked_node == null: + _tracked_node = get_node_or_null("Player") as Node3D + + var start_coord := SelectedCharacter.get_coord() + _center_coord = Vector2i(roundi(start_coord.x), roundi(start_coord.y)) + + _block.visible = false + await _load_existing_locations() + _rebuild_tiles(_center_coord) + _snap_camera_to_coord(_center_coord) + + +func _process(_delta: float) -> void: + if not _locations_loaded: + return + var target_world_pos := _get_stream_position() + var target_coord := _world_to_coord(target_world_pos) + if target_coord == _center_coord: + return + _center_coord = target_coord + _rebuild_tiles(_center_coord) + + +func _get_stream_position() -> Vector3: + if _tracked_node: + return _tracked_node.global_position + if _camera: + return _camera.global_position + return _coord_to_world(_center_coord) + + +func _world_to_coord(world_pos: Vector3) -> Vector2i: + return Vector2i( + roundi(world_pos.x / tile_size), + roundi(world_pos.z / tile_size) + ) + + +func _coord_to_world(coord: Vector2i) -> Vector3: + return Vector3(coord.x * tile_size, block_height * 0.5, coord.y * tile_size) + + +func _snap_camera_to_coord(coord: Vector2i) -> void: + if _camera == null: + return + var center_world := _coord_to_world(coord) + _camera.global_position = center_world + _camera_start_offset + _camera.look_at(center_world, Vector3.UP) + + +func _rebuild_tiles(center: Vector2i) -> void: + var wanted_keys: Dictionary = {} + for x in range(center.x - tile_radius, center.x + tile_radius + 1): + for y in range(center.y - tile_radius, center.y + tile_radius + 1): + var coord := Vector2i(x, y) + if not _known_location_coords.has(coord): + continue + wanted_keys[coord] = true + if _tile_nodes.has(coord): + continue + _spawn_tile(coord) + + var keys_to_remove: Array = _tile_nodes.keys() + for key in keys_to_remove: + if wanted_keys.has(key): + continue + var tile_node := _tile_nodes[key] as Node3D + if tile_node: + tile_node.queue_free() + _tile_nodes.erase(key) + + +func _spawn_tile(coord: Vector2i) -> void: + var tile_root := Node3D.new() + tile_root.name = "Tile_%d_%d" % [coord.x, coord.y] + tile_root.position = _coord_to_world(coord) + _tiles_root.add_child(tile_root) + + var tile := _block.duplicate() as MeshInstance3D + tile.name = "TileMesh" + tile.visible = true + tile.scale = Vector3(tile_size, block_height, tile_size) + tile_root.add_child(tile) + + tile.add_child(_create_tile_border()) + if show_tile_labels: + tile_root.add_child(_create_tile_label(coord)) + + var anchor := Marker3D.new() + anchor.name = "NpcAnchor" + anchor.position = Vector3(0.0, (block_height * 0.5) + 0.5, 0.0) + tile_root.add_child(anchor) + + _tile_nodes[coord] = tile_root + + +func _create_tile_border() -> MeshInstance3D: + var top_y := 0.5 + border_height_bias + var corners := [ + Vector3(-0.5, top_y, -0.5), + Vector3(0.5, top_y, -0.5), + Vector3(0.5, top_y, 0.5), + Vector3(-0.5, top_y, 0.5), + ] + + var border_mesh := ImmediateMesh.new() + border_mesh.surface_begin(Mesh.PRIMITIVE_LINES, _get_border_material()) + for idx in range(corners.size()): + var current: Vector3 = corners[idx] + var next: Vector3 = corners[(idx + 1) % corners.size()] + border_mesh.surface_add_vertex(current) + border_mesh.surface_add_vertex(next) + border_mesh.surface_end() + + var border := MeshInstance3D.new() + border.name = "TileBorder" + border.mesh = border_mesh + return border + + +func _get_border_material() -> StandardMaterial3D: + if _border_material: + return _border_material + + var material := StandardMaterial3D.new() + material.albedo_color = border_color + material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED + material.disable_receive_shadows = true + material.no_depth_test = true + _border_material = material + return _border_material + + +func _create_tile_label(coord: Vector2i) -> Label3D: + var label := Label3D.new() + label.name = "LocationIdLabel" + label.text = _location_id_for_coord(coord) + label.position = Vector3(0.0, (block_height * 0.5) + border_height_bias + tile_label_height, 0.0) + label.rotation_degrees = Vector3(-90.0, 0.0, 0.0) + label.billboard = BaseMaterial3D.BILLBOARD_DISABLED + label.modulate = tile_label_color + label.pixel_size = 0.01 + label.outline_size = 12 + label.no_depth_test = false + return label + + +func _location_id_for_coord(coord: Vector2i) -> String: + return "%d,%d" % [coord.x, coord.y] + + +func _load_existing_locations() -> void: + _locations_loaded = false + _known_location_coords.clear() + + var request := HTTPRequest.new() + add_child(request) + + var headers := PackedStringArray() + if not AuthState.access_token.is_empty(): + headers.append("Authorization: Bearer %s" % AuthState.access_token) + + var err := request.request(LOCATIONS_API_URL, headers, HTTPClient.METHOD_GET) + if err != OK: + push_warning("Failed to request locations: %s" % err) + request.queue_free() + _locations_loaded = true + return + + var result: Array = await request.request_completed + request.queue_free() + + var result_code: int = result[0] + var response_code: int = result[1] + var response_bytes: PackedByteArray = result[3] + var response_body: String = response_bytes.get_string_from_utf8() + if result_code != HTTPRequest.RESULT_SUCCESS or response_code < 200 or response_code >= 300: + push_warning("Failed to load locations (%s/%s): %s" % [result_code, response_code, response_body]) + _locations_loaded = true + return + + var parsed: Variant = JSON.parse_string(response_body) + if typeof(parsed) != TYPE_ARRAY: + push_warning("Locations response was not an array.") + _locations_loaded = true + return + + for item in parsed: + if typeof(item) != TYPE_DICTIONARY: + continue + var location: Dictionary = item + var coord_variant: Variant = location.get("coord", {}) + if typeof(coord_variant) != TYPE_DICTIONARY: + continue + var coord_dict: Dictionary = coord_variant + var x := int(coord_dict.get("x", 0)) + var y := int(coord_dict.get("y", 0)) + _known_location_coords[Vector2i(x, y)] = true + + _locations_loaded = true diff --git a/game/scenes/UI/login_screen.gd b/game/scenes/UI/login_screen.gd index 0d5f699..4008e41 100644 --- a/game/scenes/UI/login_screen.gd +++ b/game/scenes/UI/login_screen.gd @@ -2,16 +2,22 @@ extends Control const AUTH_LOGIN_URL := "https://pauth.ranaze.com/api/Auth/login" -@onready var _username_input: LineEdit = %UsernameInput -@onready var _password_input: LineEdit = %PasswordInput -@onready var _login_request: HTTPRequest = %LoginRequest -@onready var _error_label: Label = %ErrorLabel - -func _on_log_in_button_pressed() -> void: - var username := _username_input.text.strip_edges() - var password := _password_input.text - if username.is_empty() or password.is_empty(): - _show_error("Username and password required.") +@onready var _username_input: LineEdit = %UsernameInput +@onready var _password_input: LineEdit = %PasswordInput +@onready var _login_request: HTTPRequest = %LoginRequest +@onready var _error_label: Label = %ErrorLabel + +func _ready() -> void: + if not _username_input.is_connected("text_submitted", Callable(self, "_on_input_text_submitted")): + _username_input.text_submitted.connect(_on_input_text_submitted) + if not _password_input.is_connected("text_submitted", Callable(self, "_on_input_text_submitted")): + _password_input.text_submitted.connect(_on_input_text_submitted) + +func _on_log_in_button_pressed() -> void: + var username := _username_input.text.strip_edges() + var password := _password_input.text + if username.is_empty() or password.is_empty(): + _show_error("Username and password required.") return var payload := { @@ -44,5 +50,8 @@ func _on_login_request_completed(result: int, response_code: int, _headers: Pack func _on_back_button_pressed() -> void: get_tree().change_scene_to_file("res://scenes/UI/start_screen.tscn") -func _show_error(message: String) -> void: - _error_label.text = message +func _show_error(message: String) -> void: + _error_label.text = message + +func _on_input_text_submitted(_new_text: String) -> void: + _on_log_in_button_pressed()