Adding interactable object spawning

This commit is contained in:
2026-03-19 16:35:07 -05:00
parent 932f7be66b
commit 1aefd5ba88
12 changed files with 768 additions and 68 deletions
+203 -5
View File
@@ -1,6 +1,7 @@
extends Node3D
const CHARACTER_API_URL := "https://pchar.ranaze.com/api/Characters"
const LOCATION_API_URL := "https://ploc.ranaze.com/api/Locations"
@export var tile_size := 16.0
@export var block_height := 1.0
@@ -31,6 +32,7 @@ var _coord_sync_in_flight := false
var _queued_coord_sync: Variant = null
var _locations_refresh_in_flight := false
var _queued_locations_refresh := false
var _interact_in_flight := false
func _ready() -> void:
@@ -69,6 +71,11 @@ func _process(_delta: float) -> void:
_queue_locations_refresh()
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("interact"):
_try_interact_current_tile()
func _get_stream_position() -> Vector3:
if _tracked_node:
return _tracked_node.global_position
@@ -102,9 +109,11 @@ func _rebuild_tiles(center: Vector2i) -> void:
if not _known_locations.has(coord):
continue
wanted_keys[coord] = true
var location_data := _known_locations[coord] as Dictionary
if _tile_nodes.has(coord):
_update_tile(coord, location_data)
continue
_spawn_tile(coord, String(_known_locations[coord]))
_spawn_tile(coord, location_data)
for key in _tile_nodes.keys():
if wanted_keys.has(key):
@@ -115,7 +124,7 @@ func _rebuild_tiles(center: Vector2i) -> void:
_tile_nodes.erase(key)
func _spawn_tile(coord: Vector2i, location_name: String) -> void:
func _spawn_tile(coord: Vector2i, location_data: Dictionary) -> void:
var tile_root := Node3D.new()
tile_root.name = "Tile_%d_%d" % [coord.x, coord.y]
tile_root.position = _coord_to_world(coord)
@@ -138,11 +147,60 @@ func _spawn_tile(coord: Vector2i, location_name: String) -> void:
tile.add_child(_create_tile_border())
if show_tile_labels:
tile_root.add_child(_create_tile_label(location_name))
tile_root.add_child(_create_tile_label(String(location_data.get("name", ""))))
_update_tile_object(tile_root, location_data)
_tile_nodes[coord] = tile_root
func _update_tile(coord: Vector2i, location_data: Dictionary) -> void:
var tile_root := _tile_nodes.get(coord) as Node3D
if tile_root == null:
return
if show_tile_labels:
var label := tile_root.get_node_or_null("LocationNameLabel") as Label3D
if label:
label.text = String(location_data.get("name", ""))
_update_tile_object(tile_root, location_data)
func _update_tile_object(tile_root: Node3D, location_data: Dictionary) -> void:
var existing := tile_root.get_node_or_null("LocationObject")
if existing:
existing.queue_free()
var object_data_variant: Variant = location_data.get("locationObject", {})
if typeof(object_data_variant) != TYPE_DICTIONARY:
return
var object_data := object_data_variant as Dictionary
if object_data.is_empty():
return
var object_root := Node3D.new()
object_root.name = "LocationObject"
object_root.position = Vector3(0.0, (block_height * 0.5) + 0.6, 0.0)
tile_root.add_child(object_root)
var object_mesh := MeshInstance3D.new()
object_mesh.name = "ObjectMesh"
object_mesh.mesh = SphereMesh.new()
object_mesh.scale = Vector3(0.6, 0.4, 0.6)
object_mesh.material_override = _create_object_material(String(object_data.get("objectKey", "")))
object_root.add_child(object_mesh)
var object_label := Label3D.new()
object_label.name = "ObjectLabel"
object_label.text = _build_object_label(object_data)
object_label.position = Vector3(0.0, 0.6, 0.0)
object_label.billboard = BaseMaterial3D.BILLBOARD_ENABLED
object_label.pixel_size = 0.01
object_label.outline_size = 10
object_root.add_child(object_label)
func _create_tile_border() -> MeshInstance3D:
var top_y := 0.5 + border_height_bias
var corners := [
@@ -193,10 +251,42 @@ func _create_tile_label(location_name: String) -> Label3D:
return label
func _create_object_material(object_key: String) -> StandardMaterial3D:
var material := StandardMaterial3D.new()
material.roughness = 1.0
material.metallic = 0.0
if object_key.contains("grass"):
material.albedo_color = Color(0.28, 0.68, 0.25, 1.0)
elif object_key.contains("wood"):
material.albedo_color = Color(0.54, 0.36, 0.18, 1.0)
elif object_key.contains("stone"):
material.albedo_color = Color(0.55, 0.57, 0.6, 1.0)
else:
material.albedo_color = Color(0.85, 0.75, 0.3, 1.0)
return material
func _build_object_label(object_data: Dictionary) -> String:
var object_name := String(object_data.get("name", "")).strip_edges()
var state_variant: Variant = object_data.get("state", {})
var remaining_quantity := 0
if typeof(state_variant) == TYPE_DICTIONARY:
remaining_quantity = int((state_variant as Dictionary).get("remainingQuantity", 0))
if object_name.is_empty():
object_name = "Object"
return "%s x%d" % [object_name, remaining_quantity]
func _ensure_selected_location_exists(coord: Vector2i) -> void:
if _known_locations.has(coord):
return
_known_locations[coord] = _selected_location_name(coord)
_known_locations[coord] = {
"id": "",
"name": _selected_location_name(coord),
"locationObject": {}
}
func _selected_location_name(coord: Vector2i) -> String:
@@ -217,6 +307,7 @@ func _load_existing_locations() -> void:
if _character_id.is_empty():
push_warning("Selected character is missing an id; cannot load visible locations.")
_locations_loaded = true
_locations_refresh_in_flight = false
return
var request := HTTPRequest.new()
@@ -231,6 +322,7 @@ func _load_existing_locations() -> void:
push_warning("Failed to request visible locations: %s" % err)
request.queue_free()
_locations_loaded = true
_locations_refresh_in_flight = false
return
var result: Array = await request.request_completed
@@ -242,12 +334,14 @@ func _load_existing_locations() -> void:
if result_code != HTTPRequest.RESULT_SUCCESS or response_code < 200 or response_code >= 300:
push_warning("Failed to load visible locations (%s/%s): %s" % [result_code, response_code, response_body])
_locations_loaded = true
_locations_refresh_in_flight = false
return
var parsed: Variant = JSON.parse_string(response_body)
if typeof(parsed) != TYPE_ARRAY:
push_warning("Visible locations response was not an array.")
_locations_loaded = true
_locations_refresh_in_flight = false
return
var loaded_count := 0
@@ -263,7 +357,15 @@ func _load_existing_locations() -> void:
var location_name := String(location.get("name", "")).strip_edges()
if location_name.is_empty():
location_name = "Location %d,%d" % [coord.x, coord.y]
_known_locations[coord] = location_name
var location_object := {}
var object_variant: Variant = location.get("locationObject", {})
if typeof(object_variant) == TYPE_DICTIONARY:
location_object = (object_variant as Dictionary).duplicate(true)
_known_locations[coord] = {
"id": String(location.get("id", "")).strip_edges(),
"name": location_name,
"locationObject": location_object
}
loaded_count += 1
print("LocationLevel loaded %d visible locations for character %s." % [loaded_count, _character_id])
@@ -295,6 +397,102 @@ func _refresh_visible_locations_async() -> void:
await _load_existing_locations()
func _try_interact_current_tile() -> void:
if _interact_in_flight:
return
if _character_id.is_empty():
return
var location_data: Dictionary = _known_locations.get(_center_coord, {})
if location_data.is_empty():
push_warning("No known location data for %s." % _center_coord)
return
var location_id := String(location_data.get("id", "")).strip_edges()
if location_id.is_empty():
push_warning("Current location is missing an id.")
return
var object_data: Dictionary = location_data.get("locationObject", {})
if object_data.is_empty():
push_warning("Current location has no interactable object.")
return
var object_id := String(object_data.get("id", "")).strip_edges()
if object_id.is_empty():
push_warning("Current location object is missing an id.")
return
_interact_in_flight = true
_interact_with_location_async(location_id, object_id)
func _interact_with_location_async(location_id: String, object_id: String) -> void:
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)
headers.append("Content-Type: application/json")
var body := JSON.stringify({
"characterId": _character_id,
"objectId": object_id
})
var err := request.request("%s/%s/interact" % [LOCATION_API_URL, location_id], headers, HTTPClient.METHOD_POST, body)
if err != OK:
push_warning("Failed to send location interaction request: %s" % err)
request.queue_free()
_interact_in_flight = false
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_body: String = result[3].get_string_from_utf8()
if result_code != HTTPRequest.RESULT_SUCCESS or response_code < 200 or response_code >= 300:
push_warning("Location interaction failed (%s/%s): %s" % [result_code, response_code, response_body])
_interact_in_flight = false
return
var parsed: Variant = JSON.parse_string(response_body)
if typeof(parsed) != TYPE_DICTIONARY:
push_warning("Location interaction response was not an object.")
_interact_in_flight = false
return
var interaction := parsed as Dictionary
_apply_interaction_result(location_id, interaction)
_interact_in_flight = false
func _apply_interaction_result(location_id: String, interaction: Dictionary) -> void:
var consumed := bool(interaction.get("consumed", false))
var remaining_quantity := int(interaction.get("remainingQuantity", 0))
for coord_variant in _known_locations.keys():
var coord: Vector2i = coord_variant
var location_data: Dictionary = _known_locations[coord]
if String(location_data.get("id", "")) != location_id:
continue
var updated_location := location_data.duplicate(true)
if consumed:
updated_location["locationObject"] = {}
else:
var object_data: Dictionary = updated_location.get("locationObject", {})
var state: Dictionary = object_data.get("state", {})
state["remainingQuantity"] = remaining_quantity
object_data["state"] = state
updated_location["locationObject"] = object_data
_known_locations[coord] = updated_location
_update_tile(coord, updated_location)
return
func _queue_coord_sync(coord: Vector2i) -> void:
if coord == _persisted_coord:
return