class_name PlayerStateRecord extends RefCounted const SCHEMA_VERSION := 2 const LEGACY_SCHEMA_VERSION := 1 const MAX_HEALTH := 100.0 const RESET_HUNGER := 40.0 const RESET_ENERGY := 100.0 const STACK_THRESHOLD := 80.0 const DEATH_STANDING_PENALTY := 0.85 const DEFAULT_STARVATION_DEATH_THRESHOLD := 1 const DEFAULT_CARRY_CAPACITY := 3.0 var data: Dictionary func _init(record_data: Dictionary = {}) -> void: data = record_data.duplicate(true) static func create_default() -> PlayerStateRecord: return ( PlayerStateRecord . new( { "schema_version": SCHEMA_VERSION, "hunger": RESET_HUNGER, "energy": RESET_ENERGY, "health": MAX_HEALTH, "max_health": MAX_HEALTH, "downed": false, "downed_ticks": 0, "dead": false, "deaths": 0, "starvation_ticks": 0, "starvation_death_threshold": DEFAULT_STARVATION_DEATH_THRESHOLD, "carry_capacity": DEFAULT_CARRY_CAPACITY, "inventory": {}, } ) ) # Compatibility constructor for the first embodied-player slice. New code # should use create_default(), but keeping this alias lets older quest/needs # resources load without introducing a second player-state schema. static func create() -> PlayerStateRecord: return create_default() static func from_dictionary(record_data: Dictionary) -> PlayerStateRecord: var version := int(record_data.get("schema_version", -1)) if version == LEGACY_SCHEMA_VERSION: record_data = _migrate_v1(record_data) elif version != SCHEMA_VERSION: return null if not record_data.has_all( ["hunger", "energy", "health", "max_health", "downed", "downed_ticks", "inventory"] ): return null if not record_data["inventory"] is Dictionary: return null var hunger := float(record_data["hunger"]) var energy := float(record_data["energy"]) var health := float(record_data["health"]) var max_health := float(record_data["max_health"]) if ( not is_finite(hunger) or not is_finite(energy) or not is_finite(health) or not is_finite(max_health) ): return null var normalized_inventory := {} for raw_item_id in record_data["inventory"]: var item_id := String(raw_item_id) var amount := float(record_data["inventory"][raw_item_id]) if ( item_id.is_empty() or normalized_inventory.has(item_id) or not is_finite(amount) or amount < 0.0 ): return null normalized_inventory[item_id] = amount return ( PlayerStateRecord . new( { "schema_version": SCHEMA_VERSION, "hunger": clampf(hunger, 0.0, 100.0), "energy": clampf(energy, 0.0, 100.0), "health": clampf(health, 0.0, max_health), "max_health": max_health, "downed": bool(record_data["downed"]), "downed_ticks": int(record_data["downed_ticks"]), "dead": bool(record_data.get("dead", false)), "deaths": maxi(int(record_data.get("deaths", 0)), 0), "starvation_ticks": maxi(int(record_data.get("starvation_ticks", 0)), 0), "starvation_death_threshold": maxi( int( record_data.get( "starvation_death_threshold", DEFAULT_STARVATION_DEATH_THRESHOLD ) ), 1 ), "carry_capacity": maxf(float(record_data.get("carry_capacity", DEFAULT_CARRY_CAPACITY)), 0.0), "inventory": normalized_inventory, } ) ) static func _migrate_v1(legacy_data: Dictionary) -> Dictionary: var migrated := legacy_data.duplicate(true) migrated["schema_version"] = SCHEMA_VERSION migrated["health"] = MAX_HEALTH migrated["max_health"] = MAX_HEALTH migrated["downed"] = false migrated["downed_ticks"] = 0 migrated["dead"] = false migrated["deaths"] = 0 migrated["starvation_ticks"] = 0 migrated["starvation_death_threshold"] = DEFAULT_STARVATION_DEATH_THRESHOLD migrated["carry_capacity"] = DEFAULT_CARRY_CAPACITY return migrated func get_hunger() -> float: return clampf(float(data["hunger"]), 0.0, 100.0) func get_energy() -> float: return clampf(float(data["energy"]), 0.0, 100.0) func get_health() -> float: return clampf(float(data["health"]), 0.0, get_max_health()) func get_max_health() -> float: return maxf(float(data["max_health"]), 1.0) func is_downed() -> bool: return bool(data["downed"]) func is_dead() -> bool: return bool(data.get("dead", false)) func is_starving() -> bool: return get_hunger() >= STACK_THRESHOLD and not is_dead() func get_deaths() -> int: return maxi(int(data.get("deaths", 0)), 0) func get_downed_ticks() -> int: return int(data["downed_ticks"]) func set_hunger(amount: float) -> void: data["hunger"] = clampf(amount, 0.0, 100.0) func set_energy(amount: float) -> void: data["energy"] = clampf(amount, 0.0, 100.0) func advance(hunger_rate: float, energy_drain: float) -> bool: if is_dead(): return true set_hunger(get_hunger() + maxf(hunger_rate, 0.0)) set_energy(get_energy() - maxf(energy_drain, 0.0)) if get_hunger() < STACK_THRESHOLD: data["starvation_ticks"] = 0 return false data["starvation_ticks"] = int(data.get("starvation_ticks", 0)) + 1 if int(data["starvation_ticks"]) >= int(data.get("starvation_death_threshold", 1)): die() return true return false func eat(amount: float) -> float: if not is_finite(amount) or amount <= 0.0 or is_dead(): return 0.0 var consumed := minf(amount, get_hunger()) set_hunger(get_hunger() - consumed) set_energy(get_energy() + consumed * 0.5) data["starvation_ticks"] = 0 return consumed func die() -> void: if is_dead(): return data["dead"] = true data["deaths"] = get_deaths() + 1 func respawn() -> void: data["dead"] = false data["downed"] = false data["downed_ticks"] = 0 data["hunger"] = RESET_HUNGER data["energy"] = RESET_ENERGY data["health"] = get_max_health() data["starvation_ticks"] = 0 func get_available_carry() -> float: return maxf( float(data.get("carry_capacity", DEFAULT_CARRY_CAPACITY)) - get_carried_total(), 0.0 ) func take_damage(amount: float) -> float: if not is_finite(amount) or amount <= 0.0 or is_downed(): return 0.0 var previous := get_health() data["health"] = clampf(previous - amount, 0.0, get_max_health()) if get_health() <= 0.0: data["downed"] = true data["downed_ticks"] = 20 return previous - get_health() func advance_health() -> void: if is_downed(): data["downed_ticks"] = maxi(get_downed_ticks() - 1, 0) if get_downed_ticks() <= 0: data["downed"] = false data["health"] = get_max_health() * 0.5 return if get_health() < get_max_health(): data["health"] = clampf(get_health() + 0.25, 0.0, get_max_health()) func get_inventory_amount(item_id: StringName) -> float: return float(data["inventory"].get(String(item_id), 0.0)) func get_carried_total() -> float: var total := 0.0 for amount in data["inventory"].values(): total += float(amount) return total func add_inventory(item_id: StringName, amount: float) -> float: if item_id.is_empty() or not is_finite(amount) or amount <= 0.0: return 0.0 var added := minf(amount, get_available_carry()) if added <= 0.0: return 0.0 data["inventory"][String(item_id)] = get_inventory_amount(item_id) + added return added func remove_inventory(item_id: StringName, requested_amount: float) -> float: if item_id.is_empty() or not is_finite(requested_amount) or requested_amount <= 0.0: return 0.0 var removed := minf(requested_amount, get_inventory_amount(item_id)) if removed <= 0.0: return 0.0 data["inventory"][String(item_id)] = get_inventory_amount(item_id) - removed return removed func to_dictionary() -> Dictionary: return data.duplicate(true)