class_name NewGame extends RefCounted ## New-game construction (spec "New-game construction"). Reads three immutable ## inputs (origin, world, creation) and writes two products (CanonLog, GameState). ## Nothing flows back up — charter §2 on the client. Validation is front-loaded: ## a broken origin or illegal class fails here, loudly, not three scenes later. static func construct(origin: Dictionary, world: ContentDB, creation: Dictionary, rng: RandomNumberGenerator) -> Dictionary: var errors: Array = [] # 1. content integrity — every referenced id must resolve in world content. for ref in world.unresolved_refs(origin): errors.append("unresolved ref: %s" % ref) # 2. build constraints — chosen class must be allowed by the origin. var bc: Dictionary = origin.get("build_constraints", {}) var allowed: Array = bc.get("allowed_classes", []) var class_id: String = creation.get("class_id", "") if class_id not in allowed: errors.append("class not allowed by origin: %s" % class_id) if creation.get("name", "").strip_edges() == "": errors.append("player name is required") if not errors.is_empty(): return {"ok": false, "errors": errors, "log": null, "state": null} # 3. roll Luck (numeric -> state only) and 4. the stat block (data only). var state := GameState.new() state.luck_base = Luck.roll_base(rng, int(bc.get("luck_modifier", 0))) state.luck = state.luck_base state.stats = { "str": _roll_stat(rng), "dex": _roll_stat(rng), "con": _roll_stat(rng), "fth": _roll_stat(rng), "mag": _roll_stat(rng), } # 5. assemble the canon log. var log := CanonLog.new() log.player = LogPlayer.new(creation.get("name", ""), class_id, state.luck_descriptor()) var loc: Dictionary = world.location(origin["start_location_id"]) log.set_location(loc.get("id", ""), loc.get("name", "")) var overrides: Dictionary = origin.get("disposition_overrides", {}) var companion_ids := {} for c in world.companions(): companion_ids[c["id"]] = true log.party.append(PartyMember.new(c["id"], c.get("name", ""), int(overrides.get(c["id"], 0)))) # non-companion overrides go to game state, never the log (spec §5 / charter §6). for npc_id in overrides: if not companion_ids.has(npc_id): state.set_npc_disposition(npc_id, int(overrides[npc_id])) for line in origin.get("situation", []): log.push_event(line) for fact in origin.get("opening_facts", []): log.add_fact(fact) var quest_id: Variant = origin.get("start_quest_id", null) if quest_id != null: var qd: Dictionary = world.quest(quest_id) log.active_quests.append(Quest.new(qd.get("id", ""), qd.get("name", ""), "active", qd.get("objective", ""))) # inventory grants -> game state (NOT the log). Charter §2. grant() routes # currency to the purse; everything else to the inventory dict. for grant in origin.get("inventory_grants", []): state.grant(grant.get("item_id", ""), int(grant.get("qty", 0))) return {"ok": true, "errors": [], "log": log, "state": state} static func _roll_stat(rng: RandomNumberGenerator) -> int: # 3d6 placeholder; the stat block is data-only in this plan. TUNABLE. return rng.randi_range(1, 6) + rng.randi_range(1, 6) + rng.randi_range(1, 6)