feat: give the player embodied hunger, energy, and starvation death
This commit is contained in:
@@ -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"]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user