diff --git a/client/scripts/content/content_db.gd b/client/scripts/content/content_db.gd new file mode 100644 index 0000000..5c262ba --- /dev/null +++ b/client/scripts/content/content_db.gd @@ -0,0 +1,83 @@ +class_name ContentDB +extends RefCounted +## Loads authored world content (/content) and cross-checks origin references. +## Mirrors api/app/content.py so client and api resolve ids identically. This is +## content integrity, distinct from JSON-schema validation. + +var locations: Dictionary = {} +var npcs: Dictionary = {} +var quests: Dictionary = {} +var items: Dictionary = {} + + +static func default_content_root() -> String: + return ProjectSettings.globalize_path("res://").path_join("../content").simplify_path() + + +static func origin_path(id: String) -> String: + return default_content_root().path_join("origins").path_join("%s.json" % id) + + +static func load_json(abs_path: String) -> Variant: + var text := FileAccess.get_file_as_string(abs_path) + return JSON.parse_string(text) + + +func load_from(content_root: String) -> void: + var world := content_root.path_join("world") + locations = _load_dir(world.path_join("locations")) + npcs = _load_dir(world.path_join("npcs")) + quests = _load_dir(world.path_join("quests")) + items = _load_dir(world.path_join("items")) + + +func _load_dir(dir_path: String) -> Dictionary: + var out: Dictionary = {} + var dir := DirAccess.open(dir_path) + if dir == null: + push_error("content dir not found: %s" % dir_path) + return out + for file in dir.get_files(): + if not file.ends_with(".json"): + continue + var data: Variant = load_json(dir_path.path_join(file)) + if typeof(data) != TYPE_DICTIONARY or not data.has("id"): + push_error("bad content file: %s" % file) + continue + out[data["id"]] = data + return out + + +func location(id: String) -> Dictionary: return locations.get(id, {}) +func npc(id: String) -> Dictionary: return npcs.get(id, {}) +func quest(id: String) -> Dictionary: return quests.get(id, {}) +func item(id: String) -> Dictionary: return items.get(id, {}) +func has_location(id: String) -> bool: return locations.has(id) +func has_npc(id: String) -> bool: return npcs.has(id) +func has_quest(id: String) -> bool: return quests.has(id) +func has_item(id: String) -> bool: return items.has(id) + + +func companions() -> Array: + var out: Array = [] + for id in npcs: + if npcs[id].get("role", "") == "companion": + out.append(npcs[id]) + return out + + +func unresolved_refs(origin: Dictionary) -> Array: + var missing: Array = [] + var loc: String = origin.get("start_location_id", "") + if not has_location(loc): + missing.append("location:%s" % loc) + var q: Variant = origin.get("start_quest_id", null) + if q != null and not has_quest(q): + missing.append("quest:%s" % q) + for grant in origin.get("inventory_grants", []): + if not has_item(grant.get("item_id", "")): + missing.append("item:%s" % grant.get("item_id", "")) + for npc_id in origin.get("disposition_overrides", {}): + if not has_npc(npc_id): + missing.append("npc:%s" % npc_id) + return missing diff --git a/client/scripts/content/content_db.gd.uid b/client/scripts/content/content_db.gd.uid new file mode 100644 index 0000000..52d4228 --- /dev/null +++ b/client/scripts/content/content_db.gd.uid @@ -0,0 +1 @@ +uid://dnd1rmbg2c1el diff --git a/client/tests/unit/test_content_db.gd b/client/tests/unit/test_content_db.gd new file mode 100644 index 0000000..f237fb3 --- /dev/null +++ b/client/tests/unit/test_content_db.gd @@ -0,0 +1,52 @@ +extends "res://addons/gut/test.gd" + +const ContentDB = preload("res://scripts/content/content_db.gd") + +var db + + +func before_each(): + db = ContentDB.new() + db.load_from(ContentDB.default_content_root()) + + +func _deserter() -> Dictionary: + return ContentDB.load_json(ContentDB.origin_path("deserter")) + + +func test_loads_world_ids(): + assert_true(db.has_location("greywater_docks")) + assert_true(db.has_quest("find_the_ledger")) + assert_true(db.has_item("worn_shortsword")) + assert_true(db.has_item("coin")) + + +func test_companions_are_brannoc_and_cadwyn(): + var ids := [] + for n in db.companions(): + ids.append(n["id"]) + ids.sort() + assert_eq(ids, ["brannoc_thane","cadwyn_vell"]) + + +func test_good_origin_has_no_unresolved_refs(): + assert_eq(db.unresolved_refs(_deserter()), []) + + +func test_broken_location_ref_detected(): + var o := _deserter() + o["start_location_id"] = "nowhere" + assert_true("location:nowhere" in db.unresolved_refs(o)) + + +func test_broken_item_ref_detected(): + var o := _deserter() + o["inventory_grants"] = o["inventory_grants"].duplicate(true) + o["inventory_grants"].append({"item_id": "ghost_blade", "qty": 1}) + assert_true("item:ghost_blade" in db.unresolved_refs(o)) + + +func test_null_quest_ref_resolves(): + var o := _deserter() + o["start_quest_id"] = null + assert_eq(db.unresolved_refs(o), []) diff --git a/client/tests/unit/test_content_db.gd.uid b/client/tests/unit/test_content_db.gd.uid new file mode 100644 index 0000000..8cf786a --- /dev/null +++ b/client/tests/unit/test_content_db.gd.uid @@ -0,0 +1 @@ +uid://bi2ysgccscqym