promiscuity/game/scenes/Levels/location_level.gd

236 lines
6.8 KiB
GDScript

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:
_tiles_root = Node3D.new()
_tiles_root.name = "GeneratedTiles"
add_child(_tiles_root)
if _camera:
_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