diff --git a/player/player.gd b/player/player.gd index 2a6485d..67c0611 100644 --- a/player/player.gd +++ b/player/player.gd @@ -37,7 +37,7 @@ func _physics_process(delta: float) -> void: 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.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() simulation_manager.eat_food(stomach_capacity_for_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( "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: var definition := SimulationDefinitions.get_action(SimulationIds.ACTION_FEED_ANIMAL) return definition.completion_cost_amount if definition != null else 1.0 diff --git a/simulation/SimulationManager.gd b/simulation/SimulationManager.gd index f8293ad..fba2d12 100644 --- a/simulation/SimulationManager.gd +++ b/simulation/SimulationManager.gd @@ -29,6 +29,8 @@ signal player_quest_completed(quest: PlayerQuestRecord) signal player_quest_expired(quest: PlayerQuestRecord) signal player_standing_changed(standing: PlayerStandingRecord) signal player_tier_advanced(tier: int, tier_name: String) +signal player_needs_changed(state: PlayerStateRecord) +signal player_died(state: PlayerStateRecord) var village := SimVillage.new() @@ -52,6 +54,7 @@ var relationship_system := RelationshipSystemScript.new() var event_knowledge_system := EventKnowledgeSystemScript.new() var opportunity_system := VillageOpportunitySystem.new() var player_quest_system := PlayerQuestSystem.new() +var player_needs := PlayerNeedsSystem.new() var _last_player_tier := PlayerStandingRecord.TIER_STRANGER var storage_states: Dictionary: get: @@ -176,6 +179,7 @@ func simulate_tick() -> void: print("--- Tick ", tick_count, " ---") animal_care.advance(tick_count) _consider_animal_care_quests() + _advance_player_needs() var village_was_changed := false _population_view.rebuild(npcs) for npc in npcs: @@ -591,19 +595,15 @@ func _finish_quest_resolution(resolution: Dictionary) -> void: var quest: PlayerQuestRecord = resolution["quest"] if resolution["completed"]: player_quest_completed.emit(quest) - _emit_standing_change_and_tier() + var standing := player_quest_system.standing + player_standing_changed.emit(standing) + if standing.get_tier() > _last_player_tier: + _last_player_tier = standing.get_tier() + player_tier_advanced.emit(standing.get_tier(), standing.get_tier_name()) else: player_quest_expired.emit(quest) -func _emit_standing_change_and_tier() -> void: - var standing := player_quest_system.standing - player_standing_changed.emit(standing) - if standing.get_tier() > _last_player_tier: - _last_player_tier = standing.get_tier() - player_tier_advanced.emit(standing.get_tier(), standing.get_tier_name()) - - func try_communicate_at_shared_activity(speaker_id: int, listener_id: int) -> bool: var speaker := _find_npc_by_id(speaker_id) var listener := _find_npc_by_id(listener_id) @@ -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: - 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) - <= KNOWLEDGE_COMMUNICATION_RADIUS * KNOWLEDGE_COMMUNICATION_RADIUS + return EventKnowledgeSystem.can_communicate_at_shared_activity( + speaker, listener, storage_states, KNOWLEDGE_COMMUNICATION_RADIUS ) @@ -713,33 +690,9 @@ func _maintain_event_knowledge(expire_by_age: bool) -> void: func _get_lasting_knowledge_records() -> Array[KnownEventStateRecord]: - var lasting: Array[KnownEventStateRecord] = [] - var seen := {} - 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 + return event_knowledge_system.collect_lasting_records( + relationship_system.get_all_sorted(), opportunity_system.get_open_opportunity() + ) 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() +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: var pantry: StorageStateRecord = get_pantry() 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 for quest in player_quest_system.get_all_sorted(): record.player_quests.append(quest) + record.player_state = player_needs.state 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 ) _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) wander_random_sources.clear() var wander_streams: Array = record.simulation["wander_random_streams"] diff --git a/simulation/knowledge/EventKnowledgeSystem.gd b/simulation/knowledge/EventKnowledgeSystem.gd index e14a6a8..58fe0d3 100644 --- a/simulation/knowledge/EventKnowledgeSystem.gd +++ b/simulation/knowledge/EventKnowledgeSystem.gd @@ -204,6 +204,65 @@ func restore(records: Array[KnownEventStateRecord]) -> void: 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( knower_id: int, event_id: int, diff --git a/simulation/quests/PlayerNeedsSystem.gd b/simulation/quests/PlayerNeedsSystem.gd new file mode 100644 index 0000000..2dcf25c --- /dev/null +++ b/simulation/quests/PlayerNeedsSystem.gd @@ -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() diff --git a/simulation/quests/PlayerNeedsSystem.gd.uid b/simulation/quests/PlayerNeedsSystem.gd.uid new file mode 100644 index 0000000..410bb19 --- /dev/null +++ b/simulation/quests/PlayerNeedsSystem.gd.uid @@ -0,0 +1 @@ +uid://brf3a7drbfs7 diff --git a/simulation/state/PlayerStateRecord.gd b/simulation/state/PlayerStateRecord.gd new file mode 100644 index 0000000..02cf6b5 --- /dev/null +++ b/simulation/state/PlayerStateRecord.gd @@ -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) diff --git a/simulation/state/PlayerStateRecord.gd.uid b/simulation/state/PlayerStateRecord.gd.uid new file mode 100644 index 0000000..4e51238 --- /dev/null +++ b/simulation/state/PlayerStateRecord.gd.uid @@ -0,0 +1 @@ +uid://v86fryuks1u3 diff --git a/simulation/state/SimulationStateRecord.gd b/simulation/state/SimulationStateRecord.gd index 6b14d81..ef2b647 100644 --- a/simulation/state/SimulationStateRecord.gd +++ b/simulation/state/SimulationStateRecord.gd @@ -2,7 +2,7 @@ class_name SimulationStateRecord extends RefCounted const SCHEMA_NAME := "the_steward.simulation" -const SCHEMA_VERSION := 12 +const SCHEMA_VERSION := 13 const LEGACY_SCHEMA_VERSION := 1 const EVENT_LEGACY_SCHEMA_VERSION := 2 const RELATIONSHIP_LEGACY_SCHEMA_VERSION := 3 @@ -14,6 +14,7 @@ const ANIMAL_LEGACY_SCHEMA_VERSION := 9 const ROUTINE_LEGACY_SCHEMA_VERSION := 10 const PREVIOUS_SCHEMA_VERSION := 7 const PLAYER_LEGACY_SCHEMA_VERSION := 11 +const PLAYER_STATE_LEGACY_SCHEMA_VERSION := 12 var simulation: Dictionary var village: VillageStateRecord @@ -27,6 +28,7 @@ var event_knowledge: Array[KnownEventStateRecord] = [] var opportunities: Array[OpportunityStateRecord] = [] var player_standing: PlayerStandingRecord var player_quests: Array[PlayerQuestRecord] = [] +var player_state: PlayerStateRecord func to_dictionary() -> Dictionary: @@ -73,7 +75,8 @@ func to_dictionary() -> Dictionary: "event_knowledge": knowledge_data, "opportunities": opportunity_data, "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, ROUTINE_LEGACY_SCHEMA_VERSION, PLAYER_LEGACY_SCHEMA_VERSION, + PLAYER_STATE_LEGACY_SCHEMA_VERSION, ] ): record_data = _migrate_legacy(record_data, version) @@ -127,6 +131,7 @@ static func from_dictionary(record_data: Dictionary) -> SimulationStateRecord: "opportunities", "player_standing", "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) 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 @@ -850,6 +863,8 @@ static func _migrate_legacy(legacy_data: Dictionary, version: int) -> Dictionary var quest_simulation_data: Dictionary = migrated.get("simulation", {}) quest_simulation_data["next_quest_id"] = 0 migrated["simulation"] = quest_simulation_data + if version <= PLAYER_STATE_LEGACY_SCHEMA_VERSION: + migrated["player_state"] = PlayerStateRecord.create().to_dictionary() return migrated diff --git a/tests/player_needs_loop_test.gd b/tests/player_needs_loop_test.gd new file mode 100644 index 0000000..786b538 --- /dev/null +++ b/tests/player_needs_loop_test.gd @@ -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) diff --git a/tests/player_needs_loop_test.gd.uid b/tests/player_needs_loop_test.gd.uid new file mode 100644 index 0000000..0462580 --- /dev/null +++ b/tests/player_needs_loop_test.gd.uid @@ -0,0 +1 @@ +uid://jeax46jjayqj diff --git a/world/ui/ui.gd b/world/ui/ui.gd index 607161a..1343a9e 100644 --- a/world/ui/ui.gd +++ b/world/ui/ui.gd @@ -87,6 +87,7 @@ func _on_village_changed(village: SimVillage) -> void: wood_rate = " (−%.0f/d)" % rates["wood_per_day"] var opportunity_text := _build_active_opportunity_display() var standing_text := _build_standing_display() + var player_needs_text := _build_player_needs_display() village_stats_label.text = ( """ Village [%s] @@ -100,7 +101,7 @@ func _on_village_changed(village: SimVillage) -> void: Food Mod: %.2f Safety Mod: %.2f Knowledge Mod: %.2f - %s%s%s + %s%s%s%s """ % [ speed_text, @@ -114,6 +115,7 @@ func _on_village_changed(village: SimVillage) -> void: village.food_modifier, village.safety_modifier, village.knowledge_modifier, + player_needs_text, standing_text, opportunity_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: if not simulation_manager.has_method("get_player_standing"): return ""