Files
code_of_conquest_dnd/client/scripts/content/content_db.gd
Phillip Tarrant dc4b14d3e5 fix(content): repair client consumers broken by the Duncarrow purge
The purge plan assumed content/world/** was pure build output. The client
reads it: content_db.gd loads content/world/topics/ (now absent, since every
topic was Duncarrow-generated) and its tests asserted on deleted ids.

- content_db: a missing content dir is an empty dir, not an error (§13)
- test_content_db: repoint at cosmology canon; drop deleted npc.mera-fenn
- test_content_db: assert the no-body-on-client invariant structurally, so it
  cannot pass vacuously when the topic set is empty
- schema.md: finish repointing the worked example onto specimen.md's ids
- restore a local real-bible --check smoke test (was only in CI)
- roadmap: record that generated content has client consumers

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 16:00:06 -05:00

93 lines
3.2 KiB
GDScript

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 = {}
var canon_entities: Dictionary = {}
var topics: 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"))
canon_entities = _load_dir(world.path_join("canon"))
topics = _load_dir(world.path_join("topics"))
func _load_dir(dir_path: String) -> Dictionary:
var out: Dictionary = {}
var dir := DirAccess.open(dir_path)
if dir == null:
# A missing content dir means "no content of this kind authored yet",
# not an error — e.g. content/world/topics/ before any gated content exists.
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 canon(id: String) -> Dictionary: return canon_entities.get(id, {})
func topic(id: String) -> Dictionary: return topics.get(id, {})
func has_canon(id: String) -> bool: return canon_entities.has(id)
func has_topic(id: String) -> bool: return topics.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