class_name CharacterSheet extends RefCounted ## The character, as state (§2). STORES only what was rolled or chosen, plus the two ## numbers that genuinely mutate in play (current hp/mp). Everything else is DERIVED — ## so a derived number can never drift from the inputs that produced it, and M5's level ## curve becomes a function change rather than a stored-field migration. ## ## LCK IS NOT HERE, and there is no accessor for it. It lives in GameState.luck: no ## save, no skill, no row (§7). That holds by construction, not by discipline. const PROF_BONUS_L1 := 2 # --- stored: rolled, chosen, or genuinely mutable ------------------------------- var attributes: Dictionary = {} # {str,dex,con,fth,mag} — final, post-spend var race_id: String = "" var calling_id: String = "" var level: int = 1 var skills: Array = [] # chosen + race-granted + the human's bonus pick var bonus_skill: String = "" # human only; also present in `skills` var hp: int = 0 # CURRENT hp — combat writes this var mp: int = 0 # CURRENT mp # --- derived: functions, never stored ------------------------------------------- func modifier(stat: String) -> int: return Attributes.modifier(int(attributes.get(stat, 10))) func prof_bonus() -> int: return PROF_BONUS_L1 # the level curve is M5's; creation is L1 func max_hp() -> int: return Callings.hit_die(calling_id) + modifier("con") func max_mp() -> int: # TUNABLE — M5 owns the real MP curve (races-and-classes spec §6 defers it). # The SHAPE is what M4-b needs to draw a bar: martials have no pool at all; a # caster's scales off the casting stat, with a floor so a bad roll still casts. if not Callings.is_caster(calling_id): return 0 return maxi(4, 4 + 4 * modifier(Callings.casting_stat(calling_id))) func ac() -> int: return 10 + modifier("dex") # armor is an item -> M7 func initiative() -> int: return modifier("dex") func is_proficient(skill: String) -> bool: return skill in skills func save(stat: String) -> int: var v := modifier(stat) if stat in Callings.saves(calling_id): v += prof_bonus() v += Races.save_bonus(race_id) # human's +1 to all five return v func poison_save_bonus() -> int: # Situational (vs poison + the affliction track) — deliberately NOT folded into # save("con"), because a conditional inside a flat number is a sheet that lies. return Races.poison_save_bonus(race_id) func skill_bonus(skill: String) -> int: var v := modifier(Skills.attribute(skill)) if is_proficient(skill): v += prof_bonus() return v func spell_dc() -> int: if not Callings.is_caster(calling_id): return 0 return 8 + prof_bonus() + modifier(Callings.casting_stat(calling_id)) func has_nightsight() -> bool: return Races.flag(race_id, "nightsight") func has_claws() -> bool: return Races.flag(race_id, "claws") func has_keen_scent() -> bool: return Races.flag(race_id, "keen_scent")