feat: give the player embodied hunger, energy, and starvation death
This commit is contained in:
+35
-1
@@ -37,7 +37,7 @@ func _physics_process(delta: float) -> void:
|
|||||||
|
|
||||||
var direction := (right * input.x + forward * -input.y).normalized()
|
var direction := (right * input.x + forward * -input.y).normalized()
|
||||||
|
|
||||||
var target_velocity := direction * move_speed
|
var target_velocity := direction * move_speed * _needs_speed_factor()
|
||||||
|
|
||||||
velocity.x = move_toward(velocity.x, target_velocity.x, acceleration * delta)
|
velocity.x = move_toward(velocity.x, target_velocity.x, acceleration * delta)
|
||||||
velocity.z = move_toward(velocity.z, target_velocity.z, acceleration * delta)
|
velocity.z = move_toward(velocity.z, target_velocity.z, acceleration * delta)
|
||||||
@@ -373,11 +373,45 @@ func _execute_pantry_interaction(context: PlayerInteractionResult) -> void:
|
|||||||
var food_before := _get_pantry_food()
|
var food_before := _get_pantry_food()
|
||||||
simulation_manager.eat_food(stomach_capacity_for_food)
|
simulation_manager.eat_food(stomach_capacity_for_food)
|
||||||
var consumed := food_before - _get_pantry_food()
|
var consumed := food_before - _get_pantry_food()
|
||||||
|
var player_hunger := _get_player_hunger()
|
||||||
|
if consumed > 0.0 and player_hunger > 0.0:
|
||||||
|
simulation_manager.player_eat(consumed)
|
||||||
interaction_feedback.emit(
|
interaction_feedback.emit(
|
||||||
"Ate from the pantry", "%.0f village food consumed." % consumed, consumed > 0.0
|
"Ate from the pantry", "%.0f village food consumed." % consumed, consumed > 0.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _get_player_hunger() -> float:
|
||||||
|
if simulation_manager == null or not simulation_manager.has_method("get_player_state"):
|
||||||
|
return 0.0
|
||||||
|
var state: PlayerStateRecord = simulation_manager.get_player_state()
|
||||||
|
return state.get_hunger() if state != null else 0.0
|
||||||
|
|
||||||
|
|
||||||
|
func is_starving() -> bool:
|
||||||
|
if simulation_manager == null or not simulation_manager.has_method("get_player_state"):
|
||||||
|
return false
|
||||||
|
var state: PlayerStateRecord = simulation_manager.get_player_state()
|
||||||
|
return state.is_starving() if state != null else false
|
||||||
|
|
||||||
|
|
||||||
|
func _needs_speed_factor() -> float:
|
||||||
|
if simulation_manager == null or not simulation_manager.has_method("get_player_state"):
|
||||||
|
return 1.0
|
||||||
|
var state: PlayerStateRecord = simulation_manager.get_player_state()
|
||||||
|
if state == null or state.is_dead():
|
||||||
|
return 0.0
|
||||||
|
var factor := 1.0
|
||||||
|
if state.is_starving():
|
||||||
|
factor *= 0.5
|
||||||
|
var energy := state.get_energy()
|
||||||
|
if energy < 20.0:
|
||||||
|
factor *= 0.6
|
||||||
|
elif energy < 45.0:
|
||||||
|
factor *= 0.8
|
||||||
|
return factor
|
||||||
|
|
||||||
|
|
||||||
func _get_animal_feed_cost() -> float:
|
func _get_animal_feed_cost() -> float:
|
||||||
var definition := SimulationDefinitions.get_action(SimulationIds.ACTION_FEED_ANIMAL)
|
var definition := SimulationDefinitions.get_action(SimulationIds.ACTION_FEED_ANIMAL)
|
||||||
return definition.completion_cost_amount if definition != null else 1.0
|
return definition.completion_cost_amount if definition != null else 1.0
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ signal player_quest_completed(quest: PlayerQuestRecord)
|
|||||||
signal player_quest_expired(quest: PlayerQuestRecord)
|
signal player_quest_expired(quest: PlayerQuestRecord)
|
||||||
signal player_standing_changed(standing: PlayerStandingRecord)
|
signal player_standing_changed(standing: PlayerStandingRecord)
|
||||||
signal player_tier_advanced(tier: int, tier_name: String)
|
signal player_tier_advanced(tier: int, tier_name: String)
|
||||||
|
signal player_needs_changed(state: PlayerStateRecord)
|
||||||
|
signal player_died(state: PlayerStateRecord)
|
||||||
|
|
||||||
var village := SimVillage.new()
|
var village := SimVillage.new()
|
||||||
|
|
||||||
@@ -52,6 +54,7 @@ var relationship_system := RelationshipSystemScript.new()
|
|||||||
var event_knowledge_system := EventKnowledgeSystemScript.new()
|
var event_knowledge_system := EventKnowledgeSystemScript.new()
|
||||||
var opportunity_system := VillageOpportunitySystem.new()
|
var opportunity_system := VillageOpportunitySystem.new()
|
||||||
var player_quest_system := PlayerQuestSystem.new()
|
var player_quest_system := PlayerQuestSystem.new()
|
||||||
|
var player_needs := PlayerNeedsSystem.new()
|
||||||
var _last_player_tier := PlayerStandingRecord.TIER_STRANGER
|
var _last_player_tier := PlayerStandingRecord.TIER_STRANGER
|
||||||
var storage_states: Dictionary:
|
var storage_states: Dictionary:
|
||||||
get:
|
get:
|
||||||
@@ -176,6 +179,7 @@ func simulate_tick() -> void:
|
|||||||
print("--- Tick ", tick_count, " ---")
|
print("--- Tick ", tick_count, " ---")
|
||||||
animal_care.advance(tick_count)
|
animal_care.advance(tick_count)
|
||||||
_consider_animal_care_quests()
|
_consider_animal_care_quests()
|
||||||
|
_advance_player_needs()
|
||||||
var village_was_changed := false
|
var village_was_changed := false
|
||||||
_population_view.rebuild(npcs)
|
_population_view.rebuild(npcs)
|
||||||
for npc in npcs:
|
for npc in npcs:
|
||||||
@@ -591,17 +595,13 @@ func _finish_quest_resolution(resolution: Dictionary) -> void:
|
|||||||
var quest: PlayerQuestRecord = resolution["quest"]
|
var quest: PlayerQuestRecord = resolution["quest"]
|
||||||
if resolution["completed"]:
|
if resolution["completed"]:
|
||||||
player_quest_completed.emit(quest)
|
player_quest_completed.emit(quest)
|
||||||
_emit_standing_change_and_tier()
|
|
||||||
else:
|
|
||||||
player_quest_expired.emit(quest)
|
|
||||||
|
|
||||||
|
|
||||||
func _emit_standing_change_and_tier() -> void:
|
|
||||||
var standing := player_quest_system.standing
|
var standing := player_quest_system.standing
|
||||||
player_standing_changed.emit(standing)
|
player_standing_changed.emit(standing)
|
||||||
if standing.get_tier() > _last_player_tier:
|
if standing.get_tier() > _last_player_tier:
|
||||||
_last_player_tier = standing.get_tier()
|
_last_player_tier = standing.get_tier()
|
||||||
player_tier_advanced.emit(standing.get_tier(), standing.get_tier_name())
|
player_tier_advanced.emit(standing.get_tier(), standing.get_tier_name())
|
||||||
|
else:
|
||||||
|
player_quest_expired.emit(quest)
|
||||||
|
|
||||||
|
|
||||||
func try_communicate_at_shared_activity(speaker_id: int, listener_id: int) -> bool:
|
func try_communicate_at_shared_activity(speaker_id: int, listener_id: int) -> bool:
|
||||||
@@ -631,31 +631,8 @@ func try_communicate_at_shared_activity(speaker_id: int, listener_id: int) -> bo
|
|||||||
|
|
||||||
|
|
||||||
func _can_communicate_at_shared_activity(speaker: SimNPC, listener: SimNPC) -> bool:
|
func _can_communicate_at_shared_activity(speaker: SimNPC, listener: SimNPC) -> bool:
|
||||||
if speaker == null or listener == null or speaker.id == listener.id:
|
return EventKnowledgeSystem.can_communicate_at_shared_activity(
|
||||||
return false
|
speaker, listener, storage_states, KNOWLEDGE_COMMUNICATION_RADIUS
|
||||||
if speaker.is_dead or listener.is_dead:
|
|
||||||
return false
|
|
||||||
if (
|
|
||||||
speaker.task_state != SimNPC.TASK_STATE_WORKING
|
|
||||||
or listener.task_state != SimNPC.TASK_STATE_WORKING
|
|
||||||
):
|
|
||||||
return false
|
|
||||||
if speaker.target_id.is_empty() or speaker.target_id != listener.target_id:
|
|
||||||
return false
|
|
||||||
if storage_states.has(speaker.target_id):
|
|
||||||
return false
|
|
||||||
var speaker_action := SimulationDefinitions.get_action(speaker.current_task)
|
|
||||||
var listener_action := SimulationDefinitions.get_action(listener.current_task)
|
|
||||||
if (
|
|
||||||
speaker_action == null
|
|
||||||
or listener_action == null
|
|
||||||
or speaker_action.target_type != SimulationIds.TARGET_ACTIVITY
|
|
||||||
or listener_action.target_type != SimulationIds.TARGET_ACTIVITY
|
|
||||||
):
|
|
||||||
return false
|
|
||||||
return (
|
|
||||||
speaker.position.distance_squared_to(listener.position)
|
|
||||||
<= KNOWLEDGE_COMMUNICATION_RADIUS * KNOWLEDGE_COMMUNICATION_RADIUS
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -713,33 +690,9 @@ func _maintain_event_knowledge(expire_by_age: bool) -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _get_lasting_knowledge_records() -> Array[KnownEventStateRecord]:
|
func _get_lasting_knowledge_records() -> Array[KnownEventStateRecord]:
|
||||||
var lasting: Array[KnownEventStateRecord] = []
|
return event_knowledge_system.collect_lasting_records(
|
||||||
var seen := {}
|
relationship_system.get_all_sorted(), opportunity_system.get_open_opportunity()
|
||||||
for relationship in relationship_system.get_all_sorted():
|
|
||||||
var event_id := relationship.get_last_trust_cause_event_id()
|
|
||||||
if event_id == RelationshipStateRecord.NO_CAUSE_EVENT:
|
|
||||||
continue
|
|
||||||
var record := event_knowledge_system.get_record(relationship.get_observer_id(), event_id)
|
|
||||||
if record == null:
|
|
||||||
continue
|
|
||||||
var key := "%d:%d" % [record.get_knower_id(), record.get_event_id()]
|
|
||||||
if seen.has(key):
|
|
||||||
continue
|
|
||||||
seen[key] = true
|
|
||||||
lasting.append(record)
|
|
||||||
var open_opportunity: OpportunityStateRecord = opportunity_system.get_open_opportunity()
|
|
||||||
if open_opportunity != null:
|
|
||||||
var opportunity_record := event_knowledge_system.get_record(
|
|
||||||
open_opportunity.get_interested_npc_id(), open_opportunity.get_trigger_event_id()
|
|
||||||
)
|
)
|
||||||
if opportunity_record != null:
|
|
||||||
var opportunity_key := (
|
|
||||||
"%d:%d" % [opportunity_record.get_knower_id(), opportunity_record.get_event_id()]
|
|
||||||
)
|
|
||||||
if not seen.has(opportunity_key):
|
|
||||||
seen[opportunity_key] = true
|
|
||||||
lasting.append(opportunity_record)
|
|
||||||
return lasting
|
|
||||||
|
|
||||||
|
|
||||||
func _get_lasting_event_ids(npc_id: int) -> Array[int]:
|
func _get_lasting_event_ids(npc_id: int) -> Array[int]:
|
||||||
@@ -788,6 +741,36 @@ func get_active_player_quest() -> PlayerQuestRecord:
|
|||||||
return player_quest_system.get_active_quest()
|
return player_quest_system.get_active_quest()
|
||||||
|
|
||||||
|
|
||||||
|
func _advance_player_needs() -> void:
|
||||||
|
if not player_needs.advance(village):
|
||||||
|
player_needs_changed.emit(player_needs.state)
|
||||||
|
return
|
||||||
|
player_died.emit(player_needs.state)
|
||||||
|
player_needs.apply_death_consequence(player_quest_system.standing)
|
||||||
|
_last_player_tier = player_quest_system.standing.get_tier()
|
||||||
|
player_standing_changed.emit(player_quest_system.standing)
|
||||||
|
|
||||||
|
|
||||||
|
func respawn_player() -> bool:
|
||||||
|
var was_dead := player_needs.state.is_dead()
|
||||||
|
player_needs.respawn()
|
||||||
|
if was_dead and not player_needs.state.is_dead():
|
||||||
|
player_needs_changed.emit(player_needs.state)
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
func get_player_state() -> PlayerStateRecord:
|
||||||
|
return player_needs.state
|
||||||
|
|
||||||
|
|
||||||
|
func player_eat(amount: float) -> float:
|
||||||
|
var eaten := player_needs.eat(amount)
|
||||||
|
if eaten > 0.0:
|
||||||
|
player_needs_changed.emit(player_needs.state)
|
||||||
|
return eaten
|
||||||
|
|
||||||
|
|
||||||
func _consider_animal_care_quests() -> void:
|
func _consider_animal_care_quests() -> void:
|
||||||
var pantry: StorageStateRecord = get_pantry()
|
var pantry: StorageStateRecord = get_pantry()
|
||||||
for animal_state in animal_care.get_all_states():
|
for animal_state in animal_care.get_all_states():
|
||||||
@@ -1141,6 +1124,7 @@ func create_state_record() -> SimulationStateRecord:
|
|||||||
record.player_standing = player_quest_system.standing
|
record.player_standing = player_quest_system.standing
|
||||||
for quest in player_quest_system.get_all_sorted():
|
for quest in player_quest_system.get_all_sorted():
|
||||||
record.player_quests.append(quest)
|
record.player_quests.append(quest)
|
||||||
|
record.player_state = player_needs.state
|
||||||
return record
|
return record
|
||||||
|
|
||||||
|
|
||||||
@@ -1182,6 +1166,7 @@ func restore_state(record: SimulationStateRecord) -> bool:
|
|||||||
record.player_quests, int(record.simulation.get("next_quest_id", 0)), record.player_standing
|
record.player_quests, int(record.simulation.get("next_quest_id", 0)), record.player_standing
|
||||||
)
|
)
|
||||||
_last_player_tier = player_quest_system.standing.get_tier()
|
_last_player_tier = player_quest_system.standing.get_tier()
|
||||||
|
player_needs.restore(record.player_state)
|
||||||
_maintain_event_knowledge(tick_count % get_knowledge_review_interval() == 0)
|
_maintain_event_knowledge(tick_count % get_knowledge_review_interval() == 0)
|
||||||
wander_random_sources.clear()
|
wander_random_sources.clear()
|
||||||
var wander_streams: Array = record.simulation["wander_random_streams"]
|
var wander_streams: Array = record.simulation["wander_random_streams"]
|
||||||
|
|||||||
@@ -204,6 +204,65 @@ func restore(records: Array[KnownEventStateRecord]) -> void:
|
|||||||
known_events[_key(record.get_knower_id(), record.get_event_id())] = record
|
known_events[_key(record.get_knower_id(), record.get_event_id())] = record
|
||||||
|
|
||||||
|
|
||||||
|
static func can_communicate_at_shared_activity(
|
||||||
|
speaker: SimNPC, listener: SimNPC, storage_states: Dictionary, radius: float
|
||||||
|
) -> bool:
|
||||||
|
if speaker == null or listener == null or speaker.id == listener.id:
|
||||||
|
return false
|
||||||
|
if speaker.is_dead or listener.is_dead:
|
||||||
|
return false
|
||||||
|
if (
|
||||||
|
speaker.task_state != SimNPC.TASK_STATE_WORKING
|
||||||
|
or listener.task_state != SimNPC.TASK_STATE_WORKING
|
||||||
|
):
|
||||||
|
return false
|
||||||
|
if speaker.target_id.is_empty() or speaker.target_id != listener.target_id:
|
||||||
|
return false
|
||||||
|
if storage_states.has(speaker.target_id):
|
||||||
|
return false
|
||||||
|
var speaker_action := SimulationDefinitions.get_action(speaker.current_task)
|
||||||
|
var listener_action := SimulationDefinitions.get_action(listener.current_task)
|
||||||
|
if (
|
||||||
|
speaker_action == null
|
||||||
|
or listener_action == null
|
||||||
|
or speaker_action.target_type != SimulationIds.TARGET_ACTIVITY
|
||||||
|
or listener_action.target_type != SimulationIds.TARGET_ACTIVITY
|
||||||
|
):
|
||||||
|
return false
|
||||||
|
return speaker.position.distance_squared_to(listener.position) <= radius * radius
|
||||||
|
|
||||||
|
|
||||||
|
func collect_lasting_records(
|
||||||
|
relationships: Array[RelationshipStateRecord], open_opportunity: OpportunityStateRecord
|
||||||
|
) -> Array[KnownEventStateRecord]:
|
||||||
|
var lasting: Array[KnownEventStateRecord] = []
|
||||||
|
var seen := {}
|
||||||
|
for relationship in relationships:
|
||||||
|
var event_id := relationship.get_last_trust_cause_event_id()
|
||||||
|
if event_id == RelationshipStateRecord.NO_CAUSE_EVENT:
|
||||||
|
continue
|
||||||
|
var record := get_record(relationship.get_observer_id(), event_id)
|
||||||
|
if record == null:
|
||||||
|
continue
|
||||||
|
var key := "%d:%d" % [record.get_knower_id(), record.get_event_id()]
|
||||||
|
if seen.has(key):
|
||||||
|
continue
|
||||||
|
seen[key] = true
|
||||||
|
lasting.append(record)
|
||||||
|
if open_opportunity != null:
|
||||||
|
var opportunity_record := get_record(
|
||||||
|
open_opportunity.get_interested_npc_id(), open_opportunity.get_trigger_event_id()
|
||||||
|
)
|
||||||
|
if opportunity_record != null:
|
||||||
|
var opportunity_key := (
|
||||||
|
"%d:%d" % [opportunity_record.get_knower_id(), opportunity_record.get_event_id()]
|
||||||
|
)
|
||||||
|
if not seen.has(opportunity_key):
|
||||||
|
seen[opportunity_key] = true
|
||||||
|
lasting.append(opportunity_record)
|
||||||
|
return lasting
|
||||||
|
|
||||||
|
|
||||||
func _remember(
|
func _remember(
|
||||||
knower_id: int,
|
knower_id: int,
|
||||||
event_id: int,
|
event_id: int,
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
class_name PlayerNeedsSystem
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
var state := PlayerStateRecord.create()
|
||||||
|
|
||||||
|
|
||||||
|
func advance(village: SimVillage) -> bool:
|
||||||
|
if state.is_dead():
|
||||||
|
return false
|
||||||
|
var hunger_rate := 0.125 * village.food_modifier
|
||||||
|
if state.advance(hunger_rate, 0.5):
|
||||||
|
state.die()
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
func eat(amount: float) -> float:
|
||||||
|
var eaten := state.eat(amount)
|
||||||
|
return eaten
|
||||||
|
|
||||||
|
|
||||||
|
func respawn() -> void:
|
||||||
|
if state.is_dead():
|
||||||
|
state.respawn()
|
||||||
|
|
||||||
|
|
||||||
|
func apply_death_consequence(standing: PlayerStandingRecord) -> void:
|
||||||
|
var reduced := standing.get_standing() * PlayerStateRecord.DEATH_STANDING_PENALTY
|
||||||
|
standing.data["standing"] = reduced
|
||||||
|
|
||||||
|
|
||||||
|
func restore(record: PlayerStateRecord) -> void:
|
||||||
|
state = record if record != null else PlayerStateRecord.create()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://brf3a7drbfs7
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
class_name PlayerStateRecord
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
const SCHEMA_VERSION := 1
|
||||||
|
const HUNGER_DEATH_THRESHOLD := 100.0
|
||||||
|
const STACK_THRESHOLD := 90.0
|
||||||
|
const DEATH_STANDING_PENALTY := 0.5
|
||||||
|
const RESET_HUNGER := 20.0
|
||||||
|
const RESET_ENERGY := 70.0
|
||||||
|
|
||||||
|
var data: Dictionary
|
||||||
|
|
||||||
|
|
||||||
|
func _init(record_data: Dictionary = {}) -> void:
|
||||||
|
data = record_data.duplicate(true)
|
||||||
|
|
||||||
|
|
||||||
|
static func create() -> PlayerStateRecord:
|
||||||
|
return (
|
||||||
|
PlayerStateRecord
|
||||||
|
. new(
|
||||||
|
{
|
||||||
|
"schema_version": SCHEMA_VERSION,
|
||||||
|
"hunger": 20.0,
|
||||||
|
"energy": 90.0,
|
||||||
|
"is_starving": false,
|
||||||
|
"starvation_ticks": 0,
|
||||||
|
"starvation_death_threshold": 600,
|
||||||
|
"is_dead": false,
|
||||||
|
"deaths": 0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
static func from_dictionary(record_data: Dictionary) -> PlayerStateRecord:
|
||||||
|
if int(record_data.get("schema_version", -1)) != SCHEMA_VERSION:
|
||||||
|
return null
|
||||||
|
if not (
|
||||||
|
record_data
|
||||||
|
. has_all(
|
||||||
|
[
|
||||||
|
"hunger",
|
||||||
|
"energy",
|
||||||
|
"is_starving",
|
||||||
|
"starvation_ticks",
|
||||||
|
"starvation_death_threshold",
|
||||||
|
"is_dead",
|
||||||
|
"deaths",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
):
|
||||||
|
return null
|
||||||
|
var hunger := float(record_data["hunger"])
|
||||||
|
var energy := float(record_data["energy"])
|
||||||
|
var starvation_ticks := int(record_data["starvation_ticks"])
|
||||||
|
var death_threshold := int(record_data["starvation_death_threshold"])
|
||||||
|
var deaths := int(record_data["deaths"])
|
||||||
|
if (
|
||||||
|
not is_finite(hunger)
|
||||||
|
or hunger < 0.0
|
||||||
|
or hunger > HUNGER_DEATH_THRESHOLD
|
||||||
|
or not is_finite(energy)
|
||||||
|
or energy < 0.0
|
||||||
|
or energy > 100.0
|
||||||
|
or starvation_ticks < 0
|
||||||
|
or death_threshold < 1
|
||||||
|
or deaths < 0
|
||||||
|
):
|
||||||
|
return null
|
||||||
|
return (
|
||||||
|
PlayerStateRecord
|
||||||
|
. new(
|
||||||
|
{
|
||||||
|
"schema_version": SCHEMA_VERSION,
|
||||||
|
"hunger": hunger,
|
||||||
|
"energy": energy,
|
||||||
|
"is_starving": bool(record_data["is_starving"]),
|
||||||
|
"starvation_ticks": starvation_ticks,
|
||||||
|
"starvation_death_threshold": death_threshold,
|
||||||
|
"is_dead": bool(record_data["is_dead"]),
|
||||||
|
"deaths": deaths,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func get_hunger() -> float:
|
||||||
|
return float(data["hunger"])
|
||||||
|
|
||||||
|
|
||||||
|
func get_energy() -> float:
|
||||||
|
return float(data["energy"])
|
||||||
|
|
||||||
|
|
||||||
|
func is_starving() -> bool:
|
||||||
|
return bool(data["is_starving"])
|
||||||
|
|
||||||
|
|
||||||
|
func is_dead() -> bool:
|
||||||
|
return bool(data["is_dead"])
|
||||||
|
|
||||||
|
|
||||||
|
func get_starvation_ticks() -> int:
|
||||||
|
return int(data["starvation_ticks"])
|
||||||
|
|
||||||
|
|
||||||
|
func get_deaths() -> int:
|
||||||
|
return int(data["deaths"])
|
||||||
|
|
||||||
|
|
||||||
|
func eat(amount: float) -> float:
|
||||||
|
if amount <= 0.0 or is_dead():
|
||||||
|
return 0.0
|
||||||
|
var previous := get_hunger()
|
||||||
|
data["hunger"] = clampf(previous - amount, 0.0, HUNGER_DEATH_THRESHOLD)
|
||||||
|
var consumed := previous - get_hunger()
|
||||||
|
if consumed > 0.0:
|
||||||
|
data["energy"] = minf(get_energy() + consumed * 0.5, 100.0)
|
||||||
|
return consumed
|
||||||
|
|
||||||
|
|
||||||
|
func advance(hunger_rate: float, energy_rate: float) -> bool:
|
||||||
|
if is_dead():
|
||||||
|
return false
|
||||||
|
data["hunger"] = minf(get_hunger() + maxf(hunger_rate, 0.0), HUNGER_DEATH_THRESHOLD)
|
||||||
|
data["energy"] = clampf(get_energy() - maxf(energy_rate, 0.0), 0.0, 100.0)
|
||||||
|
data["is_starving"] = get_hunger() >= STACK_THRESHOLD
|
||||||
|
if is_starving():
|
||||||
|
data["starvation_ticks"] = int(data["starvation_ticks"]) + 1
|
||||||
|
if get_starvation_ticks() >= int(data["starvation_death_threshold"]):
|
||||||
|
return true
|
||||||
|
else:
|
||||||
|
data["starvation_ticks"] = 0
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
func die() -> void:
|
||||||
|
data["is_dead"] = true
|
||||||
|
data["deaths"] = int(data["deaths"]) + 1
|
||||||
|
|
||||||
|
|
||||||
|
func respawn() -> void:
|
||||||
|
data["hunger"] = RESET_HUNGER
|
||||||
|
data["energy"] = RESET_ENERGY
|
||||||
|
data["is_starving"] = false
|
||||||
|
data["starvation_ticks"] = 0
|
||||||
|
data["is_dead"] = false
|
||||||
|
|
||||||
|
|
||||||
|
func to_dictionary() -> Dictionary:
|
||||||
|
return data.duplicate(true)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://v86fryuks1u3
|
||||||
@@ -2,7 +2,7 @@ class_name SimulationStateRecord
|
|||||||
extends RefCounted
|
extends RefCounted
|
||||||
|
|
||||||
const SCHEMA_NAME := "the_steward.simulation"
|
const SCHEMA_NAME := "the_steward.simulation"
|
||||||
const SCHEMA_VERSION := 12
|
const SCHEMA_VERSION := 13
|
||||||
const LEGACY_SCHEMA_VERSION := 1
|
const LEGACY_SCHEMA_VERSION := 1
|
||||||
const EVENT_LEGACY_SCHEMA_VERSION := 2
|
const EVENT_LEGACY_SCHEMA_VERSION := 2
|
||||||
const RELATIONSHIP_LEGACY_SCHEMA_VERSION := 3
|
const RELATIONSHIP_LEGACY_SCHEMA_VERSION := 3
|
||||||
@@ -14,6 +14,7 @@ const ANIMAL_LEGACY_SCHEMA_VERSION := 9
|
|||||||
const ROUTINE_LEGACY_SCHEMA_VERSION := 10
|
const ROUTINE_LEGACY_SCHEMA_VERSION := 10
|
||||||
const PREVIOUS_SCHEMA_VERSION := 7
|
const PREVIOUS_SCHEMA_VERSION := 7
|
||||||
const PLAYER_LEGACY_SCHEMA_VERSION := 11
|
const PLAYER_LEGACY_SCHEMA_VERSION := 11
|
||||||
|
const PLAYER_STATE_LEGACY_SCHEMA_VERSION := 12
|
||||||
|
|
||||||
var simulation: Dictionary
|
var simulation: Dictionary
|
||||||
var village: VillageStateRecord
|
var village: VillageStateRecord
|
||||||
@@ -27,6 +28,7 @@ var event_knowledge: Array[KnownEventStateRecord] = []
|
|||||||
var opportunities: Array[OpportunityStateRecord] = []
|
var opportunities: Array[OpportunityStateRecord] = []
|
||||||
var player_standing: PlayerStandingRecord
|
var player_standing: PlayerStandingRecord
|
||||||
var player_quests: Array[PlayerQuestRecord] = []
|
var player_quests: Array[PlayerQuestRecord] = []
|
||||||
|
var player_state: PlayerStateRecord
|
||||||
|
|
||||||
|
|
||||||
func to_dictionary() -> Dictionary:
|
func to_dictionary() -> Dictionary:
|
||||||
@@ -73,7 +75,8 @@ func to_dictionary() -> Dictionary:
|
|||||||
"event_knowledge": knowledge_data,
|
"event_knowledge": knowledge_data,
|
||||||
"opportunities": opportunity_data,
|
"opportunities": opportunity_data,
|
||||||
"player_standing": player_standing.to_dictionary(),
|
"player_standing": player_standing.to_dictionary(),
|
||||||
"player_quests": player_quest_data
|
"player_quests": player_quest_data,
|
||||||
|
"player_state": player_state.to_dictionary()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -106,6 +109,7 @@ static func from_dictionary(record_data: Dictionary) -> SimulationStateRecord:
|
|||||||
ANIMAL_LEGACY_SCHEMA_VERSION,
|
ANIMAL_LEGACY_SCHEMA_VERSION,
|
||||||
ROUTINE_LEGACY_SCHEMA_VERSION,
|
ROUTINE_LEGACY_SCHEMA_VERSION,
|
||||||
PLAYER_LEGACY_SCHEMA_VERSION,
|
PLAYER_LEGACY_SCHEMA_VERSION,
|
||||||
|
PLAYER_STATE_LEGACY_SCHEMA_VERSION,
|
||||||
]
|
]
|
||||||
):
|
):
|
||||||
record_data = _migrate_legacy(record_data, version)
|
record_data = _migrate_legacy(record_data, version)
|
||||||
@@ -127,6 +131,7 @@ static func from_dictionary(record_data: Dictionary) -> SimulationStateRecord:
|
|||||||
"opportunities",
|
"opportunities",
|
||||||
"player_standing",
|
"player_standing",
|
||||||
"player_quests",
|
"player_quests",
|
||||||
|
"player_state",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
@@ -530,6 +535,14 @@ static func from_dictionary(record_data: Dictionary) -> SimulationStateRecord:
|
|||||||
highest_quest_id = maxi(highest_quest_id, quest_id)
|
highest_quest_id = maxi(highest_quest_id, quest_id)
|
||||||
record.player_quests.append(quest_record)
|
record.player_quests.append(quest_record)
|
||||||
|
|
||||||
|
var player_state_data = record_data["player_state"]
|
||||||
|
if not player_state_data is Dictionary:
|
||||||
|
return null
|
||||||
|
var player_state_record := PlayerStateRecord.from_dictionary(player_state_data)
|
||||||
|
if player_state_record == null:
|
||||||
|
return null
|
||||||
|
record.player_state = player_state_record
|
||||||
|
|
||||||
return record
|
return record
|
||||||
|
|
||||||
|
|
||||||
@@ -850,6 +863,8 @@ static func _migrate_legacy(legacy_data: Dictionary, version: int) -> Dictionary
|
|||||||
var quest_simulation_data: Dictionary = migrated.get("simulation", {})
|
var quest_simulation_data: Dictionary = migrated.get("simulation", {})
|
||||||
quest_simulation_data["next_quest_id"] = 0
|
quest_simulation_data["next_quest_id"] = 0
|
||||||
migrated["simulation"] = quest_simulation_data
|
migrated["simulation"] = quest_simulation_data
|
||||||
|
if version <= PLAYER_STATE_LEGACY_SCHEMA_VERSION:
|
||||||
|
migrated["player_state"] = PlayerStateRecord.create().to_dictionary()
|
||||||
return migrated
|
return migrated
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
var failures: Array[String] = []
|
||||||
|
var died_events := 0
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
call_deferred("_run")
|
||||||
|
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
var manager := _create_manager()
|
||||||
|
manager.player_died.connect(_on_player_died)
|
||||||
|
|
||||||
|
var state: PlayerStateRecord = manager.get_player_state()
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
state != null
|
||||||
|
and is_equal_approx(state.get_hunger(), 20.0)
|
||||||
|
and is_equal_approx(state.get_energy(), 90.0)
|
||||||
|
and not state.is_dead()
|
||||||
|
),
|
||||||
|
"A fresh player should start with embodied needs and no death state",
|
||||||
|
)
|
||||||
|
|
||||||
|
for _tick in 3:
|
||||||
|
manager.simulate_tick()
|
||||||
|
_check(
|
||||||
|
state.get_hunger() > 20.0 and state.get_energy() < 90.0,
|
||||||
|
"Simulation ticks should advance the player's hunger and drain energy",
|
||||||
|
)
|
||||||
|
|
||||||
|
state.data["hunger"] = 80.0
|
||||||
|
var hunger_before_eat: float = state.get_hunger()
|
||||||
|
var eaten: float = manager.player_eat(10.0)
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
is_equal_approx(eaten, 10.0)
|
||||||
|
and state.get_hunger() < hunger_before_eat
|
||||||
|
and state.get_energy() > 80.0
|
||||||
|
),
|
||||||
|
"Eating should reduce the player's own hunger and restore energy",
|
||||||
|
)
|
||||||
|
|
||||||
|
var initial_standing: float = manager.get_player_standing().get_standing()
|
||||||
|
manager.get_player_standing().grant_standing(20.0, 1)
|
||||||
|
var standing_after_help: float = manager.get_player_standing().get_standing()
|
||||||
|
state.data["hunger"] = PlayerStateRecord.STACK_THRESHOLD
|
||||||
|
state.data["starvation_ticks"] = (int(state.data["starvation_death_threshold"]) - 1)
|
||||||
|
manager.simulate_tick()
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
died_events == 1
|
||||||
|
and state.is_dead()
|
||||||
|
and is_equal_approx(
|
||||||
|
manager.get_player_standing().get_standing(),
|
||||||
|
standing_after_help * PlayerStateRecord.DEATH_STANDING_PENALTY
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"Starvation death should emit a death event and cut standing as a consequence",
|
||||||
|
)
|
||||||
|
|
||||||
|
var saved_json: String = manager.serialize_state()
|
||||||
|
var restored := _create_manager(902)
|
||||||
|
_check(
|
||||||
|
restored.restore_state_from_json(saved_json),
|
||||||
|
"The dead player state should restore through the current schema"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
restored.get_state_checksum() == manager.get_state_checksum()
|
||||||
|
and restored.get_player_state().is_dead()
|
||||||
|
and restored.get_player_state().get_deaths() == 1
|
||||||
|
),
|
||||||
|
"Restore should preserve the embodied death state and checksum",
|
||||||
|
)
|
||||||
|
|
||||||
|
_check(manager.respawn_player(), "A dead player should be able to respawn")
|
||||||
|
_check(
|
||||||
|
(
|
||||||
|
not state.is_dead()
|
||||||
|
and is_equal_approx(state.get_hunger(), PlayerStateRecord.RESET_HUNGER)
|
||||||
|
and is_equal_approx(state.get_energy(), PlayerStateRecord.RESET_ENERGY)
|
||||||
|
),
|
||||||
|
"Respawn should reset needs to the documented baseline",
|
||||||
|
)
|
||||||
|
|
||||||
|
manager.free()
|
||||||
|
restored.free()
|
||||||
|
_finish()
|
||||||
|
|
||||||
|
|
||||||
|
func _create_manager(seed_value: int = 901) -> Node:
|
||||||
|
var manager: Node = load("res://simulation/SimulationManager.gd").new()
|
||||||
|
manager.simulation_seed = seed_value
|
||||||
|
manager.debug_logs = false
|
||||||
|
var home_positions: Array[Vector3] = [
|
||||||
|
Vector3(0.0, 0.0, 0.0),
|
||||||
|
Vector3(2.0, 0.0, 0.0),
|
||||||
|
Vector3(10.0, 0.0, 0.0),
|
||||||
|
Vector3(12.0, 0.0, 0.0),
|
||||||
|
Vector3(30.0, 0.0, 30.0),
|
||||||
|
Vector3(40.0, 0.0, 40.0),
|
||||||
|
]
|
||||||
|
manager.home_positions = home_positions
|
||||||
|
root.add_child(manager)
|
||||||
|
manager.set_process(false)
|
||||||
|
return manager
|
||||||
|
|
||||||
|
|
||||||
|
func _on_player_died(_state: PlayerStateRecord) -> void:
|
||||||
|
died_events += 1
|
||||||
|
|
||||||
|
|
||||||
|
func _check(condition: bool, message: String) -> void:
|
||||||
|
if not condition:
|
||||||
|
failures.append(message)
|
||||||
|
|
||||||
|
|
||||||
|
func _finish() -> void:
|
||||||
|
if failures.is_empty():
|
||||||
|
print("[TEST] Embodied player needs passed")
|
||||||
|
quit(0)
|
||||||
|
return
|
||||||
|
for failure in failures:
|
||||||
|
push_error("[TEST] " + failure)
|
||||||
|
quit(1)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://jeax46jjayqj
|
||||||
+16
-1
@@ -87,6 +87,7 @@ func _on_village_changed(village: SimVillage) -> void:
|
|||||||
wood_rate = " (−%.0f/d)" % rates["wood_per_day"]
|
wood_rate = " (−%.0f/d)" % rates["wood_per_day"]
|
||||||
var opportunity_text := _build_active_opportunity_display()
|
var opportunity_text := _build_active_opportunity_display()
|
||||||
var standing_text := _build_standing_display()
|
var standing_text := _build_standing_display()
|
||||||
|
var player_needs_text := _build_player_needs_display()
|
||||||
village_stats_label.text = (
|
village_stats_label.text = (
|
||||||
"""
|
"""
|
||||||
Village [%s]
|
Village [%s]
|
||||||
@@ -100,7 +101,7 @@ func _on_village_changed(village: SimVillage) -> void:
|
|||||||
Food Mod: %.2f
|
Food Mod: %.2f
|
||||||
Safety Mod: %.2f
|
Safety Mod: %.2f
|
||||||
Knowledge Mod: %.2f
|
Knowledge Mod: %.2f
|
||||||
%s%s%s
|
%s%s%s%s
|
||||||
"""
|
"""
|
||||||
% [
|
% [
|
||||||
speed_text,
|
speed_text,
|
||||||
@@ -114,6 +115,7 @@ func _on_village_changed(village: SimVillage) -> void:
|
|||||||
village.food_modifier,
|
village.food_modifier,
|
||||||
village.safety_modifier,
|
village.safety_modifier,
|
||||||
village.knowledge_modifier,
|
village.knowledge_modifier,
|
||||||
|
player_needs_text,
|
||||||
standing_text,
|
standing_text,
|
||||||
opportunity_text,
|
opportunity_text,
|
||||||
event_text,
|
event_text,
|
||||||
@@ -121,6 +123,19 @@ func _on_village_changed(village: SimVillage) -> void:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _build_player_needs_display() -> String:
|
||||||
|
if not simulation_manager.has_method("get_player_state"):
|
||||||
|
return ""
|
||||||
|
var state: PlayerStateRecord = simulation_manager.get_player_state()
|
||||||
|
if state == null:
|
||||||
|
return ""
|
||||||
|
var status := "dead" if state.is_dead() else ("starving" if state.is_starving() else "ok")
|
||||||
|
return (
|
||||||
|
"\n\nYou\nHunger: %.0f Energy: %.0f (%s)"
|
||||||
|
% [state.get_hunger(), state.get_energy(), status]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func _build_standing_display() -> String:
|
func _build_standing_display() -> String:
|
||||||
if not simulation_manager.has_method("get_player_standing"):
|
if not simulation_manager.has_method("get_player_standing"):
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
Reference in New Issue
Block a user