Files
2026-08-16 16:53:30 +02:00

279 lines
7.3 KiB
GDScript

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)