feat(newgame): the seeded creation pipeline
construct() takes a seed, not a stat block. It rebuilds the RNG and rolls the attributes itself, so it never trusts a number handed to it (§2) and the same seed yields an identical character, hidden Luck included (§10). The whole character is reproducible from four primitives: seed, race_id, calling_id, spend. The rng parameter is dropped — the seed is now the only source of randomness. The roll is 3d6 with a hard floor of 8. Straight 3d6 puts ~9% on a primary the +3 pool cannot rescue, and a character bad at the one thing he is FOR is not grit — it is the fine §7 forbids, paid for twenty hours. LCK is unspendable, not merely hidden: spending on it is a validation error. GameState.stats becomes GameState.sheet. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QYa9u7Kdxv5gX4AnwWexy8
This commit is contained in:
@@ -1,47 +1,66 @@
|
||||
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 calling fails here, loudly, not three scenes later.
|
||||
## New-game construction. Reads three immutable inputs (origin, world, creation) and
|
||||
## writes two products (CanonLog, GameState). Nothing flows back up — charter §2.
|
||||
##
|
||||
## Creation carries a SEED, not a stat block. construct() rebuilds the RNG and rolls
|
||||
## the attributes ITSELF, so it never trusts a number handed to it (§2) and the same
|
||||
## seed always yields the same character (§10 — "the difference between a bug you can
|
||||
## reproduce and a bug you cannot"). The whole character is reproducible from four
|
||||
## primitives: seed, race_id, calling_id, spend.
|
||||
##
|
||||
## `creation` stays a plain Dictionary: a saga (later) synthesizes one and skips the
|
||||
## UI entirely, so the creation SCENE must never become the only thing that can make it.
|
||||
|
||||
static func construct(origin: Dictionary, world: ContentDB, creation: Dictionary, rng: RandomNumberGenerator) -> Dictionary:
|
||||
var errors: Array = []
|
||||
const SPEND_POOL := 3
|
||||
|
||||
# 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 calling must be allowed by the origin.
|
||||
var bc: Dictionary = origin.get("build_constraints", {})
|
||||
var allowed: Array = bc.get("allowed_callings", [])
|
||||
var calling_id: String = creation.get("calling_id", "")
|
||||
if calling_id not in allowed:
|
||||
errors.append("calling not allowed by origin: %s" % calling_id)
|
||||
|
||||
var race_id: String = creation.get("race_id", "")
|
||||
if not Races.exists(race_id):
|
||||
errors.append("unknown race: %s" % race_id)
|
||||
|
||||
if creation.get("name", "").strip_edges() == "":
|
||||
errors.append("player name is required")
|
||||
|
||||
static func construct(origin: Dictionary, world: ContentDB, creation: Dictionary) -> Dictionary:
|
||||
var errors := _validate(origin, world, creation)
|
||||
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 rng := RandomNumberGenerator.new()
|
||||
rng.seed = int(creation.get("seed", 0))
|
||||
|
||||
var state := GameState.new()
|
||||
var sheet := CharacterSheet.new()
|
||||
sheet.race_id = str(creation.get("race_id", ""))
|
||||
sheet.calling_id = str(creation.get("calling_id", ""))
|
||||
|
||||
# The RNG ORDER is part of the contract: five attributes in a fixed order, then
|
||||
# Luck. Change the order and every existing seed produces a different character.
|
||||
var attrs: Dictionary = {}
|
||||
for stat in Attributes.IDS:
|
||||
attrs[stat] = _roll_attribute(rng)
|
||||
|
||||
var bc: Dictionary = origin.get("build_constraints", {})
|
||||
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 spend: Dictionary = creation.get("spend", {})
|
||||
for stat in spend:
|
||||
attrs[stat] = int(attrs[stat]) + int(spend[stat])
|
||||
sheet.attributes = attrs
|
||||
|
||||
# Proficiencies: what the race grants + what the player picked + the human's bonus.
|
||||
# All land in one list, so is_proficient() has exactly one thing to read.
|
||||
var skills: Array = []
|
||||
skills.append_array(Races.granted_skills(sheet.race_id))
|
||||
skills.append_array(creation.get("skills", []))
|
||||
var bonus := str(creation.get("bonus_skill", ""))
|
||||
if bonus != "":
|
||||
skills.append(bonus)
|
||||
sheet.bonus_skill = bonus
|
||||
sheet.skills = skills
|
||||
|
||||
sheet.hp = sheet.max_hp() # the character starts whole
|
||||
sheet.mp = sheet.max_mp()
|
||||
state.sheet = sheet
|
||||
|
||||
var log := CanonLog.new()
|
||||
log.player = LogPlayer.new(creation.get("name", ""), race_id, calling_id,
|
||||
state.luck_descriptor())
|
||||
log.player = LogPlayer.new(str(creation.get("name", "")), sheet.race_id,
|
||||
sheet.calling_id, state.luck_descriptor())
|
||||
|
||||
var loc: Dictionary = world.location(origin["start_location_id"])
|
||||
log.set_location(loc.get("id", ""), loc.get("name", ""))
|
||||
@@ -51,7 +70,6 @@ static func construct(origin: Dictionary, world: ContentDB, creation: Dictionary
|
||||
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]))
|
||||
@@ -64,16 +82,100 @@ static func construct(origin: Dictionary, world: ContentDB, creation: Dictionary
|
||||
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", "")))
|
||||
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)))
|
||||
# inventory grants -> game state (NOT the log). §2. grant() routes currency to the
|
||||
# purse; everything else to the inventory dict.
|
||||
for g in origin.get("inventory_grants", []):
|
||||
state.grant(g.get("item_id", ""), int(g.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)
|
||||
static func _roll_attribute(rng: RandomNumberGenerator) -> int:
|
||||
# 3d6 with a HARD FLOOR of 8. The floor clamps the die, not the player: straight
|
||||
# 3d6 puts ~9% on a primary the +3 pool cannot rescue, and a character who is bad
|
||||
# at the one thing he is FOR is not grit — it is the "fine" §7 forbids, paid for
|
||||
# twenty hours.
|
||||
var roll := rng.randi_range(1, 6) + rng.randi_range(1, 6) + rng.randi_range(1, 6)
|
||||
return maxi(8, roll)
|
||||
|
||||
|
||||
static func _validate(origin: Dictionary, world: ContentDB, creation: Dictionary) -> Array:
|
||||
var errors: Array = []
|
||||
|
||||
for ref in world.unresolved_refs(origin):
|
||||
errors.append("unresolved ref: %s" % ref)
|
||||
|
||||
if str(creation.get("name", "")).strip_edges() == "":
|
||||
errors.append("player name is required")
|
||||
|
||||
var race_id: String = str(creation.get("race_id", ""))
|
||||
if not Races.exists(race_id):
|
||||
errors.append("unknown race: %s" % race_id)
|
||||
|
||||
var calling_id: String = str(creation.get("calling_id", ""))
|
||||
if not Callings.exists(calling_id):
|
||||
errors.append("unknown calling: %s" % calling_id)
|
||||
else:
|
||||
var allowed: Array = origin.get("build_constraints", {}).get("allowed_callings", [])
|
||||
if calling_id not in allowed:
|
||||
errors.append("calling not allowed by origin: %s" % calling_id)
|
||||
|
||||
errors.append_array(_validate_spend(creation.get("spend", {})))
|
||||
if Races.exists(race_id) and Callings.exists(calling_id):
|
||||
errors.append_array(_validate_skills(creation, race_id, calling_id))
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
static func _validate_spend(spend: Dictionary) -> Array:
|
||||
var errors: Array = []
|
||||
var total := 0
|
||||
for stat in spend:
|
||||
# LCK is not merely hidden at creation — it is unspendable (§7).
|
||||
if not Attributes.exists(str(stat)):
|
||||
errors.append("cannot spend on '%s' — not an attribute" % stat)
|
||||
continue
|
||||
var v := int(spend[stat])
|
||||
if v < 0:
|
||||
errors.append("spend is additive only: %s is negative" % stat)
|
||||
total += v
|
||||
if total > SPEND_POOL:
|
||||
errors.append("spend exceeds the pool of %d (got %d)" % [SPEND_POOL, total])
|
||||
return errors
|
||||
|
||||
|
||||
static func _validate_skills(creation: Dictionary, race_id: String, calling_id: String) -> Array:
|
||||
var errors: Array = []
|
||||
var picks: Array = creation.get("skills", [])
|
||||
var granted: Array = Races.granted_skills(race_id)
|
||||
var pool: Array = Callings.skill_pool(calling_id)
|
||||
var want := Callings.skill_count(calling_id)
|
||||
|
||||
if picks.size() != want:
|
||||
errors.append("%s picks %d skills, got %d" % [calling_id, want, picks.size()])
|
||||
|
||||
var seen := {}
|
||||
for s in picks:
|
||||
if s in seen:
|
||||
errors.append("duplicate skill pick: %s" % s)
|
||||
seen[s] = true
|
||||
if s not in pool:
|
||||
errors.append("%s is not in the %s pool" % [s, calling_id])
|
||||
if s in granted:
|
||||
errors.append("%s is already granted by %s — the pick buys nothing" % [s, race_id])
|
||||
|
||||
var bonus := str(creation.get("bonus_skill", ""))
|
||||
if Races.wants_bonus_skill(race_id):
|
||||
if bonus == "":
|
||||
errors.append("%s must choose a bonus skill" % race_id)
|
||||
elif not Skills.exists(bonus):
|
||||
errors.append("unknown bonus skill: %s" % bonus)
|
||||
elif bonus in picks or bonus in granted:
|
||||
errors.append("bonus skill %s is already proficient" % bonus)
|
||||
elif bonus != "":
|
||||
errors.append("%s does not get a bonus skill" % race_id)
|
||||
|
||||
return errors
|
||||
|
||||
@@ -6,7 +6,7 @@ extends RefCounted
|
||||
|
||||
var luck: int = 0
|
||||
var luck_base: int = 0
|
||||
var stats: Dictionary = {} # {str, dex, con, fth, mag}
|
||||
var sheet: CharacterSheet = null # the whole character (§2). Built by NewGame.
|
||||
var npc_dispositions: Dictionary = {} # world-npc id -> int (−100..100)
|
||||
var inventory: Dictionary = {} # item_id -> qty (NEVER money — see purse_copper)
|
||||
var purse_copper: int = 0 # ALL money, in copper. One integer (§4.1 of the spec).
|
||||
|
||||
@@ -3,6 +3,12 @@ extends "res://addons/gut/test.gd"
|
||||
const GameState = preload("res://scripts/state/game_state.gd")
|
||||
|
||||
|
||||
func test_state_holds_a_sheet_not_a_stat_bag():
|
||||
var gs = GameState.new()
|
||||
assert_null(gs.sheet, "the sheet is built by NewGame.construct")
|
||||
assert_false("stats" in gs, "the flat stats Dictionary is gone")
|
||||
|
||||
|
||||
func test_add_item_accumulates():
|
||||
var s = GameState.new()
|
||||
s.add_item("worn_shortsword", 3)
|
||||
|
||||
@@ -15,31 +15,151 @@ func _deserter() -> Dictionary:
|
||||
return ContentDB.load_json(ContentDB.origin_path("deserter"))
|
||||
|
||||
|
||||
func _rng() -> RandomNumberGenerator:
|
||||
var r := RandomNumberGenerator.new()
|
||||
r.seed = 7
|
||||
return r
|
||||
func _creation(overrides := {}) -> Dictionary:
|
||||
var c := {
|
||||
"name": "Aldric",
|
||||
"race_id": "human",
|
||||
"calling_id": "sellsword",
|
||||
"seed": 8675309,
|
||||
"spend": {},
|
||||
"skills": ["athletics", "endurance"],
|
||||
"bonus_skill": "perception",
|
||||
}
|
||||
for k in overrides:
|
||||
c[k] = overrides[k]
|
||||
return c
|
||||
|
||||
|
||||
func _build(creation: Dictionary) -> Dictionary:
|
||||
return NewGame.construct(_deserter(), world, creation, _rng())
|
||||
return NewGame.construct(_deserter(), world, creation)
|
||||
|
||||
|
||||
func test_construct_succeeds():
|
||||
var res := _build({"name": "Aldric", "race_id": "human", "calling_id": "sellsword"})
|
||||
func test_same_seed_yields_an_identical_character():
|
||||
# The determinism guarantee (§10). Two independent constructs, one seed.
|
||||
var a := _build(_creation())
|
||||
var b := _build(_creation())
|
||||
assert_true(a["ok"] and b["ok"])
|
||||
assert_eq(a["state"].sheet.attributes, b["state"].sheet.attributes)
|
||||
assert_eq(a["state"].luck, b["state"].luck, "even the hidden Luck is reproducible")
|
||||
|
||||
|
||||
func test_a_different_seed_yields_a_different_character():
|
||||
var a := _build(_creation())
|
||||
var b := _build(_creation({"seed": 1234567}))
|
||||
assert_ne(a["state"].sheet.attributes, b["state"].sheet.attributes)
|
||||
|
||||
|
||||
func test_every_attribute_respects_the_floor_of_eight():
|
||||
# 3d6 can roll 3. The floor is a design guarantee, not a dice accident.
|
||||
for s in range(40):
|
||||
var res := _build(_creation({"seed": s}))
|
||||
assert_true(res["ok"], str(res["errors"]))
|
||||
for stat in Attributes.IDS:
|
||||
assert_true(int(res["state"].sheet.attributes[stat]) >= 8,
|
||||
"seed %d rolled %s below the floor" % [s, stat])
|
||||
|
||||
|
||||
func test_spend_is_additive_on_top_of_the_roll():
|
||||
var base := _build(_creation())
|
||||
var spent := _build(_creation({"spend": {"str": 2, "con": 1}}))
|
||||
assert_eq(int(spent["state"].sheet.attributes["str"]),
|
||||
int(base["state"].sheet.attributes["str"]) + 2)
|
||||
assert_eq(int(spent["state"].sheet.attributes["con"]),
|
||||
int(base["state"].sheet.attributes["con"]) + 1)
|
||||
assert_eq(int(spent["state"].sheet.attributes["dex"]),
|
||||
int(base["state"].sheet.attributes["dex"]), "unspent stats are untouched")
|
||||
|
||||
|
||||
func test_spend_over_the_pool_is_rejected():
|
||||
var res := _build(_creation({"spend": {"str": 2, "con": 2}}))
|
||||
assert_false(res["ok"])
|
||||
assert_true("spend" in str(res["errors"]).to_lower())
|
||||
|
||||
|
||||
func test_spend_cannot_be_negative():
|
||||
var res := _build(_creation({"spend": {"str": -2, "con": 1}}))
|
||||
assert_false(res["ok"], "additive only — there is no [-]")
|
||||
|
||||
|
||||
func test_luck_can_never_be_spent():
|
||||
# §7: LCK is untouchable at creation. Not hidden — impossible.
|
||||
var res := _build(_creation({"spend": {"lck": 3}}))
|
||||
assert_false(res["ok"])
|
||||
|
||||
|
||||
func test_skills_must_come_from_the_callings_pool():
|
||||
var res := _build(_creation({"skills": ["athletics", "sorcery"]}))
|
||||
assert_false(res["ok"], "sorcery is not in the sellsword's pool")
|
||||
|
||||
|
||||
func test_skills_must_be_the_right_count():
|
||||
var res := _build(_creation({"skills": ["athletics"]}))
|
||||
assert_false(res["ok"], "a sellsword picks 2")
|
||||
|
||||
|
||||
func test_skills_reject_a_duplicate():
|
||||
var res := _build(_creation({"skills": ["athletics", "athletics"]}))
|
||||
assert_false(res["ok"])
|
||||
|
||||
|
||||
func test_an_elf_cannot_spend_a_pick_on_perception():
|
||||
# He already has it. A pick that buys nothing is a trap, not a choice.
|
||||
var res := _build(_creation({
|
||||
"race_id": "elf", "bonus_skill": "",
|
||||
"skills": ["athletics", "perception"]}))
|
||||
assert_false(res["ok"])
|
||||
|
||||
|
||||
func test_the_elf_is_granted_perception_anyway():
|
||||
var res := _build(_creation({
|
||||
"race_id": "elf", "bonus_skill": "", "skills": ["athletics", "endurance"]}))
|
||||
assert_true(res["ok"], str(res["errors"]))
|
||||
assert_true(res["state"].sheet.is_proficient("perception"))
|
||||
|
||||
|
||||
func test_numeric_luck_absent_from_log():
|
||||
var res := _build({"name": "Aldric", "race_id": "human", "calling_id": "sellsword"})
|
||||
func test_a_human_must_choose_a_bonus_skill():
|
||||
var res := _build(_creation({"bonus_skill": ""}))
|
||||
assert_false(res["ok"])
|
||||
|
||||
|
||||
func test_a_non_human_must_not_have_one():
|
||||
var res := _build(_creation({"race_id": "dwarf", "bonus_skill": "stealth"}))
|
||||
assert_false(res["ok"])
|
||||
|
||||
|
||||
func test_the_humans_bonus_skill_confers_proficiency():
|
||||
var res := _build(_creation()) # human, bonus_skill perception
|
||||
assert_true(res["ok"], str(res["errors"]))
|
||||
assert_true(res["state"].sheet.is_proficient("perception"))
|
||||
|
||||
|
||||
func test_the_character_starts_whole():
|
||||
var res := _build(_creation())
|
||||
var sheet = res["state"].sheet
|
||||
assert_eq(sheet.hp, sheet.max_hp())
|
||||
assert_eq(sheet.mp, sheet.max_mp())
|
||||
|
||||
|
||||
func test_a_calling_the_origin_forbids_is_rejected():
|
||||
var o := _deserter()
|
||||
o["build_constraints"] = o["build_constraints"].duplicate(true)
|
||||
o["build_constraints"]["allowed_callings"] = ["bonesetter"]
|
||||
var res := NewGame.construct(o, world, _creation())
|
||||
assert_false(res["ok"])
|
||||
|
||||
|
||||
func test_no_luck_number_and_no_attributes_reach_the_log():
|
||||
# §7 + §11: the log carries only what the AI narrates from.
|
||||
var res := _build(_creation())
|
||||
var pd: Dictionary = res["log"].player.to_dict()
|
||||
assert_false("luck" in pd)
|
||||
assert_eq(pd.keys().size(), 4)
|
||||
assert_false("luck" in pd)
|
||||
assert_false("attributes" in pd)
|
||||
assert_true(res["state"].luck >= Luck.MIN and res["state"].luck <= Luck.MAX)
|
||||
|
||||
|
||||
func test_inventory_and_dispo_land_in_state_not_log():
|
||||
var res := _build({"name": "Aldric", "race_id": "human", "calling_id": "sellsword"})
|
||||
var res := _build(_creation())
|
||||
assert_eq(res["state"].inventory.get("worn_shortsword", 0), 1)
|
||||
assert_eq(res["state"].purse_copper, 47, "the deserter is down to his last coin")
|
||||
assert_false("copper" in res["state"].inventory, "money is never an inventory row")
|
||||
@@ -47,20 +167,20 @@ func test_inventory_and_dispo_land_in_state_not_log():
|
||||
|
||||
|
||||
func test_companion_dispositions_from_overrides():
|
||||
var res := _build({"name": "Aldric", "race_id": "human", "calling_id": "sellsword"})
|
||||
var res := _build(_creation())
|
||||
assert_eq(res["log"].party_member("brannoc_thane").disposition, 40)
|
||||
assert_eq(res["log"].party_member("cadwyn_vell").disposition, 15)
|
||||
|
||||
|
||||
func test_situation_and_facts_seeded():
|
||||
var res := _build({"name": "Aldric", "race_id": "human", "calling_id": "sellsword"})
|
||||
var res := _build(_creation())
|
||||
assert_eq(res["log"].recent_events.size(), 2)
|
||||
assert_true("the player deserted the Iron Kettle mercenary company" in res["log"].established_facts)
|
||||
assert_eq(res["log"].humiliations.size(), 0)
|
||||
|
||||
|
||||
func test_active_quest_resolved_from_world():
|
||||
var res := _build({"name": "Aldric", "race_id": "human", "calling_id": "sellsword"})
|
||||
var res := _build(_creation())
|
||||
assert_eq(res["log"].active_quests.size(), 1)
|
||||
assert_eq(res["log"].active_quests[0].id, "find_the_ledger")
|
||||
assert_eq(res["log"].active_quests[0].status, "active")
|
||||
@@ -69,32 +189,25 @@ func test_active_quest_resolved_from_world():
|
||||
func test_null_start_quest_yields_no_quest():
|
||||
var o := _deserter()
|
||||
o["start_quest_id"] = null
|
||||
var res := NewGame.construct(o, world, {"name": "X", "race_id": "human", "calling_id": "sellsword"}, _rng())
|
||||
var res := NewGame.construct(o, world, _creation())
|
||||
assert_eq(res["log"].active_quests.size(), 0)
|
||||
|
||||
|
||||
func test_calling_not_allowed_is_rejected():
|
||||
var res := _build({"name": "X", "race_id": "human", "calling_id": "berserker"})
|
||||
assert_false(res["ok"])
|
||||
assert_true(res["errors"].size() > 0)
|
||||
assert_null(res["log"])
|
||||
|
||||
|
||||
func test_unresolved_origin_is_rejected():
|
||||
var o := _deserter()
|
||||
o["start_location_id"] = "nowhere"
|
||||
var res := NewGame.construct(o, world, {"name": "X", "race_id": "human", "calling_id": "sellsword"}, _rng())
|
||||
var res := NewGame.construct(o, world, _creation())
|
||||
assert_false(res["ok"])
|
||||
assert_true("unresolved ref: location:nowhere" in res["errors"])
|
||||
|
||||
|
||||
func test_empty_name_is_rejected():
|
||||
var missing_name := _build({"race_id": "human", "calling_id": "sellsword"})
|
||||
var missing_name := _build(_creation({"name": ""}))
|
||||
assert_false(missing_name["ok"])
|
||||
assert_true(missing_name["errors"].size() > 0)
|
||||
assert_null(missing_name["log"])
|
||||
|
||||
var whitespace_name := _build({"name": " ", "race_id": "human", "calling_id": "sellsword"})
|
||||
var whitespace_name := _build(_creation({"name": " "}))
|
||||
assert_false(whitespace_name["ok"])
|
||||
assert_true(whitespace_name["errors"].size() > 0)
|
||||
assert_null(whitespace_name["log"])
|
||||
@@ -104,7 +217,7 @@ func test_non_companion_override_goes_to_state_not_log():
|
||||
world.npcs["oda_fenn"] = {"id": "oda_fenn", "name": "Oda Fenn", "role": "npc"}
|
||||
var o := _deserter()
|
||||
o["disposition_overrides"] = {"brannoc_thane": 40, "oda_fenn": -25}
|
||||
var res := NewGame.construct(o, world, {"name": "X", "race_id": "human", "calling_id": "sellsword"}, _rng())
|
||||
var res := NewGame.construct(o, world, _creation())
|
||||
assert_true(res["ok"], str(res["errors"]))
|
||||
assert_eq(res["state"].npc_dispositions["oda_fenn"], -25)
|
||||
assert_null(res["log"].party_member("oda_fenn"))
|
||||
|
||||
@@ -8,10 +8,17 @@ const CanonLog = preload("res://scripts/canon_log/canon_log.gd")
|
||||
func _built_log():
|
||||
var world = ContentDB.new()
|
||||
world.load_from(ContentDB.default_content_root())
|
||||
var rng := RandomNumberGenerator.new()
|
||||
rng.seed = 3
|
||||
var origin: Dictionary = ContentDB.load_json(ContentDB.origin_path("deserter"))
|
||||
var res := NewGame.construct(origin, world, {"name": "Aldric", "race_id": "human", "calling_id": "bonesetter"}, rng)
|
||||
var creation := {
|
||||
"name": "Aldric",
|
||||
"race_id": "human",
|
||||
"calling_id": "bonesetter",
|
||||
"seed": 3,
|
||||
"spend": {},
|
||||
"skills": ["faith_lore", "perception"],
|
||||
"bonus_skill": "endurance",
|
||||
}
|
||||
var res := NewGame.construct(origin, world, creation)
|
||||
assert_true(res["ok"], str(res["errors"]))
|
||||
return res["log"]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user