feat: Integrate custom prop library and sandbox functionality
- Added support for custom props in the sandbox, including loading, saving, and rendering. - Implemented `PropLibrary` for managing custom prop entries and scanning for `.prop` files. - Introduced `PropVisuals` for shared visual and collision representation of custom props. - Enhanced `StageSpawner` to handle spawning of custom props with layered visuals and collision hulls. - Created `MissingPropPlaceholder` to manage missing custom props during scene reloads. - Updated thumbnail generation to support custom props with unique keys based on file paths. - Added tests for custom prop lifecycle, authored visual values, and missing prop handling. - Refactored existing code to accommodate new prop functionalities and ensure backward compatibility.
This commit is contained in:
+33
-10
@@ -39,6 +39,7 @@ const RULE_EDITOR := preload("res://scenes/rule_editor.tscn")
|
||||
const WAYPOINT_CONTEXT := preload("res://scripts/waypoint_context.gd")
|
||||
const STAGE_SCENE_IO := preload("res://scripts/stage_scene_io.gd")
|
||||
const MISSING_STICKMAN_PLACEHOLDER := preload("res://scripts/missing_stickman_placeholder.gd")
|
||||
const MISSING_PROP_PLACEHOLDER := preload("res://scripts/missing_prop_placeholder.gd")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Enums
|
||||
@@ -1272,7 +1273,7 @@ func _collect_scene_data(is_autosave: bool = false) -> Dictionary:
|
||||
var n := node as Node2D
|
||||
var type := ""
|
||||
if n is TerrainBlock: type = "terrain"
|
||||
elif n is PropBlock: type = "prop"
|
||||
elif n is PropBlock or n.get_script() == MISSING_PROP_PLACEHOLDER: type = "prop"
|
||||
elif n is STICKMAN_RIG or n.get_script() == MISSING_STICKMAN_PLACEHOLDER: type = "stickman"
|
||||
elif n is TriggerArea: type = "area"
|
||||
else: continue
|
||||
@@ -1286,6 +1287,9 @@ func _collect_scene_data(is_autosave: bool = false) -> Dictionary:
|
||||
if type == "terrain":
|
||||
entry["asset_id"] = (n as TerrainBlock).spawn_id
|
||||
entry["scale"] = STAGE_SCENE_IO.vector_to_data(n.scale)
|
||||
elif type == "prop" and n.get_script() == MISSING_PROP_PLACEHOLDER:
|
||||
entry = (n.get("original_entry") as Dictionary).duplicate(true)
|
||||
entry["file_id"] = file_id; entry["type"] = "prop"; entry["position"] = STAGE_SCENE_IO.vector_to_data(pos); entry["rotation"] = rot; entry["scale"] = STAGE_SCENE_IO.vector_to_data(n.scale)
|
||||
elif type == "prop":
|
||||
var prop := n as PropBlock
|
||||
entry["asset_id"] = prop.spawn_id
|
||||
@@ -1355,7 +1359,11 @@ func _apply_scene_data(data: Dictionary, source_path: String, is_recovery: bool)
|
||||
var node: Node2D = null
|
||||
match type:
|
||||
"terrain": node = _spawner.spawn_terrain_asset(String(entry.get("asset_id", "")), pos)
|
||||
"prop": node = _spawner.spawn_prop_asset(String(entry.get("asset_id", "")), pos)
|
||||
"prop":
|
||||
node = _spawner.spawn_prop_asset(String(entry.get("asset_id", "")), pos)
|
||||
if node == null and PROP_LIBRARY.is_custom_path(String(entry.get("asset_id", ""))):
|
||||
var prop_placeholder: Node2D = MISSING_PROP_PLACEHOLDER.new() as Node2D
|
||||
prop_placeholder.configure(entry); prop_placeholder.position = pos; _world.add_child(prop_placeholder); node = prop_placeholder
|
||||
"area": node = _spawner.spawn_area(pos)
|
||||
"stickman":
|
||||
node = _spawner.spawn_stickman_asset(String(entry.get("stk_path", "")), pos)
|
||||
@@ -2408,7 +2416,7 @@ func _open_selector(kind: String) -> void:
|
||||
if not entries.is_empty() and _stickman_library.find_by_path(_spawner.get_selected_stickman_path()).is_empty():
|
||||
_spawner.selected_stickman_path = String(entries[0]["path"])
|
||||
else:
|
||||
entries = PROP_LIBRARY.get_entries()
|
||||
entries = PROP_LIBRARY.scan()
|
||||
if kind == "stickman" and entries.size() == 1:
|
||||
_on_asset_selected(entries[0])
|
||||
return
|
||||
@@ -2449,18 +2457,29 @@ func _on_selector_cancelled() -> void:
|
||||
|
||||
func _on_browse_requested() -> void:
|
||||
if _browse_dialog != null:
|
||||
if _selector_kind == "prop":
|
||||
_browse_dialog.title = "Open .prop"
|
||||
_browse_dialog.filters = PackedStringArray(["*.prop ; Prop Files"])
|
||||
else:
|
||||
_browse_dialog.title = "Open .stk"
|
||||
_browse_dialog.filters = PackedStringArray(["*.stk ; Stickman Files"])
|
||||
_browse_dialog.popup_centered()
|
||||
|
||||
|
||||
func _on_browse_file_selected(path: String) -> void:
|
||||
var entry := _stickman_library.make_entry(path)
|
||||
var entry: Dictionary = PROP_LIBRARY.make_custom_entry(path) if _selector_kind == "prop" else _stickman_library.make_entry(path)
|
||||
if entry.is_empty():
|
||||
_show_toast("Could not load stickman: " + path)
|
||||
_show_toast("Could not load %s: %s" % [_selector_kind, path])
|
||||
return
|
||||
_on_asset_selected(entry)
|
||||
|
||||
|
||||
func _on_refresh_requested() -> void:
|
||||
if _selector_kind == "prop":
|
||||
var prop_entries: Array[Dictionary] = PROP_LIBRARY.scan()
|
||||
_selector.set_entries(prop_entries)
|
||||
_load_or_enqueue_thumbnails("prop", prop_entries)
|
||||
return
|
||||
var entries: Array[Dictionary] = _stickman_library.scan()
|
||||
if not entries.is_empty() and _stickman_library.find_by_path(_spawner.get_selected_stickman_path()).is_empty():
|
||||
_spawner.selected_stickman_path = String(entries[0]["path"])
|
||||
@@ -2485,7 +2504,7 @@ func _load_or_enqueue_thumbnails(kind: String, entries: Array[Dictionary]) -> vo
|
||||
_thumbnail_queue.append({ "kind": kind, "entry": entry, "key": key, "cache_path": png, "generation": generation })
|
||||
else:
|
||||
var id := String(entry.get("id", ""))
|
||||
var png := _thumbnail_cache.prop_png(id)
|
||||
var png := _thumbnail_cache.prop_file_png(id) if bool(entry.get("is_custom", false)) else _thumbnail_cache.prop_png(id)
|
||||
var tex := _thumbnail_cache.get_texture(png)
|
||||
if tex != null:
|
||||
if tex != null and _selector != null:
|
||||
@@ -2509,11 +2528,15 @@ func _drain_thumbnail_queue() -> void:
|
||||
if tex != null:
|
||||
_thumbnail_cache.store_texture(String(job.get("cache_path", _thumbnail_cache.stickman_png(key))), tex)
|
||||
else:
|
||||
var payload: Dictionary = entry["payload"]
|
||||
var preset: int = int(entry["material_preset"])
|
||||
tex = await _prop_thumb.render(payload, preset)
|
||||
if bool(entry.get("is_custom", false)):
|
||||
tex = await _prop_thumb.render_shapes(entry.get("shapes", []) as Array[Dictionary])
|
||||
else:
|
||||
var payload: Dictionary = entry["payload"]
|
||||
var preset: int = int(entry["material_preset"])
|
||||
tex = await _prop_thumb.render(payload, preset)
|
||||
if tex != null:
|
||||
_thumbnail_cache.store_texture(String(job.get("cache_path", _thumbnail_cache.prop_png(String(entry["id"])))), tex)
|
||||
var fallback := _thumbnail_cache.prop_file_png(String(entry["id"])) if bool(entry.get("is_custom", false)) else _thumbnail_cache.prop_png(String(entry["id"]))
|
||||
_thumbnail_cache.store_texture(String(job.get("cache_path", fallback)), tex)
|
||||
_thumbnail_busy = false
|
||||
if _selector != null and _selector_open and int(job.get("generation", -1)) == _thumbnail_generation and tex != null:
|
||||
_selector.set_thumbnail(entry, tex)
|
||||
|
||||
Reference in New Issue
Block a user