All checks were successful
Deploy Promiscuity Auth API / deploy (push) Successful in 48s
Deploy Promiscuity Character API / deploy (push) Successful in 47s
Deploy Promiscuity Inventory API / deploy (push) Successful in 59s
Deploy Promiscuity Locations API / deploy (push) Successful in 46s
k8s smoke test / test (push) Successful in 10s
1184 lines
39 KiB
GDScript
1184 lines
39 KiB
GDScript
extends Node3D
|
|
|
|
const CHARACTER_API_URL := "https://pchar.ranaze.com/api/Characters"
|
|
const LOCATION_API_URL := "https://ploc.ranaze.com/api/Locations"
|
|
const INVENTORY_API_URL := "https://pinv.ranaze.com/api/inventory"
|
|
const START_SCREEN_SCENE := "res://scenes/UI/start_screen.tscn"
|
|
const SETTINGS_SCENE := "res://scenes/UI/Settings.tscn"
|
|
const CHARACTER_SLOT_COUNT := 6
|
|
|
|
@export var tile_size := 8.0
|
|
@export var block_height := 1.0
|
|
@export_range(1, 8, 1) var tile_radius := 3
|
|
@export var tracked_node_path: NodePath
|
|
@export var player_spawn_height := 2.0
|
|
@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 _player: RigidBody3D = $Player
|
|
@onready var _camera: Camera3D = $Player/Camera3D
|
|
@onready var _player_visual: Node3D = $Player/TestCharAnimated
|
|
@onready var _pause_menu: CanvasLayer = $PauseMenu
|
|
@onready var _inventory_menu: CanvasLayer = $InventoryMenu
|
|
@onready var _inventory_location_label: Label = $InventoryMenu/MarginContainer/Panel/VBoxContainer/CurrentLocationLabel
|
|
@onready var _character_items_list: ItemList = $InventoryMenu/MarginContainer/Panel/VBoxContainer/Columns/CharacterPanel/VBoxContainer/CharacterItems
|
|
@onready var _ground_items_list: ItemList = $InventoryMenu/MarginContainer/Panel/VBoxContainer/Columns/GroundPanel/VBoxContainer/GroundItems
|
|
@onready var _target_slot_spin_box: SpinBox = $InventoryMenu/MarginContainer/Panel/VBoxContainer/ControlsPanel/VBoxContainer/ControlsRow/TargetSlotSpinBox
|
|
@onready var _quantity_spin_box: SpinBox = $InventoryMenu/MarginContainer/Panel/VBoxContainer/ControlsPanel/VBoxContainer/ControlsRow/QuantitySpinBox
|
|
@onready var _inventory_status_label: Label = $InventoryMenu/MarginContainer/Panel/VBoxContainer/ControlsPanel/VBoxContainer/StatusLabel
|
|
|
|
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 _biome_materials: Dictionary = {}
|
|
var _known_locations: Dictionary = {}
|
|
var _locations_loaded := false
|
|
var _character_id := ""
|
|
var _persisted_coord := Vector2i.ZERO
|
|
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
|
|
var _inventory_request_in_flight := false
|
|
var _character_inventory_items: Array = []
|
|
var _selected_character_item_id := ""
|
|
var _selected_ground_item_id := ""
|
|
|
|
|
|
func _ready() -> void:
|
|
_tiles_root = Node3D.new()
|
|
_tiles_root.name = "GeneratedTiles"
|
|
add_child(_tiles_root)
|
|
|
|
if _camera:
|
|
_camera_start_offset = _camera.position
|
|
|
|
_tracked_node = get_node_or_null(tracked_node_path) as Node3D
|
|
if _tracked_node == null:
|
|
_tracked_node = _player
|
|
|
|
var start_coord := SelectedCharacter.get_coord()
|
|
_center_coord = Vector2i(roundi(start_coord.x), roundi(start_coord.y))
|
|
_persisted_coord = _center_coord
|
|
_character_id = String(SelectedCharacter.character.get("id", SelectedCharacter.character.get("Id", ""))).strip_edges()
|
|
|
|
_block.visible = false
|
|
_deactivate_player_for_load()
|
|
await _load_existing_locations()
|
|
_ensure_selected_location_exists(_center_coord)
|
|
_rebuild_tiles(_center_coord)
|
|
_move_player_to_coord(_center_coord)
|
|
_activate_player_after_load()
|
|
|
|
|
|
func _process(_delta: float) -> void:
|
|
if not _locations_loaded:
|
|
return
|
|
if _inventory_menu.visible:
|
|
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
|
|
_queue_coord_sync(_center_coord)
|
|
_queue_locations_refresh()
|
|
|
|
|
|
func _input(event: InputEvent) -> void:
|
|
if event.is_action_pressed("player_phone"):
|
|
if get_tree().paused:
|
|
return
|
|
_toggle_inventory_menu()
|
|
get_viewport().set_input_as_handled()
|
|
|
|
|
|
func _unhandled_input(event: InputEvent) -> void:
|
|
if event.is_action_pressed("ui_cancel") and _inventory_menu.visible:
|
|
_close_inventory_menu()
|
|
get_viewport().set_input_as_handled()
|
|
return
|
|
if event.is_action_pressed("ui_cancel"):
|
|
_toggle_pause_menu()
|
|
get_viewport().set_input_as_handled()
|
|
return
|
|
if event.is_action_pressed("interact"):
|
|
if get_tree().paused or _inventory_menu.visible:
|
|
return
|
|
_try_interact_current_tile()
|
|
|
|
|
|
func _get_stream_position() -> Vector3:
|
|
if _tracked_node:
|
|
return _tracked_node.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 _move_player_to_coord(coord: Vector2i) -> void:
|
|
if _player == null:
|
|
return
|
|
_player.global_position = Vector3(coord.x * tile_size, player_spawn_height, coord.y * tile_size)
|
|
_player.linear_velocity = Vector3.ZERO
|
|
_player.angular_velocity = Vector3.ZERO
|
|
|
|
|
|
func _deactivate_player_for_load() -> void:
|
|
if _player == null:
|
|
return
|
|
_player.freeze = true
|
|
_player.sleeping = true
|
|
_player.linear_velocity = Vector3.ZERO
|
|
_player.angular_velocity = Vector3.ZERO
|
|
if _player_visual:
|
|
_player_visual.visible = false
|
|
|
|
|
|
func _activate_player_after_load() -> void:
|
|
if _player == null:
|
|
return
|
|
_player.linear_velocity = Vector3.ZERO
|
|
_player.angular_velocity = Vector3.ZERO
|
|
_player.sleeping = false
|
|
_player.freeze = false
|
|
if _player_visual:
|
|
_player_visual.visible = true
|
|
|
|
|
|
func _toggle_pause_menu() -> void:
|
|
if _pause_menu == null:
|
|
return
|
|
if get_tree().paused:
|
|
_resume_game()
|
|
else:
|
|
_pause_game()
|
|
|
|
|
|
func _pause_game() -> void:
|
|
if _inventory_menu.visible:
|
|
_close_inventory_menu()
|
|
get_tree().paused = true
|
|
_pause_menu.visible = true
|
|
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
|
|
|
|
|
|
func _resume_game() -> void:
|
|
get_tree().paused = false
|
|
_pause_menu.visible = false
|
|
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED if not _inventory_menu.visible else Input.MOUSE_MODE_VISIBLE)
|
|
|
|
|
|
func _toggle_inventory_menu() -> void:
|
|
if _inventory_menu.visible:
|
|
_close_inventory_menu()
|
|
return
|
|
_open_inventory_menu()
|
|
|
|
|
|
func _open_inventory_menu() -> void:
|
|
if _character_id.is_empty():
|
|
return
|
|
_inventory_menu.visible = true
|
|
_inventory_status_label.text = "Loading inventory..."
|
|
_set_player_menu_lock(true)
|
|
_update_inventory_location_label()
|
|
_refresh_inventory_menu_data()
|
|
|
|
|
|
func _close_inventory_menu() -> void:
|
|
_inventory_menu.visible = false
|
|
_inventory_status_label.text = ""
|
|
_selected_character_item_id = ""
|
|
_selected_ground_item_id = ""
|
|
_character_items_list.deselect_all()
|
|
_ground_items_list.deselect_all()
|
|
_set_player_menu_lock(false)
|
|
|
|
|
|
func _set_player_menu_lock(locked: bool) -> void:
|
|
if _player == null:
|
|
return
|
|
if locked:
|
|
_player.freeze = true
|
|
_player.sleeping = true
|
|
_player.linear_velocity = Vector3.ZERO
|
|
_player.angular_velocity = Vector3.ZERO
|
|
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
|
|
return
|
|
_player.sleeping = false
|
|
_player.freeze = false
|
|
if not get_tree().paused:
|
|
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
|
|
|
|
|
|
func _on_pause_continue_pressed() -> void:
|
|
_resume_game()
|
|
|
|
|
|
func _on_pause_settings_pressed() -> void:
|
|
_resume_game()
|
|
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
|
|
get_tree().change_scene_to_file(SETTINGS_SCENE)
|
|
|
|
|
|
func _on_pause_main_menu_pressed() -> void:
|
|
_resume_game()
|
|
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
|
|
get_tree().change_scene_to_file(START_SCREEN_SCENE)
|
|
|
|
|
|
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_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, location_data)
|
|
|
|
for key in _tile_nodes.keys():
|
|
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, 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)
|
|
_tiles_root.add_child(tile_root)
|
|
|
|
var tile_body := StaticBody3D.new()
|
|
tile_body.name = "TileBody"
|
|
tile_body.scale = Vector3(tile_size, block_height, tile_size)
|
|
tile_root.add_child(tile_body)
|
|
|
|
var collision_shape := CollisionShape3D.new()
|
|
collision_shape.name = "CollisionShape3D"
|
|
collision_shape.shape = BoxShape3D.new()
|
|
tile_body.add_child(collision_shape)
|
|
|
|
var tile := _block.duplicate() as MeshInstance3D
|
|
tile.name = "TileMesh"
|
|
tile.visible = true
|
|
var biome_key := String(location_data.get("biomeKey", "plains")).strip_edges()
|
|
var biome_material := _get_biome_material(tile, biome_key)
|
|
if biome_material:
|
|
tile.material_override = biome_material
|
|
tile_body.add_child(tile)
|
|
|
|
tile.add_child(_create_tile_border())
|
|
if show_tile_labels:
|
|
tile_root.add_child(_create_tile_label(String(location_data.get("name", ""))))
|
|
_update_tile_object(tile_root, location_data)
|
|
_update_tile_inventory(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)
|
|
_update_tile_inventory(tile_root, location_data)
|
|
|
|
|
|
func _update_tile_object(tile_root: Node3D, location_data: Dictionary) -> void:
|
|
var object_data_variant: Variant = location_data.get("locationObject", {})
|
|
if typeof(object_data_variant) != TYPE_DICTIONARY:
|
|
var existing_missing := tile_root.get_node_or_null("LocationObject")
|
|
if existing_missing:
|
|
existing_missing.queue_free()
|
|
return
|
|
|
|
var object_data := object_data_variant as Dictionary
|
|
if object_data.is_empty():
|
|
var existing_empty := tile_root.get_node_or_null("LocationObject")
|
|
if existing_empty:
|
|
existing_empty.queue_free()
|
|
return
|
|
|
|
var object_root := tile_root.get_node_or_null("LocationObject") as Node3D
|
|
if object_root == null:
|
|
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 := object_root.get_node_or_null("ObjectMesh") as MeshInstance3D
|
|
if object_mesh == null:
|
|
object_mesh = MeshInstance3D.new()
|
|
object_mesh.name = "ObjectMesh"
|
|
object_mesh.mesh = SphereMesh.new()
|
|
object_mesh.scale = Vector3(0.6, 0.4, 0.6)
|
|
object_root.add_child(object_mesh)
|
|
object_mesh.material_override = _create_object_material(String(object_data.get("objectKey", "")))
|
|
|
|
var object_label := object_root.get_node_or_null("ObjectLabel") as Label3D
|
|
if object_label == null:
|
|
object_label = Label3D.new()
|
|
object_label.name = "ObjectLabel"
|
|
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)
|
|
object_label.text = _build_object_label(object_data)
|
|
|
|
|
|
func _update_tile_inventory(tile_root: Node3D, location_data: Dictionary) -> void:
|
|
var floor_items: Array = location_data.get("floorItems", [])
|
|
var existing_label := tile_root.get_node_or_null("FloorInventoryLabel") as Label3D
|
|
if floor_items.is_empty():
|
|
if existing_label:
|
|
existing_label.queue_free()
|
|
return
|
|
|
|
if existing_label == null:
|
|
existing_label = Label3D.new()
|
|
existing_label.name = "FloorInventoryLabel"
|
|
existing_label.position = Vector3(0.0, (block_height * 0.5) + 1.7, -0.9)
|
|
existing_label.billboard = BaseMaterial3D.BILLBOARD_ENABLED
|
|
existing_label.pixel_size = 0.01
|
|
existing_label.outline_size = 10
|
|
existing_label.modulate = Color(1.0, 0.95, 0.75, 1.0)
|
|
tile_root.add_child(existing_label)
|
|
|
|
existing_label.text = _build_floor_inventory_label(floor_items)
|
|
|
|
|
|
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
|
|
|
|
_border_material = StandardMaterial3D.new()
|
|
_border_material.albedo_color = border_color
|
|
_border_material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
|
_border_material.disable_receive_shadows = true
|
|
_border_material.no_depth_test = true
|
|
return _border_material
|
|
|
|
|
|
func _create_tile_label(location_name: String) -> Label3D:
|
|
var label := Label3D.new()
|
|
label.name = "LocationNameLabel"
|
|
label.text = location_name
|
|
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 _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 _build_floor_inventory_label(floor_items: Array) -> String:
|
|
var parts: Array[String] = []
|
|
for entry in floor_items:
|
|
if typeof(entry) != TYPE_DICTIONARY:
|
|
continue
|
|
var item := entry as Dictionary
|
|
parts.append("%s x%d" % [
|
|
String(item.get("itemKey", "")).strip_edges(),
|
|
int(item.get("quantity", 0))
|
|
])
|
|
if parts.size() >= 3:
|
|
break
|
|
|
|
if parts.is_empty():
|
|
return ""
|
|
|
|
var label := "Floor: %s" % ", ".join(parts)
|
|
if floor_items.size() > parts.size():
|
|
label += " ..."
|
|
return label
|
|
|
|
|
|
func _update_inventory_location_label() -> void:
|
|
if _inventory_location_label == null:
|
|
return
|
|
var location_data := _get_location_data(_center_coord)
|
|
var location_name := String(location_data.get("name", "Unknown Location")).strip_edges()
|
|
_inventory_location_label.text = "%s (%d,%d)" % [location_name, _center_coord.x, _center_coord.y]
|
|
|
|
|
|
func _refresh_inventory_menu_data() -> void:
|
|
if _inventory_request_in_flight:
|
|
return
|
|
_refresh_inventory_menu_data_async()
|
|
|
|
|
|
func _refresh_inventory_menu_data_async() -> void:
|
|
_inventory_request_in_flight = true
|
|
_update_inventory_location_label()
|
|
_character_inventory_items = await _fetch_character_inventory()
|
|
var location_id := _get_current_location_id()
|
|
if not location_id.is_empty():
|
|
await _refresh_location_inventory(location_id)
|
|
_render_inventory_menu()
|
|
_inventory_request_in_flight = false
|
|
|
|
|
|
func _render_inventory_menu() -> void:
|
|
var current_character_selection := _selected_character_item_id
|
|
var current_ground_selection := _selected_ground_item_id
|
|
|
|
_character_items_list.clear()
|
|
var slot_map := {}
|
|
for item_variant in _character_inventory_items:
|
|
if typeof(item_variant) != TYPE_DICTIONARY:
|
|
continue
|
|
var item := item_variant as Dictionary
|
|
var slot_value: Variant = item.get("slot", null)
|
|
if typeof(slot_value) == TYPE_NIL:
|
|
continue
|
|
slot_map[int(slot_value)] = item
|
|
|
|
for slot_index in range(CHARACTER_SLOT_COUNT):
|
|
var text := "Slot %d: (empty)" % slot_index
|
|
var metadata: Dictionary = {}
|
|
if slot_map.has(slot_index):
|
|
var slot_item := slot_map[slot_index] as Dictionary
|
|
text = "Slot %d: %s x%d" % [
|
|
slot_index,
|
|
String(slot_item.get("itemKey", "")).strip_edges(),
|
|
int(slot_item.get("quantity", 0))
|
|
]
|
|
metadata = slot_item
|
|
_character_items_list.add_item(text)
|
|
_character_items_list.set_item_metadata(slot_index, metadata)
|
|
if not current_character_selection.is_empty() and String(metadata.get("itemId", metadata.get("id", ""))).strip_edges() == current_character_selection:
|
|
_character_items_list.select(slot_index)
|
|
|
|
_ground_items_list.clear()
|
|
var floor_items := _get_current_floor_items()
|
|
for index in range(floor_items.size()):
|
|
var floor_item := floor_items[index] as Dictionary
|
|
var floor_text := "%s x%d" % [
|
|
String(floor_item.get("itemKey", "")).strip_edges(),
|
|
int(floor_item.get("quantity", 0))
|
|
]
|
|
_ground_items_list.add_item(floor_text)
|
|
_ground_items_list.set_item_metadata(index, floor_item)
|
|
if not current_ground_selection.is_empty() and String(floor_item.get("itemId", floor_item.get("id", ""))).strip_edges() == current_ground_selection:
|
|
_ground_items_list.select(index)
|
|
|
|
_update_inventory_controls()
|
|
|
|
|
|
func _update_inventory_controls() -> void:
|
|
var selected_item := _get_selected_inventory_item()
|
|
var max_quantity := 1
|
|
if not selected_item.is_empty():
|
|
max_quantity = max(1, int(selected_item.get("quantity", 1)))
|
|
if _selected_ground_item_id == String(selected_item.get("itemId", selected_item.get("id", ""))).strip_edges():
|
|
_target_slot_spin_box.value = float(_default_slot_for_item(selected_item))
|
|
_quantity_spin_box.max_value = float(max_quantity)
|
|
if int(_quantity_spin_box.value) > max_quantity:
|
|
_quantity_spin_box.value = float(max_quantity)
|
|
if int(_quantity_spin_box.value) < 1:
|
|
_quantity_spin_box.value = 1.0
|
|
|
|
|
|
func _get_selected_inventory_item() -> Dictionary:
|
|
if not _selected_character_item_id.is_empty():
|
|
return _find_item_by_id(_character_inventory_items, _selected_character_item_id)
|
|
if not _selected_ground_item_id.is_empty():
|
|
return _find_item_by_id(_get_current_floor_items(), _selected_ground_item_id)
|
|
return {}
|
|
|
|
|
|
func _find_item_by_id(items: Array, item_id: String) -> Dictionary:
|
|
for item_variant in items:
|
|
if typeof(item_variant) != TYPE_DICTIONARY:
|
|
continue
|
|
var item := item_variant as Dictionary
|
|
if String(item.get("itemId", item.get("id", ""))).strip_edges() == item_id:
|
|
return item
|
|
return {}
|
|
|
|
|
|
func _get_current_location_id() -> String:
|
|
var location_data := _get_location_data(_center_coord)
|
|
return String(location_data.get("id", "")).strip_edges()
|
|
|
|
|
|
func _get_current_floor_items() -> Array:
|
|
var location_data := _get_location_data(_center_coord)
|
|
return location_data.get("floorItems", [])
|
|
|
|
|
|
func _default_slot_for_item(item: Dictionary) -> int:
|
|
var item_key := String(item.get("itemKey", "")).strip_edges()
|
|
for existing_variant in _character_inventory_items:
|
|
if typeof(existing_variant) != TYPE_DICTIONARY:
|
|
continue
|
|
var existing := existing_variant as Dictionary
|
|
if String(existing.get("itemKey", "")).strip_edges() != item_key:
|
|
continue
|
|
return int(existing.get("slot", 0))
|
|
return _first_open_character_slot()
|
|
|
|
|
|
func _first_open_character_slot() -> int:
|
|
var used_slots := {}
|
|
for item_variant in _character_inventory_items:
|
|
if typeof(item_variant) != TYPE_DICTIONARY:
|
|
continue
|
|
var item := item_variant as Dictionary
|
|
var slot_value: Variant = item.get("slot", null)
|
|
if typeof(slot_value) == TYPE_NIL:
|
|
continue
|
|
used_slots[int(slot_value)] = true
|
|
for slot_index in range(CHARACTER_SLOT_COUNT):
|
|
if not used_slots.has(slot_index):
|
|
return slot_index
|
|
return 0
|
|
|
|
|
|
func _on_character_items_selected(index: int) -> void:
|
|
var metadata: Variant = _character_items_list.get_item_metadata(index)
|
|
_selected_ground_item_id = ""
|
|
_ground_items_list.deselect_all()
|
|
if typeof(metadata) != TYPE_DICTIONARY or (metadata as Dictionary).is_empty():
|
|
_selected_character_item_id = ""
|
|
else:
|
|
var item := metadata as Dictionary
|
|
_selected_character_item_id = String(item.get("itemId", item.get("id", ""))).strip_edges()
|
|
_target_slot_spin_box.value = float(int(item.get("slot", 0)))
|
|
_update_inventory_controls()
|
|
|
|
|
|
func _on_ground_items_selected(index: int) -> void:
|
|
var metadata: Variant = _ground_items_list.get_item_metadata(index)
|
|
_selected_character_item_id = ""
|
|
_character_items_list.deselect_all()
|
|
if typeof(metadata) != TYPE_DICTIONARY or (metadata as Dictionary).is_empty():
|
|
_selected_ground_item_id = ""
|
|
else:
|
|
var item := metadata as Dictionary
|
|
_selected_ground_item_id = String(item.get("itemId", item.get("id", ""))).strip_edges()
|
|
_target_slot_spin_box.value = float(_default_slot_for_item(item))
|
|
_update_inventory_controls()
|
|
|
|
|
|
func _on_inventory_move_pressed() -> void:
|
|
if _selected_character_item_id.is_empty():
|
|
_inventory_status_label.text = "Select a character item first."
|
|
return
|
|
var to_slot := int(_target_slot_spin_box.value)
|
|
var quantity := int(_quantity_spin_box.value)
|
|
_move_character_item_async(_selected_character_item_id, to_slot, quantity)
|
|
|
|
|
|
func _on_inventory_drop_pressed() -> void:
|
|
if _selected_character_item_id.is_empty():
|
|
_inventory_status_label.text = "Select a character item to drop."
|
|
return
|
|
var location_id := _get_current_location_id()
|
|
if location_id.is_empty():
|
|
_inventory_status_label.text = "Current location is missing an id."
|
|
return
|
|
var quantity := int(_quantity_spin_box.value)
|
|
_transfer_item_async(_selected_character_item_id, "character", _character_id, "location", location_id, null, quantity, "Dropped item.")
|
|
|
|
|
|
func _on_inventory_pickup_pressed() -> void:
|
|
if _selected_ground_item_id.is_empty():
|
|
_inventory_status_label.text = "Select a ground item to pick up."
|
|
return
|
|
var location_id := _get_current_location_id()
|
|
if location_id.is_empty():
|
|
_inventory_status_label.text = "Current location is missing an id."
|
|
return
|
|
var quantity := int(_quantity_spin_box.value)
|
|
var to_slot := int(_target_slot_spin_box.value)
|
|
_transfer_item_async(_selected_ground_item_id, "location", location_id, "character", _character_id, to_slot, quantity, "Picked up item.")
|
|
|
|
|
|
func _on_inventory_refresh_pressed() -> void:
|
|
_inventory_status_label.text = "Refreshing..."
|
|
_refresh_inventory_menu_data()
|
|
|
|
|
|
func _on_inventory_close_pressed() -> void:
|
|
_close_inventory_menu()
|
|
|
|
|
|
func _move_character_item_async(item_id: String, to_slot: int, quantity: int) -> void:
|
|
if _inventory_request_in_flight:
|
|
return
|
|
_inventory_request_in_flight = true
|
|
_inventory_status_label.text = "Moving item..."
|
|
|
|
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({
|
|
"itemId": item_id,
|
|
"toSlot": to_slot,
|
|
"quantity": quantity
|
|
})
|
|
var err := request.request("%s/by-owner/character/%s/move" % [INVENTORY_API_URL, _character_id], headers, HTTPClient.METHOD_POST, body)
|
|
if err != OK:
|
|
request.queue_free()
|
|
_inventory_status_label.text = "Move request failed."
|
|
_inventory_request_in_flight = false
|
|
return
|
|
|
|
var result: Array = await request.request_completed
|
|
request.queue_free()
|
|
_inventory_request_in_flight = false
|
|
_handle_inventory_mutation_response(result, "Item moved.")
|
|
|
|
|
|
func _transfer_item_async(item_id: String, from_owner_type: String, from_owner_id: String, to_owner_type: String, to_owner_id: String, to_slot: Variant, quantity: int, success_message: String) -> void:
|
|
if _inventory_request_in_flight:
|
|
return
|
|
_inventory_request_in_flight = true
|
|
_inventory_status_label.text = "Transferring item..."
|
|
|
|
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 payload := {
|
|
"itemId": item_id,
|
|
"fromOwnerType": from_owner_type,
|
|
"fromOwnerId": from_owner_id,
|
|
"toOwnerType": to_owner_type,
|
|
"toOwnerId": to_owner_id,
|
|
"quantity": quantity
|
|
}
|
|
if typeof(to_slot) != TYPE_NIL:
|
|
payload["toSlot"] = int(to_slot)
|
|
|
|
var err := request.request("%s/transfer" % INVENTORY_API_URL, headers, HTTPClient.METHOD_POST, JSON.stringify(payload))
|
|
if err != OK:
|
|
request.queue_free()
|
|
_inventory_status_label.text = "Transfer request failed."
|
|
_inventory_request_in_flight = false
|
|
return
|
|
|
|
var result: Array = await request.request_completed
|
|
request.queue_free()
|
|
_inventory_request_in_flight = false
|
|
_handle_inventory_mutation_response(result, success_message)
|
|
|
|
|
|
func _handle_inventory_mutation_response(result: Array, success_message: String) -> void:
|
|
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:
|
|
_inventory_status_label.text = "Inventory action failed."
|
|
push_warning("Inventory action failed (%s/%s): %s" % [result_code, response_code, response_body])
|
|
return
|
|
|
|
_inventory_status_label.text = success_message
|
|
_refresh_inventory_menu_data()
|
|
|
|
|
|
func _fetch_character_inventory() -> Array:
|
|
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("%s/by-owner/character/%s" % [INVENTORY_API_URL, _character_id], headers, HTTPClient.METHOD_GET)
|
|
if err != OK:
|
|
request.queue_free()
|
|
push_warning("Failed to request character inventory: %s" % err)
|
|
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("Failed to load character inventory (%s/%s): %s" % [result_code, response_code, response_body])
|
|
return []
|
|
|
|
var parsed: Variant = JSON.parse_string(response_body)
|
|
if typeof(parsed) != TYPE_DICTIONARY:
|
|
return []
|
|
|
|
var payload := parsed as Dictionary
|
|
return _parse_floor_inventory_items(payload.get("items", []))
|
|
|
|
|
|
func _ensure_selected_location_exists(coord: Vector2i) -> void:
|
|
if _known_locations.has(coord):
|
|
return
|
|
_known_locations[coord] = {
|
|
"id": "",
|
|
"name": _selected_location_name(coord),
|
|
"biomeKey": "plains",
|
|
"locationObject": {},
|
|
"floorItems": []
|
|
}
|
|
|
|
|
|
func _selected_location_name(coord: Vector2i) -> String:
|
|
var selected_name := String(SelectedCharacter.character.get("locationName", "")).strip_edges()
|
|
if not selected_name.is_empty():
|
|
return selected_name
|
|
var character_name := String(SelectedCharacter.character.get("name", "")).strip_edges()
|
|
if not character_name.is_empty():
|
|
return "%s's Location" % character_name
|
|
return "Location %d,%d" % [coord.x, coord.y]
|
|
|
|
|
|
func _load_existing_locations() -> void:
|
|
_locations_refresh_in_flight = true
|
|
_locations_loaded = false
|
|
_known_locations.clear()
|
|
|
|
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()
|
|
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("%s/%s/visible-locations" % [CHARACTER_API_URL, _character_id], headers, HTTPClient.METHOD_GET)
|
|
if err != OK:
|
|
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
|
|
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("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
|
|
for item in parsed:
|
|
if typeof(item) != TYPE_DICTIONARY:
|
|
continue
|
|
var location := item as Dictionary
|
|
var coord_variant: Variant = location.get("coord", {})
|
|
if typeof(coord_variant) != TYPE_DICTIONARY:
|
|
continue
|
|
var coord_dict := coord_variant as Dictionary
|
|
var coord := Vector2i(int(coord_dict.get("x", 0)), int(coord_dict.get("y", 0)))
|
|
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] = {
|
|
"id": String(location.get("id", "")).strip_edges(),
|
|
"name": location_name,
|
|
"biomeKey": String(location.get("biomeKey", "plains")).strip_edges(),
|
|
"locationObject": _parse_location_object(location.get("locationObject", {})),
|
|
"floorItems": _parse_floor_inventory_items(location.get("floorItems", []))
|
|
}
|
|
loaded_count += 1
|
|
|
|
print("LocationLevel loaded %d visible locations for character %s." % [loaded_count, _character_id])
|
|
if loaded_count == 0:
|
|
push_warning("Visible locations request succeeded but returned 0 locations for character %s." % _character_id)
|
|
|
|
_locations_loaded = true
|
|
_locations_refresh_in_flight = false
|
|
_rebuild_tiles(_center_coord)
|
|
if _queued_locations_refresh:
|
|
_queued_locations_refresh = false
|
|
_queue_locations_refresh()
|
|
|
|
|
|
func _queue_locations_refresh() -> void:
|
|
if _locations_refresh_in_flight:
|
|
_queued_locations_refresh = true
|
|
return
|
|
_refresh_visible_locations()
|
|
|
|
|
|
func _refresh_visible_locations() -> void:
|
|
if _character_id.is_empty():
|
|
return
|
|
_refresh_visible_locations_async()
|
|
|
|
|
|
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)
|
|
await _refresh_location_inventory(location_id)
|
|
_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
|
|
if _coord_sync_in_flight:
|
|
_queued_coord_sync = coord
|
|
return
|
|
_sync_character_coord(coord)
|
|
|
|
|
|
func _sync_character_coord(coord: Vector2i) -> void:
|
|
if _character_id.is_empty():
|
|
return
|
|
_coord_sync_in_flight = true
|
|
_queued_coord_sync = null
|
|
_sync_character_coord_async(coord)
|
|
|
|
|
|
func _sync_character_coord_async(coord: Vector2i) -> void:
|
|
var response := await CharacterService.update_character_coord(_character_id, coord)
|
|
if response.get("ok", false):
|
|
_persisted_coord = coord
|
|
SelectedCharacter.set_coord(coord)
|
|
else:
|
|
push_warning("Failed to persist character coord to %s,%s: status=%s error=%s body=%s" % [
|
|
coord.x,
|
|
coord.y,
|
|
response.get("status", "n/a"),
|
|
response.get("error", ""),
|
|
response.get("body", "")
|
|
])
|
|
|
|
_coord_sync_in_flight = false
|
|
if _queued_coord_sync != null and _queued_coord_sync is Vector2i and _queued_coord_sync != _persisted_coord:
|
|
var queued_coord: Vector2i = _queued_coord_sync
|
|
_sync_character_coord(queued_coord)
|
|
|
|
|
|
func _get_location_data(coord: Vector2i) -> Dictionary:
|
|
var value: Variant = _known_locations.get(coord, {})
|
|
if typeof(value) == TYPE_DICTIONARY:
|
|
return value as Dictionary
|
|
return {}
|
|
|
|
|
|
func _parse_location_object(value: Variant) -> Dictionary:
|
|
if typeof(value) != TYPE_DICTIONARY:
|
|
return {}
|
|
|
|
var object_data := value as Dictionary
|
|
var state_value: Variant = object_data.get("state", {})
|
|
var state: Dictionary = {}
|
|
if typeof(state_value) == TYPE_DICTIONARY:
|
|
var raw_state := state_value as Dictionary
|
|
state = {
|
|
"itemKey": String(raw_state.get("itemKey", "")).strip_edges(),
|
|
"remainingQuantity": int(raw_state.get("remainingQuantity", 0)),
|
|
"gatherQuantity": int(raw_state.get("gatherQuantity", 1))
|
|
}
|
|
|
|
return {
|
|
"id": String(object_data.get("id", "")).strip_edges(),
|
|
"objectType": String(object_data.get("objectType", "")).strip_edges(),
|
|
"objectKey": String(object_data.get("objectKey", "")).strip_edges(),
|
|
"name": String(object_data.get("name", "")).strip_edges(),
|
|
"state": state
|
|
}
|
|
|
|
|
|
func _parse_floor_inventory_items(value: Variant) -> Array:
|
|
var items: Array = []
|
|
if typeof(value) != TYPE_ARRAY:
|
|
return items
|
|
|
|
for entry in value:
|
|
if typeof(entry) != TYPE_DICTIONARY:
|
|
continue
|
|
var item := entry as Dictionary
|
|
items.append({
|
|
"itemId": String(item.get("itemId", item.get("id", ""))).strip_edges(),
|
|
"itemKey": String(item.get("itemKey", "")).strip_edges(),
|
|
"quantity": int(item.get("quantity", 0)),
|
|
"slot": item.get("slot", null)
|
|
})
|
|
|
|
return items
|
|
|
|
func _refresh_location_inventory(location_id: String) -> void:
|
|
if location_id.is_empty():
|
|
return
|
|
var floor_items := await _fetch_location_inventory(location_id)
|
|
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", "")).strip_edges() != location_id:
|
|
continue
|
|
location_data["floorItems"] = floor_items
|
|
_known_locations[coord] = location_data
|
|
_update_tile(coord, location_data)
|
|
return
|
|
|
|
|
|
func _fetch_location_inventory(location_id: String) -> Array:
|
|
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("%s/by-owner/location/%s" % [INVENTORY_API_URL, location_id], headers, HTTPClient.METHOD_GET)
|
|
if err != OK:
|
|
request.queue_free()
|
|
push_warning("Failed to request floor inventory for location %s: %s" % [location_id, err])
|
|
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("Failed to load floor inventory for location %s (%s/%s): %s" % [location_id, result_code, response_code, response_body])
|
|
return []
|
|
|
|
var parsed: Variant = JSON.parse_string(response_body)
|
|
if typeof(parsed) != TYPE_DICTIONARY:
|
|
return []
|
|
|
|
var payload := parsed as Dictionary
|
|
return _parse_floor_inventory_items(payload.get("items", []))
|
|
|
|
|
|
func _get_biome_material(tile: MeshInstance3D, biome_key: String) -> Material:
|
|
var normalized_biome := biome_key if not biome_key.is_empty() else "plains"
|
|
if _biome_materials.has(normalized_biome):
|
|
return _biome_materials[normalized_biome]
|
|
|
|
var source_material := tile.get_active_material(0)
|
|
if source_material is StandardMaterial3D:
|
|
var material := (source_material as StandardMaterial3D).duplicate() as StandardMaterial3D
|
|
material.albedo_color = _get_biome_color(normalized_biome)
|
|
_biome_materials[normalized_biome] = material
|
|
return material
|
|
|
|
return source_material
|
|
|
|
|
|
func _get_biome_color(biome_key: String) -> Color:
|
|
match biome_key:
|
|
"forest":
|
|
return Color(0.36, 0.62, 0.34, 1.0)
|
|
"wetlands":
|
|
return Color(0.28, 0.52, 0.44, 1.0)
|
|
"rocky":
|
|
return Color(0.52, 0.50, 0.44, 1.0)
|
|
"desert":
|
|
return Color(0.76, 0.67, 0.38, 1.0)
|
|
_:
|
|
return Color(0.56, 0.72, 0.38, 1.0)
|