refactor: clarify simulation ownership
This commit is contained in:
@@ -34,7 +34,7 @@ var last_task: StringName
|
||||
var random_source: RandomNumberGenerator
|
||||
var familiarity: Dictionary = {}
|
||||
var mourning_ticks := 0
|
||||
var debug_logs := true
|
||||
var debug_logs := false
|
||||
|
||||
|
||||
func _init(
|
||||
|
||||
@@ -5,7 +5,7 @@ var food := 20.0
|
||||
var wood := 10.0
|
||||
var safety := 50.0
|
||||
var knowledge := 0.0
|
||||
var debug_logs := true
|
||||
var debug_logs := false
|
||||
|
||||
var food_modifier := 1.0
|
||||
var wood_modifier := 1.0
|
||||
@@ -70,19 +70,13 @@ func update_priorities() -> void:
|
||||
print(get_priority_summary())
|
||||
|
||||
|
||||
func apply_resource_delta(resource_id: StringName, amount: float) -> void:
|
||||
match resource_id:
|
||||
&"food":
|
||||
food += amount
|
||||
&"wood":
|
||||
wood += amount
|
||||
func apply_metric_delta(metric_id: StringName, amount: float) -> void:
|
||||
match metric_id:
|
||||
&"safety":
|
||||
safety = clamp(safety + amount, 0.0, 100.0)
|
||||
&"knowledge":
|
||||
knowledge += amount
|
||||
|
||||
food = max(food, 0.0)
|
||||
wood = max(wood, 0.0)
|
||||
safety = clamp(safety, 0.0, 100.0)
|
||||
knowledge = max(knowledge, 0.0)
|
||||
|
||||
|
||||
+241
-485
@@ -1,5 +1,8 @@
|
||||
extends Node
|
||||
|
||||
const SimulationEventLogScript := preload("res://simulation/events/SimulationEventLog.gd")
|
||||
const VillageEconomyScript := preload("res://simulation/economy/VillageEconomy.gd")
|
||||
|
||||
signal npc_task_changed(npc: SimNPC, old_task: StringName, new_task: StringName)
|
||||
signal village_changed(village: SimVillage)
|
||||
signal npc_died(npc: SimNPC)
|
||||
@@ -16,7 +19,7 @@ var npcs: Array[SimNPC] = []
|
||||
@export var tick_interval := 1.2
|
||||
@export var simulation_seed: int = 1337
|
||||
@export var cycle_duration_seconds := 240.0
|
||||
@export var debug_logs := true
|
||||
@export var debug_logs := false
|
||||
@export var active_world_adapter: Node
|
||||
@export var home_positions: Array[Vector3] = []
|
||||
|
||||
@@ -24,9 +27,19 @@ var clock: SimulationClock
|
||||
var tick_count := 0
|
||||
var wander_random_sources := {}
|
||||
var resource_states: Dictionary = {}
|
||||
var storage_states: Dictionary = {}
|
||||
var economic_events: Array[EconomicEventRecord] = []
|
||||
var next_event_id := 0
|
||||
var event_log := SimulationEventLogScript.new()
|
||||
var economy := VillageEconomyScript.new()
|
||||
var storage_states: Dictionary:
|
||||
get:
|
||||
return economy.storage_states
|
||||
var economic_events: Array[EconomicEventRecord]:
|
||||
get:
|
||||
return event_log.events
|
||||
var next_event_id: int:
|
||||
get:
|
||||
return event_log.next_event_id
|
||||
set(value):
|
||||
event_log.next_event_id = value
|
||||
var latest_decisions: Dictionary = {}
|
||||
var action_selector := ActionSelectionSystem.new()
|
||||
var action_executor := ActionExecutionSystem.new()
|
||||
@@ -35,11 +48,15 @@ var speed_index := 2
|
||||
const SPEED_LEVELS := [0.25, 0.5, 1.0, 2.0, 4.0, 10.0]
|
||||
signal speed_changed(multiplier: float)
|
||||
|
||||
var names := ["Amina", "Tarik", "Jasmin", "Elma", "Mirza", "Lejla"]
|
||||
const NPC_NAMES := ["Amina", "Tarik", "Jasmin", "Elma", "Mirza", "Lejla"]
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("simulation_manager")
|
||||
event_log.event_recorded.connect(_on_economic_event_recorded)
|
||||
economy.inventory_changed.connect(_on_economy_inventory_changed)
|
||||
economy.economic_event_requested.connect(_record_economic_event)
|
||||
economy.narrative_event_requested.connect(record_narrative_event)
|
||||
var definition_errors := SimulationDefinitions.validate()
|
||||
if not definition_errors.is_empty():
|
||||
for error in definition_errors:
|
||||
@@ -50,7 +67,8 @@ func _ready() -> void:
|
||||
clock.cycle_duration_seconds = cycle_duration_seconds
|
||||
clock.elapsed_ticks = int(0.25 * cycle_duration_seconds / tick_interval)
|
||||
village.debug_logs = debug_logs
|
||||
_initialize_storage()
|
||||
economy.configure(village, debug_logs)
|
||||
economy.initialize_storage()
|
||||
village.update_modifiers()
|
||||
village.update_priorities()
|
||||
generate_npcs()
|
||||
@@ -83,12 +101,12 @@ func _input(event: InputEvent) -> void:
|
||||
|
||||
func generate_npcs() -> void:
|
||||
var profession_ids := SimulationDefinitions.get_profession_ids()
|
||||
for i in range(names.size()):
|
||||
for i in range(NPC_NAMES.size()):
|
||||
var npc_random := _create_random_source(i, 0)
|
||||
var profession_index := npc_random.randi_range(0, profession_ids.size() - 1)
|
||||
var npc := SimNPC.new(
|
||||
i,
|
||||
names[i],
|
||||
NPC_NAMES[i],
|
||||
profession_ids[profession_index],
|
||||
npc_random.randf_range(1.0, 10.0),
|
||||
npc_random.randf_range(1.0, 10.0),
|
||||
@@ -131,210 +149,8 @@ func simulate_tick() -> void:
|
||||
print("--- Tick ", tick_count, " ---")
|
||||
|
||||
var village_was_changed := false
|
||||
|
||||
for npc in npcs:
|
||||
var old_task := npc.current_task
|
||||
var old_target := npc.target_id
|
||||
var old_state := npc.task_state
|
||||
var was_complete := npc.task_complete
|
||||
var was_dead := npc.is_dead
|
||||
|
||||
action_executor.advance_npc(npc, village)
|
||||
if (
|
||||
not npc.is_dead
|
||||
and old_state in [SimNPC.TASK_STATE_IDLE, SimNPC.TASK_STATE_COMPLETE]
|
||||
and npc.task_state in [SimNPC.TASK_STATE_IDLE, SimNPC.TASK_STATE_COMPLETE]
|
||||
):
|
||||
var selection := action_selector.select_action(npc, village, clock.time_of_day(), npcs)
|
||||
if selection != null:
|
||||
latest_decisions[npc.id] = selection
|
||||
npc_decision_recorded.emit(npc, selection)
|
||||
npc.set_task(selection.action_id, selection.duration_override)
|
||||
var action_def := SimulationDefinitions.get_action(selection.action_id)
|
||||
var action_display := String(selection.action_id)
|
||||
if action_def != null:
|
||||
action_display = action_def.display_name
|
||||
record_narrative_event(
|
||||
SimulationIds.EVENT_TASK_STARTED, npc.id, &"", action_display
|
||||
)
|
||||
|
||||
if old_task != npc.current_task and old_target != &"":
|
||||
release_npc_reservation(npc.id)
|
||||
|
||||
if not was_dead and npc.is_dead:
|
||||
if old_target != &"":
|
||||
release_npc_reservation(npc.id)
|
||||
npc_died.emit(npc)
|
||||
npc_task_changed.emit(npc, old_task, npc.current_task)
|
||||
record_narrative_event(SimulationIds.EVENT_NPC_DIED, npc.id)
|
||||
_notify_mourning(npc)
|
||||
|
||||
if debug_logs:
|
||||
print("[SimulationManager] NPC died: ", npc.npc_name)
|
||||
|
||||
if old_task != npc.current_task and not npc.is_dead:
|
||||
npc_task_changed.emit(npc, old_task, npc.current_task)
|
||||
npc_target_requested.emit(npc)
|
||||
|
||||
if not was_complete and npc.task_complete and not npc.is_dead:
|
||||
var completed_task := npc.current_task
|
||||
var completed_definition := SimulationDefinitions.get_action(completed_task)
|
||||
var completion_cost_paid := _consume_action_completion_cost(
|
||||
npc, completed_definition
|
||||
)
|
||||
if not completion_cost_paid:
|
||||
if not npc.target_id.is_empty():
|
||||
release_npc_reservation(npc.id)
|
||||
elif (
|
||||
npc.target_id != &""
|
||||
and completed_definition != null
|
||||
and completed_definition.target_type == SimulationIds.TARGET_RESOURCE
|
||||
):
|
||||
var resource_state := get_resource_state(npc.target_id)
|
||||
if resource_state != null and resource_state.get_reserved_by() == npc.id:
|
||||
var extracted := resource_state.extract()
|
||||
if extracted > 0:
|
||||
if resource_state.get_resource_id() == SimulationIds.RESOURCE_FOOD:
|
||||
npc.add_inventory(SimulationIds.RESOURCE_FOOD, extracted)
|
||||
npc_inventory_changed.emit(
|
||||
npc,
|
||||
SimulationIds.RESOURCE_FOOD,
|
||||
npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD)
|
||||
)
|
||||
elif resource_state.get_resource_id() == SimulationIds.RESOURCE_WOOD:
|
||||
npc.add_inventory(SimulationIds.RESOURCE_WOOD, extracted)
|
||||
npc_inventory_changed.emit(
|
||||
npc,
|
||||
SimulationIds.RESOURCE_WOOD,
|
||||
npc.get_inventory_amount(SimulationIds.RESOURCE_WOOD)
|
||||
)
|
||||
else:
|
||||
village.apply_resource_delta(
|
||||
resource_state.get_resource_id(), extracted
|
||||
)
|
||||
_record_economic_event(
|
||||
SimulationIds.EVENT_RESOURCE_EXTRACTED,
|
||||
npc.id,
|
||||
resource_state.get_node_id(),
|
||||
(
|
||||
_npc_inventory_id(npc.id)
|
||||
if resource_state.get_resource_id() in [
|
||||
SimulationIds.RESOURCE_FOOD, SimulationIds.RESOURCE_WOOD
|
||||
]
|
||||
else &"village"
|
||||
),
|
||||
resource_state.get_resource_id(),
|
||||
extracted
|
||||
)
|
||||
if resource_state.get_amount_remaining() <= 0.0:
|
||||
record_narrative_event(
|
||||
SimulationIds.EVENT_RESOURCE_DEPLETED,
|
||||
npc.id,
|
||||
resource_state.get_node_id()
|
||||
)
|
||||
if debug_logs:
|
||||
print(
|
||||
"[SimulationManager] ",
|
||||
npc.npc_name,
|
||||
" extracted ",
|
||||
extracted,
|
||||
" ",
|
||||
resource_state.get_resource_id(),
|
||||
" from ",
|
||||
resource_state.get_node_id()
|
||||
)
|
||||
elif debug_logs:
|
||||
print(
|
||||
"[SimulationManager] ",
|
||||
npc.npc_name,
|
||||
" could not complete ",
|
||||
completed_task,
|
||||
": target reservation was invalid"
|
||||
)
|
||||
release_npc_reservation(npc.id)
|
||||
elif completed_task == SimulationIds.ACTION_DEPOSIT_FOOD:
|
||||
deposit_npc_inventory(npc, SimulationIds.RESOURCE_FOOD)
|
||||
elif completed_task == SimulationIds.ACTION_DEPOSIT_WOOD:
|
||||
deposit_npc_wood(npc)
|
||||
elif completed_task == SimulationIds.ACTION_WITHDRAW_FOOD:
|
||||
withdraw_to_npc(npc, SimulationIds.RESOURCE_FOOD, 1.0)
|
||||
elif completed_task == SimulationIds.ACTION_EAT:
|
||||
consume_npc_food(npc)
|
||||
elif completed_task == SimulationIds.ACTION_SLEEP:
|
||||
npc.energy = minf(npc.energy + 40.0, 100.0)
|
||||
npc.position = npc.home_position
|
||||
record_narrative_event(SimulationIds.EVENT_NPC_SLEPT, npc.id)
|
||||
if debug_logs:
|
||||
print("[SimulationManager] ", npc.npc_name, " completed sleep at home")
|
||||
elif (
|
||||
completed_task
|
||||
not in [
|
||||
SimulationIds.ACTION_GATHER_FOOD,
|
||||
SimulationIds.ACTION_GATHER_WOOD,
|
||||
SimulationIds.ACTION_PATROL,
|
||||
SimulationIds.ACTION_STUDY
|
||||
]
|
||||
):
|
||||
village.apply_npc_task(npc)
|
||||
elif completed_task == SimulationIds.ACTION_PATROL:
|
||||
village.apply_npc_task(npc)
|
||||
elif completed_task == SimulationIds.ACTION_STUDY:
|
||||
village.apply_npc_task(npc)
|
||||
elif debug_logs:
|
||||
print(
|
||||
"[SimulationManager] ",
|
||||
npc.npc_name,
|
||||
" could not complete ",
|
||||
completed_task,
|
||||
": no resource target"
|
||||
)
|
||||
|
||||
village_was_changed = true
|
||||
|
||||
npc.task_complete = true
|
||||
npc.task_state = SimNPC.TASK_STATE_IDLE
|
||||
npc.last_task = completed_task
|
||||
npc.current_task = SimulationIds.ACTION_IDLE
|
||||
npc.has_travel_target = false
|
||||
npc.target_id = &""
|
||||
npc_task_changed.emit(npc, completed_task, npc.current_task)
|
||||
|
||||
if debug_logs:
|
||||
print(
|
||||
npc.npc_name,
|
||||
" | ",
|
||||
npc.profession,
|
||||
" | task: ",
|
||||
npc.current_task,
|
||||
" | state: ",
|
||||
npc.task_state,
|
||||
" | progress: ",
|
||||
npc.task_progress,
|
||||
"/",
|
||||
npc.task_duration,
|
||||
" | complete: ",
|
||||
npc.task_complete,
|
||||
" | hunger: ",
|
||||
round(npc.hunger),
|
||||
" | energy: ",
|
||||
round(npc.energy),
|
||||
" | starving: ",
|
||||
npc.is_starving,
|
||||
" | starvation_ticks: ",
|
||||
npc.starvation_ticks,
|
||||
" | dead: ",
|
||||
npc.is_dead
|
||||
)
|
||||
|
||||
if old_state != npc.task_state:
|
||||
print(
|
||||
"[SimulationManager] State changed: ",
|
||||
npc.npc_name,
|
||||
" ",
|
||||
old_state,
|
||||
" -> ",
|
||||
npc.task_state
|
||||
)
|
||||
village_was_changed = _simulate_npc_tick(npc) or village_was_changed
|
||||
|
||||
if village_was_changed:
|
||||
village_changed.emit(village)
|
||||
@@ -343,11 +159,179 @@ func simulate_tick() -> void:
|
||||
print(village.get_summary())
|
||||
|
||||
|
||||
func set_npc_target_id(npc_id: int, target_id: StringName) -> void:
|
||||
for npc in npcs:
|
||||
if npc.id == npc_id:
|
||||
npc.target_id = target_id
|
||||
return
|
||||
func _simulate_npc_tick(npc: SimNPC) -> bool:
|
||||
var previous_task := npc.current_task
|
||||
var previous_target := npc.target_id
|
||||
var previous_state := npc.task_state
|
||||
var was_complete := npc.task_complete
|
||||
var was_dead := npc.is_dead
|
||||
|
||||
action_executor.advance_npc(npc, village)
|
||||
_select_action_if_idle(npc, previous_state)
|
||||
|
||||
if previous_task != npc.current_task and not previous_target.is_empty():
|
||||
release_npc_reservation(npc.id)
|
||||
|
||||
if not was_dead and npc.is_dead:
|
||||
_handle_npc_death(npc, previous_task, previous_target)
|
||||
elif previous_task != npc.current_task:
|
||||
npc_task_changed.emit(npc, previous_task, npc.current_task)
|
||||
npc_target_requested.emit(npc)
|
||||
|
||||
var completed := not was_complete and npc.task_complete and not npc.is_dead
|
||||
if completed:
|
||||
_complete_current_action(npc)
|
||||
|
||||
_debug_npc_tick(npc, previous_state)
|
||||
return completed
|
||||
|
||||
|
||||
func _select_action_if_idle(npc: SimNPC, previous_state: StringName) -> void:
|
||||
if npc.is_dead:
|
||||
return
|
||||
if previous_state not in [SimNPC.TASK_STATE_IDLE, SimNPC.TASK_STATE_COMPLETE]:
|
||||
return
|
||||
if npc.task_state not in [SimNPC.TASK_STATE_IDLE, SimNPC.TASK_STATE_COMPLETE]:
|
||||
return
|
||||
var selection := action_selector.select_action(npc, village, clock.time_of_day(), npcs)
|
||||
if selection == null:
|
||||
return
|
||||
latest_decisions[npc.id] = selection
|
||||
npc_decision_recorded.emit(npc, selection)
|
||||
npc.set_task(selection.action_id, selection.duration_override)
|
||||
var definition := SimulationDefinitions.get_action(selection.action_id)
|
||||
var display_name := definition.display_name if definition != null else String(selection.action_id)
|
||||
record_narrative_event(SimulationIds.EVENT_TASK_STARTED, npc.id, &"", display_name)
|
||||
|
||||
|
||||
func _handle_npc_death(
|
||||
npc: SimNPC, previous_task: StringName, previous_target: StringName
|
||||
) -> void:
|
||||
if not previous_target.is_empty():
|
||||
release_npc_reservation(npc.id)
|
||||
npc_died.emit(npc)
|
||||
npc_task_changed.emit(npc, previous_task, npc.current_task)
|
||||
record_narrative_event(SimulationIds.EVENT_NPC_DIED, npc.id)
|
||||
_notify_mourning(npc)
|
||||
if debug_logs:
|
||||
print("[SimulationManager] NPC died: ", npc.npc_name)
|
||||
|
||||
|
||||
func _complete_current_action(npc: SimNPC) -> void:
|
||||
var completed_task := npc.current_task
|
||||
var definition := SimulationDefinitions.get_action(completed_task)
|
||||
if economy.consume_completion_cost(npc, definition):
|
||||
_apply_action_completion(npc, completed_task, definition)
|
||||
elif not npc.target_id.is_empty():
|
||||
release_npc_reservation(npc.id)
|
||||
|
||||
npc.task_complete = true
|
||||
npc.task_state = SimNPC.TASK_STATE_IDLE
|
||||
npc.last_task = completed_task
|
||||
npc.current_task = SimulationIds.ACTION_IDLE
|
||||
npc.has_travel_target = false
|
||||
npc.target_id = &""
|
||||
npc_task_changed.emit(npc, completed_task, npc.current_task)
|
||||
|
||||
|
||||
func _apply_action_completion(
|
||||
npc: SimNPC, completed_task: StringName, definition: ActionDefinition
|
||||
) -> void:
|
||||
if definition != null and definition.target_type == SimulationIds.TARGET_RESOURCE:
|
||||
_complete_resource_gather(npc, completed_task)
|
||||
return
|
||||
match completed_task:
|
||||
SimulationIds.ACTION_DEPOSIT_FOOD:
|
||||
economy.deposit_inventory(npc, SimulationIds.RESOURCE_FOOD)
|
||||
SimulationIds.ACTION_DEPOSIT_WOOD:
|
||||
economy.deposit_inventory(npc, SimulationIds.RESOURCE_WOOD)
|
||||
SimulationIds.ACTION_WITHDRAW_FOOD:
|
||||
economy.withdraw_to_inventory(npc, SimulationIds.RESOURCE_FOOD, 1.0)
|
||||
SimulationIds.ACTION_EAT:
|
||||
economy.consume_npc_food(npc)
|
||||
SimulationIds.ACTION_SLEEP:
|
||||
npc.energy = minf(npc.energy + 40.0, 100.0)
|
||||
npc.position = npc.home_position
|
||||
record_narrative_event(SimulationIds.EVENT_NPC_SLEPT, npc.id)
|
||||
SimulationIds.ACTION_PATROL, SimulationIds.ACTION_STUDY, SimulationIds.ACTION_REST, SimulationIds.ACTION_WANDER:
|
||||
village.apply_npc_task(npc)
|
||||
if debug_logs and completed_task == SimulationIds.ACTION_SLEEP:
|
||||
print("[SimulationManager] ", npc.npc_name, " completed sleep at home")
|
||||
|
||||
|
||||
func _complete_resource_gather(npc: SimNPC, completed_task: StringName) -> void:
|
||||
var resource_state := get_resource_state(npc.target_id)
|
||||
if resource_state == null or resource_state.get_reserved_by() != npc.id:
|
||||
if debug_logs:
|
||||
print(
|
||||
"[SimulationManager] %s could not complete %s: invalid reservation"
|
||||
% [npc.npc_name, completed_task]
|
||||
)
|
||||
release_npc_reservation(npc.id)
|
||||
return
|
||||
|
||||
var extracted := resource_state.extract()
|
||||
if extracted > 0.0:
|
||||
var resource_id := resource_state.get_resource_id()
|
||||
npc.add_inventory(resource_id, extracted)
|
||||
npc_inventory_changed.emit(npc, resource_id, npc.get_inventory_amount(resource_id))
|
||||
_record_economic_event(
|
||||
SimulationIds.EVENT_RESOURCE_EXTRACTED,
|
||||
npc.id,
|
||||
resource_state.get_node_id(),
|
||||
SimulationIds.npc_inventory_id(npc.id),
|
||||
resource_id,
|
||||
extracted
|
||||
)
|
||||
if resource_state.get_amount_remaining() <= 0.0:
|
||||
record_narrative_event(
|
||||
SimulationIds.EVENT_RESOURCE_DEPLETED, npc.id, resource_state.get_node_id()
|
||||
)
|
||||
if debug_logs:
|
||||
print(
|
||||
"[SimulationManager] %s extracted %.1f %s from %s"
|
||||
% [npc.npc_name, extracted, resource_id, resource_state.get_node_id()]
|
||||
)
|
||||
release_npc_reservation(npc.id)
|
||||
|
||||
|
||||
func _debug_npc_tick(npc: SimNPC, previous_state: StringName) -> void:
|
||||
if not debug_logs:
|
||||
return
|
||||
print(
|
||||
npc.npc_name,
|
||||
" | ",
|
||||
npc.profession,
|
||||
" | task: ",
|
||||
npc.current_task,
|
||||
" | state: ",
|
||||
npc.task_state,
|
||||
" | progress: ",
|
||||
npc.task_progress,
|
||||
"/",
|
||||
npc.task_duration,
|
||||
" | complete: ",
|
||||
npc.task_complete,
|
||||
" | hunger: ",
|
||||
round(npc.hunger),
|
||||
" | energy: ",
|
||||
round(npc.energy),
|
||||
" | starving: ",
|
||||
npc.is_starving,
|
||||
" | starvation_ticks: ",
|
||||
npc.starvation_ticks,
|
||||
" | dead: ",
|
||||
npc.is_dead
|
||||
)
|
||||
if previous_state != npc.task_state:
|
||||
print(
|
||||
"[SimulationManager] State changed: ",
|
||||
npc.npc_name,
|
||||
" ",
|
||||
previous_state,
|
||||
" -> ",
|
||||
npc.task_state
|
||||
)
|
||||
|
||||
|
||||
func release_npc_reservation(npc_id: int) -> void:
|
||||
@@ -446,10 +430,6 @@ func notify_npc_navigation_failed(npc_id: int) -> void:
|
||||
return
|
||||
|
||||
|
||||
func notify_npc_target_unavailable(npc_id: int) -> void:
|
||||
notify_npc_navigation_failed(npc_id)
|
||||
|
||||
|
||||
func resolve_npc_target(npc_id: int, origin: Vector3) -> bool:
|
||||
for npc in npcs:
|
||||
if npc.id != npc_id:
|
||||
@@ -466,7 +446,7 @@ func resolve_npc_target(npc_id: int, origin: Vector3) -> bool:
|
||||
return false
|
||||
var result := target_resolver.resolve(npc, origin, self, active_world_adapter)
|
||||
if result.is_empty():
|
||||
notify_npc_target_unavailable(npc.id)
|
||||
notify_npc_navigation_failed(npc.id)
|
||||
return false
|
||||
npc.target_id = StringName(result.get("target_id", ""))
|
||||
npc.travel_target_position = result["position"]
|
||||
@@ -510,10 +490,6 @@ func get_activity_target_claim_count(target_id: StringName, except_npc_id: int =
|
||||
return count
|
||||
|
||||
|
||||
func _npc_inventory_id(npc_id: int) -> StringName:
|
||||
return StringName("npc_%d_inventory" % npc_id)
|
||||
|
||||
|
||||
func _notify_mourning(dead_npc: SimNPC) -> void:
|
||||
var best_id := -1
|
||||
var best_score := -1.0
|
||||
@@ -546,14 +522,9 @@ func _record_economic_event(
|
||||
item_id: StringName,
|
||||
amount: float
|
||||
) -> void:
|
||||
if amount <= 0.0:
|
||||
return
|
||||
var event := EconomicEventRecord.create(
|
||||
next_event_id, event_type, tick_count, actor_id, source_id, destination_id, item_id, amount
|
||||
event_log.record_economic(
|
||||
tick_count, event_type, actor_id, source_id, destination_id, item_id, amount
|
||||
)
|
||||
next_event_id += 1
|
||||
economic_events.append(event)
|
||||
economic_event_recorded.emit(event)
|
||||
|
||||
|
||||
func record_narrative_event(
|
||||
@@ -562,32 +533,19 @@ func record_narrative_event(
|
||||
source_id: StringName = &"",
|
||||
action_display: String = ""
|
||||
) -> void:
|
||||
var event := EconomicEventRecord.create_narrative(
|
||||
next_event_id, event_type, tick_count, actor_id, source_id, action_display
|
||||
)
|
||||
next_event_id += 1
|
||||
economic_events.append(event)
|
||||
economic_event_recorded.emit(event)
|
||||
event_log.record_narrative(tick_count, event_type, actor_id, source_id, action_display)
|
||||
|
||||
|
||||
func get_npc_events(npc_id: int, max_count: int = 8) -> Array[EconomicEventRecord]:
|
||||
var results: Array[EconomicEventRecord] = []
|
||||
for i in range(economic_events.size() - 1, -1, -1):
|
||||
var event: EconomicEventRecord = economic_events[i]
|
||||
if event.data["actor_id"] == npc_id:
|
||||
results.append(event)
|
||||
if results.size() >= max_count:
|
||||
break
|
||||
results.reverse()
|
||||
return results
|
||||
return event_log.get_for_actor(npc_id, max_count)
|
||||
|
||||
|
||||
func get_recent_events(max_count: int = 5) -> Array[EconomicEventRecord]:
|
||||
var results: Array[EconomicEventRecord] = []
|
||||
var start := maxi(economic_events.size() - max_count, 0)
|
||||
for i in range(start, economic_events.size()):
|
||||
results.append(economic_events[i])
|
||||
return results
|
||||
return event_log.get_recent(max_count)
|
||||
|
||||
|
||||
func _on_economic_event_recorded(event: EconomicEventRecord) -> void:
|
||||
economic_event_recorded.emit(event)
|
||||
|
||||
|
||||
func get_current_speed() -> String:
|
||||
@@ -595,57 +553,15 @@ func get_current_speed() -> String:
|
||||
|
||||
|
||||
func get_resource_rates() -> Dictionary:
|
||||
var food_consumed := 0.0
|
||||
var wood_consumed := 0.0
|
||||
var recent_ticks := maxi(tick_count - 200, 0)
|
||||
for event in economic_events:
|
||||
if int(event.data["tick"]) < recent_ticks:
|
||||
continue
|
||||
if String(event.data["event_type"]) != "item_consumed":
|
||||
continue
|
||||
if float(event.data["amount"]) <= 0.0:
|
||||
continue
|
||||
if String(event.data["item_id"]) == "food":
|
||||
food_consumed += float(event.data["amount"])
|
||||
elif String(event.data["item_id"]) == "wood":
|
||||
wood_consumed += float(event.data["amount"])
|
||||
var elapsed := maxi(tick_count - recent_ticks, 1)
|
||||
return {
|
||||
"food_per_day": food_consumed / elapsed * 200.0,
|
||||
"wood_per_day": wood_consumed / elapsed * 200.0
|
||||
}
|
||||
|
||||
|
||||
func _initialize_storage() -> void:
|
||||
if not storage_states.has(SimulationIds.STORAGE_VILLAGE_PANTRY):
|
||||
storage_states[SimulationIds.STORAGE_VILLAGE_PANTRY] = (StorageStateRecord.create(
|
||||
SimulationIds.STORAGE_VILLAGE_PANTRY,
|
||||
{String(SimulationIds.RESOURCE_FOOD): village.food}
|
||||
))
|
||||
if not storage_states.has(SimulationIds.STORAGE_VILLAGE_WOODPILE):
|
||||
storage_states[SimulationIds.STORAGE_VILLAGE_WOODPILE] = (StorageStateRecord.create(
|
||||
SimulationIds.STORAGE_VILLAGE_WOODPILE,
|
||||
{String(SimulationIds.RESOURCE_WOOD): village.wood}
|
||||
))
|
||||
_sync_village_food()
|
||||
_sync_village_wood()
|
||||
return event_log.get_consumption_rates(tick_count)
|
||||
|
||||
|
||||
func get_pantry() -> StorageStateRecord:
|
||||
return storage_states.get(SimulationIds.STORAGE_VILLAGE_PANTRY) as StorageStateRecord
|
||||
return economy.get_pantry()
|
||||
|
||||
|
||||
func get_woodpile() -> StorageStateRecord:
|
||||
return storage_states.get(SimulationIds.STORAGE_VILLAGE_WOODPILE) as StorageStateRecord
|
||||
|
||||
|
||||
func get_storage_for_resource(resource_id: StringName) -> StorageStateRecord:
|
||||
match resource_id:
|
||||
SimulationIds.RESOURCE_FOOD:
|
||||
return get_pantry()
|
||||
SimulationIds.RESOURCE_WOOD:
|
||||
return get_woodpile()
|
||||
return null
|
||||
return economy.get_woodpile()
|
||||
|
||||
|
||||
func register_loaded_storage_nodes() -> void:
|
||||
@@ -667,149 +583,8 @@ func register_storage_node(node: StorageNode) -> bool:
|
||||
return node.bind_state(storage_state)
|
||||
|
||||
|
||||
func _sync_village_food() -> void:
|
||||
var pantry := get_pantry()
|
||||
if pantry != null:
|
||||
village.food = pantry.get_amount(SimulationIds.RESOURCE_FOOD)
|
||||
village.update_modifiers()
|
||||
village.update_priorities()
|
||||
|
||||
|
||||
func _sync_village_wood() -> void:
|
||||
var woodpile := get_woodpile()
|
||||
if woodpile != null:
|
||||
village.wood = woodpile.get_amount(SimulationIds.RESOURCE_WOOD)
|
||||
village.update_modifiers()
|
||||
village.update_priorities()
|
||||
|
||||
|
||||
func _sync_village_resource(resource_id: StringName) -> void:
|
||||
match resource_id:
|
||||
SimulationIds.RESOURCE_FOOD:
|
||||
_sync_village_food()
|
||||
SimulationIds.RESOURCE_WOOD:
|
||||
_sync_village_wood()
|
||||
|
||||
|
||||
func deposit_npc_inventory(npc: SimNPC, item_id: StringName) -> float:
|
||||
var available := npc.get_inventory_amount(item_id)
|
||||
var deposited := get_pantry().deposit(item_id, available)
|
||||
npc.remove_inventory(item_id, deposited)
|
||||
_sync_village_food()
|
||||
if deposited > 0.0:
|
||||
npc_inventory_changed.emit(npc, item_id, npc.get_inventory_amount(item_id))
|
||||
_record_economic_event(
|
||||
SimulationIds.EVENT_STORAGE_DEPOSITED,
|
||||
npc.id,
|
||||
_npc_inventory_id(npc.id),
|
||||
SimulationIds.STORAGE_VILLAGE_PANTRY,
|
||||
item_id,
|
||||
deposited
|
||||
)
|
||||
return deposited
|
||||
|
||||
|
||||
func deposit_npc_wood(npc: SimNPC) -> float:
|
||||
var available := npc.get_inventory_amount(SimulationIds.RESOURCE_WOOD)
|
||||
var woodpile := get_woodpile()
|
||||
if woodpile == null:
|
||||
return 0.0
|
||||
var deposited := woodpile.deposit(SimulationIds.RESOURCE_WOOD, available)
|
||||
npc.remove_inventory(SimulationIds.RESOURCE_WOOD, deposited)
|
||||
_sync_village_wood()
|
||||
if deposited > 0.0:
|
||||
npc_inventory_changed.emit(
|
||||
npc, SimulationIds.RESOURCE_WOOD, npc.get_inventory_amount(SimulationIds.RESOURCE_WOOD)
|
||||
)
|
||||
_record_economic_event(
|
||||
SimulationIds.EVENT_STORAGE_DEPOSITED,
|
||||
npc.id,
|
||||
_npc_inventory_id(npc.id),
|
||||
SimulationIds.STORAGE_VILLAGE_WOODPILE,
|
||||
SimulationIds.RESOURCE_WOOD,
|
||||
deposited
|
||||
)
|
||||
return deposited
|
||||
|
||||
|
||||
func _consume_action_completion_cost(npc: SimNPC, definition: ActionDefinition) -> bool:
|
||||
if definition == null or not definition.has_completion_cost():
|
||||
return true
|
||||
var resource_id := definition.completion_cost_resource_id
|
||||
var required_amount := definition.completion_cost_amount
|
||||
var storage := get_storage_for_resource(resource_id)
|
||||
var available := storage.get_amount(resource_id) if storage != null else 0.0
|
||||
if storage == null or available < required_amount:
|
||||
var reason := "%s: needs %.0f %s (%.1f available)" % [
|
||||
definition.display_name,
|
||||
required_amount,
|
||||
String(resource_id).capitalize(),
|
||||
available,
|
||||
]
|
||||
record_narrative_event(
|
||||
SimulationIds.EVENT_TASK_BLOCKED,
|
||||
npc.id,
|
||||
storage.get_storage_id() if storage != null else &"",
|
||||
reason
|
||||
)
|
||||
return false
|
||||
var consumed := storage.withdraw(resource_id, required_amount)
|
||||
_sync_village_resource(resource_id)
|
||||
if consumed < required_amount:
|
||||
return false
|
||||
_record_economic_event(
|
||||
SimulationIds.EVENT_ITEM_CONSUMED,
|
||||
npc.id,
|
||||
storage.get_storage_id(),
|
||||
&"consumed",
|
||||
resource_id,
|
||||
consumed
|
||||
)
|
||||
if debug_logs:
|
||||
print(
|
||||
"[SimulationManager] %s consumed %.1f %s for %s"
|
||||
% [npc.npc_name, consumed, resource_id, definition.action_id]
|
||||
)
|
||||
return true
|
||||
|
||||
|
||||
func withdraw_to_npc(npc: SimNPC, item_id: StringName, amount: float) -> float:
|
||||
var withdrawn := get_pantry().withdraw(item_id, amount)
|
||||
npc.add_inventory(item_id, withdrawn)
|
||||
_sync_village_food()
|
||||
if withdrawn > 0.0:
|
||||
npc_inventory_changed.emit(npc, item_id, npc.get_inventory_amount(item_id))
|
||||
_record_economic_event(
|
||||
SimulationIds.EVENT_STORAGE_WITHDRAWN,
|
||||
npc.id,
|
||||
SimulationIds.STORAGE_VILLAGE_PANTRY,
|
||||
_npc_inventory_id(npc.id),
|
||||
item_id,
|
||||
withdrawn
|
||||
)
|
||||
return withdrawn
|
||||
|
||||
|
||||
func consume_npc_food(npc: SimNPC) -> bool:
|
||||
if npc.remove_inventory(SimulationIds.RESOURCE_FOOD, 1.0) < 1.0:
|
||||
npc.hunger = minf(npc.hunger + 5.0, 100.0)
|
||||
npc.is_starving = npc.hunger >= 90.0
|
||||
return false
|
||||
npc.hunger = maxf(npc.hunger - 55.0, 0.0)
|
||||
npc.starvation_ticks = 0
|
||||
npc.is_starving = npc.hunger >= 90.0
|
||||
npc_inventory_changed.emit(
|
||||
npc, SimulationIds.RESOURCE_FOOD, npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD)
|
||||
)
|
||||
_record_economic_event(
|
||||
SimulationIds.EVENT_ITEM_CONSUMED,
|
||||
npc.id,
|
||||
_npc_inventory_id(npc.id),
|
||||
&"consumed",
|
||||
SimulationIds.RESOURCE_FOOD,
|
||||
1.0
|
||||
)
|
||||
return true
|
||||
func _on_economy_inventory_changed(npc: SimNPC, item_id: StringName, amount: float) -> void:
|
||||
npc_inventory_changed.emit(npc, item_id, amount)
|
||||
|
||||
|
||||
func register_loaded_resource_nodes() -> void:
|
||||
@@ -825,11 +600,12 @@ func register_resource_node(node: ResourceNode) -> bool:
|
||||
action_definition == null
|
||||
or action_definition.target_type != SimulationIds.TARGET_RESOURCE
|
||||
or action_definition.resource_action_id != node.action_id
|
||||
or node.resource_id not in [SimulationIds.RESOURCE_FOOD, SimulationIds.RESOURCE_WOOD]
|
||||
):
|
||||
push_error(
|
||||
(
|
||||
"SimulationManager: ResourceNode '%s' has invalid action_id '%s'"
|
||||
% [node.node_id, node.action_id]
|
||||
"SimulationManager: ResourceNode '%s' has invalid action/resource IDs '%s'/'%s'"
|
||||
% [node.node_id, node.action_id, node.resource_id]
|
||||
)
|
||||
)
|
||||
return false
|
||||
@@ -863,7 +639,14 @@ func find_resource_node_for_player(from_position: Vector3, max_distance: float)
|
||||
var best_distance := max_distance * max_distance
|
||||
for node in ResourceNode.get_all():
|
||||
var resource_state := get_resource_state(node.node_id)
|
||||
if resource_state == null or not resource_state.can_player_use_resource():
|
||||
if (
|
||||
resource_state == null
|
||||
or not resource_state.can_player_use_resource()
|
||||
or not resource_state.can_extract()
|
||||
):
|
||||
continue
|
||||
var storage := economy.get_storage_for_resource(resource_state.get_resource_id())
|
||||
if storage == null or storage.get_available_capacity() <= 0.0:
|
||||
continue
|
||||
var distance := from_position.distance_squared_to(node.interaction_point.global_position)
|
||||
if distance <= best_distance:
|
||||
@@ -878,18 +661,16 @@ func harvest_resource_node(node: ResourceNode) -> float:
|
||||
var resource_state := get_resource_state(node.node_id)
|
||||
if resource_state == null or not resource_state.can_player_use_resource():
|
||||
return 0.0
|
||||
var extracted := resource_state.extract()
|
||||
var storage := economy.get_storage_for_resource(resource_state.get_resource_id())
|
||||
if storage == null:
|
||||
return 0.0
|
||||
var extracted := resource_state.extract(
|
||||
minf(resource_state.get_yield_per_action(), storage.get_available_capacity())
|
||||
)
|
||||
if extracted <= 0.0:
|
||||
return 0.0
|
||||
|
||||
if resource_state.get_resource_id() == SimulationIds.RESOURCE_FOOD:
|
||||
get_pantry().deposit(SimulationIds.RESOURCE_FOOD, extracted)
|
||||
_sync_village_food()
|
||||
elif resource_state.get_resource_id() == SimulationIds.RESOURCE_WOOD:
|
||||
get_woodpile().deposit(SimulationIds.RESOURCE_WOOD, extracted)
|
||||
_sync_village_wood()
|
||||
else:
|
||||
village.apply_resource_delta(resource_state.get_resource_id(), extracted)
|
||||
var deposited := economy.deposit_resource(resource_state.get_resource_id(), extracted)
|
||||
_record_economic_event(
|
||||
SimulationIds.EVENT_RESOURCE_EXTRACTED,
|
||||
-1,
|
||||
@@ -904,7 +685,7 @@ func harvest_resource_node(node: ResourceNode) -> float:
|
||||
)
|
||||
),
|
||||
resource_state.get_resource_id(),
|
||||
extracted
|
||||
deposited
|
||||
)
|
||||
if resource_state.get_amount_remaining() <= 0.0:
|
||||
record_narrative_event(
|
||||
@@ -922,43 +703,21 @@ func harvest_resource_node(node: ResourceNode) -> float:
|
||||
resource_state.get_node_id()
|
||||
)
|
||||
|
||||
return extracted
|
||||
return deposited
|
||||
|
||||
|
||||
func eat_food(amount: float) -> void:
|
||||
get_pantry().withdraw(SimulationIds.RESOURCE_FOOD, amount)
|
||||
_sync_village_food()
|
||||
village_changed.emit(village)
|
||||
|
||||
|
||||
func add_food(amount: float) -> void:
|
||||
if amount >= 0.0:
|
||||
get_pantry().deposit(SimulationIds.RESOURCE_FOOD, amount)
|
||||
else:
|
||||
get_pantry().withdraw(SimulationIds.RESOURCE_FOOD, -amount)
|
||||
_sync_village_food()
|
||||
village_changed.emit(village)
|
||||
|
||||
|
||||
func add_wood(amount: float) -> void:
|
||||
var woodpile := get_woodpile()
|
||||
if woodpile == null:
|
||||
return
|
||||
if amount >= 0.0:
|
||||
woodpile.deposit(SimulationIds.RESOURCE_WOOD, amount)
|
||||
else:
|
||||
woodpile.withdraw(SimulationIds.RESOURCE_WOOD, -amount)
|
||||
_sync_village_wood()
|
||||
economy.withdraw_resource(SimulationIds.RESOURCE_FOOD, amount)
|
||||
village_changed.emit(village)
|
||||
|
||||
|
||||
func add_safety(amount: float) -> void:
|
||||
village.apply_resource_delta(&"safety", amount)
|
||||
village.apply_metric_delta(&"safety", amount)
|
||||
village_changed.emit(village)
|
||||
|
||||
|
||||
func add_knowledge(amount: float) -> void:
|
||||
village.apply_resource_delta(&"knowledge", amount)
|
||||
village.apply_metric_delta(&"knowledge", amount)
|
||||
village_changed.emit(village)
|
||||
|
||||
|
||||
@@ -1081,17 +840,14 @@ func restore_state(record: SimulationStateRecord) -> bool:
|
||||
simulation_seed = int(record.simulation["seed"])
|
||||
tick_interval = float(record.simulation["tick_interval"])
|
||||
tick_count = int(record.simulation["tick_count"])
|
||||
next_event_id = int(record.simulation["next_event_id"])
|
||||
clock = SimulationClock.new(tick_interval)
|
||||
clock.accumulator = float(record.simulation["clock_accumulator"])
|
||||
clock.elapsed_ticks = int(record.simulation["clock_elapsed_ticks"])
|
||||
clock.cycle_duration_seconds = float(record.simulation.get("cycle_duration_seconds", 240.0))
|
||||
village = record.village.restore(debug_logs)
|
||||
storage_states.clear()
|
||||
for storage_record in record.storages:
|
||||
storage_states[storage_record.get_storage_id()] = storage_record
|
||||
_sync_village_food()
|
||||
economic_events = record.economic_events.duplicate()
|
||||
economy.configure(village, debug_logs)
|
||||
economy.restore_storage(record.storages)
|
||||
event_log.restore(record.economic_events, int(record.simulation["next_event_id"]))
|
||||
latest_decisions.clear()
|
||||
|
||||
npcs.clear()
|
||||
|
||||
@@ -88,18 +88,6 @@ func select_action(npc: SimNPC, village: SimVillage, time_of_day: float = 0.5, a
|
||||
SimulationIds.ACTION_GATHER_FOOD, -1.0, "Meal-time hunger; pantry is empty"
|
||||
)
|
||||
|
||||
if npc.is_starving:
|
||||
if npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD) >= 1.0:
|
||||
return ActionSelectionResult.new(
|
||||
SimulationIds.ACTION_EAT, 1.0, "Starving and carrying food"
|
||||
)
|
||||
if village.food > 0:
|
||||
return ActionSelectionResult.new(
|
||||
SimulationIds.ACTION_WITHDRAW_FOOD, 1.0, "Starving; pantry has food"
|
||||
)
|
||||
return ActionSelectionResult.new(
|
||||
SimulationIds.ACTION_GATHER_FOOD, 4.0, "Starving; pantry is empty"
|
||||
)
|
||||
if npc.hunger > 75.0:
|
||||
if npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD) >= 1.0:
|
||||
return ActionSelectionResult.new(
|
||||
|
||||
@@ -40,3 +40,7 @@ const EVENT_NPC_DIED := &"npc_died"
|
||||
const EVENT_TASK_STARTED := &"task_started"
|
||||
const EVENT_TASK_BLOCKED := &"task_blocked"
|
||||
const EVENT_RESOURCE_DEPLETED := &"resource_depleted"
|
||||
|
||||
|
||||
static func npc_inventory_id(npc_id: int) -> StringName:
|
||||
return StringName("npc_%d_inventory" % npc_id)
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
extends RefCounted
|
||||
|
||||
signal inventory_changed(npc: SimNPC, item_id: StringName, amount: float)
|
||||
signal economic_event_requested(
|
||||
event_type: StringName,
|
||||
actor_id: int,
|
||||
source_id: StringName,
|
||||
destination_id: StringName,
|
||||
item_id: StringName,
|
||||
amount: float
|
||||
)
|
||||
signal narrative_event_requested(
|
||||
event_type: StringName, actor_id: int, source_id: StringName, action_display: String
|
||||
)
|
||||
|
||||
var village: SimVillage
|
||||
var storage_states: Dictionary = {}
|
||||
var debug_logs := false
|
||||
|
||||
|
||||
func configure(village_state: SimVillage, should_debug: bool) -> void:
|
||||
village = village_state
|
||||
debug_logs = should_debug
|
||||
|
||||
|
||||
func initialize_storage() -> void:
|
||||
if village == null:
|
||||
return
|
||||
if not storage_states.has(SimulationIds.STORAGE_VILLAGE_PANTRY):
|
||||
storage_states[SimulationIds.STORAGE_VILLAGE_PANTRY] = StorageStateRecord.create(
|
||||
SimulationIds.STORAGE_VILLAGE_PANTRY,
|
||||
{String(SimulationIds.RESOURCE_FOOD): village.food}
|
||||
)
|
||||
if not storage_states.has(SimulationIds.STORAGE_VILLAGE_WOODPILE):
|
||||
storage_states[SimulationIds.STORAGE_VILLAGE_WOODPILE] = StorageStateRecord.create(
|
||||
SimulationIds.STORAGE_VILLAGE_WOODPILE,
|
||||
{String(SimulationIds.RESOURCE_WOOD): village.wood}
|
||||
)
|
||||
sync_all()
|
||||
|
||||
|
||||
func restore_storage(records: Array[StorageStateRecord]) -> void:
|
||||
storage_states.clear()
|
||||
for storage_record in records:
|
||||
storage_states[storage_record.get_storage_id()] = storage_record
|
||||
# Older saves may not contain every authored storage. Preserve their village
|
||||
# aggregates by creating only the missing records before synchronizing.
|
||||
initialize_storage()
|
||||
|
||||
|
||||
func get_storage(storage_id: StringName) -> StorageStateRecord:
|
||||
return storage_states.get(storage_id) as StorageStateRecord
|
||||
|
||||
|
||||
func get_pantry() -> StorageStateRecord:
|
||||
return get_storage(SimulationIds.STORAGE_VILLAGE_PANTRY)
|
||||
|
||||
|
||||
func get_woodpile() -> StorageStateRecord:
|
||||
return get_storage(SimulationIds.STORAGE_VILLAGE_WOODPILE)
|
||||
|
||||
|
||||
func get_storage_for_resource(resource_id: StringName) -> StorageStateRecord:
|
||||
match resource_id:
|
||||
SimulationIds.RESOURCE_FOOD:
|
||||
return get_pantry()
|
||||
SimulationIds.RESOURCE_WOOD:
|
||||
return get_woodpile()
|
||||
return null
|
||||
|
||||
|
||||
func sync_all() -> void:
|
||||
if village == null:
|
||||
return
|
||||
var pantry := get_pantry()
|
||||
var woodpile := get_woodpile()
|
||||
if pantry != null:
|
||||
village.food = pantry.get_amount(SimulationIds.RESOURCE_FOOD)
|
||||
if woodpile != null:
|
||||
village.wood = woodpile.get_amount(SimulationIds.RESOURCE_WOOD)
|
||||
_refresh_village_scores()
|
||||
|
||||
|
||||
func sync_resource(resource_id: StringName) -> void:
|
||||
if village == null:
|
||||
return
|
||||
var storage := get_storage_for_resource(resource_id)
|
||||
if storage == null:
|
||||
return
|
||||
match resource_id:
|
||||
SimulationIds.RESOURCE_FOOD:
|
||||
village.food = storage.get_amount(resource_id)
|
||||
SimulationIds.RESOURCE_WOOD:
|
||||
village.wood = storage.get_amount(resource_id)
|
||||
_refresh_village_scores()
|
||||
|
||||
|
||||
func deposit_inventory(npc: SimNPC, item_id: StringName) -> float:
|
||||
var storage := get_storage_for_resource(item_id)
|
||||
if storage == null:
|
||||
return 0.0
|
||||
var deposited := storage.deposit(item_id, npc.get_inventory_amount(item_id))
|
||||
npc.remove_inventory(item_id, deposited)
|
||||
sync_resource(item_id)
|
||||
if deposited > 0.0:
|
||||
inventory_changed.emit(npc, item_id, npc.get_inventory_amount(item_id))
|
||||
economic_event_requested.emit(
|
||||
SimulationIds.EVENT_STORAGE_DEPOSITED,
|
||||
npc.id,
|
||||
SimulationIds.npc_inventory_id(npc.id),
|
||||
storage.get_storage_id(),
|
||||
item_id,
|
||||
deposited
|
||||
)
|
||||
return deposited
|
||||
|
||||
|
||||
func withdraw_to_inventory(npc: SimNPC, item_id: StringName, amount: float) -> float:
|
||||
var storage := get_storage_for_resource(item_id)
|
||||
if storage == null:
|
||||
return 0.0
|
||||
var withdrawn := storage.withdraw(item_id, amount)
|
||||
npc.add_inventory(item_id, withdrawn)
|
||||
sync_resource(item_id)
|
||||
if withdrawn > 0.0:
|
||||
inventory_changed.emit(npc, item_id, npc.get_inventory_amount(item_id))
|
||||
economic_event_requested.emit(
|
||||
SimulationIds.EVENT_STORAGE_WITHDRAWN,
|
||||
npc.id,
|
||||
storage.get_storage_id(),
|
||||
SimulationIds.npc_inventory_id(npc.id),
|
||||
item_id,
|
||||
withdrawn
|
||||
)
|
||||
return withdrawn
|
||||
|
||||
|
||||
func consume_npc_food(npc: SimNPC) -> bool:
|
||||
if npc.remove_inventory(SimulationIds.RESOURCE_FOOD, 1.0) < 1.0:
|
||||
npc.hunger = minf(npc.hunger + 5.0, 100.0)
|
||||
npc.is_starving = npc.hunger >= 90.0
|
||||
return false
|
||||
npc.hunger = maxf(npc.hunger - 55.0, 0.0)
|
||||
npc.starvation_ticks = 0
|
||||
npc.is_starving = npc.hunger >= 90.0
|
||||
inventory_changed.emit(
|
||||
npc,
|
||||
SimulationIds.RESOURCE_FOOD,
|
||||
npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD)
|
||||
)
|
||||
economic_event_requested.emit(
|
||||
SimulationIds.EVENT_ITEM_CONSUMED,
|
||||
npc.id,
|
||||
SimulationIds.npc_inventory_id(npc.id),
|
||||
&"consumed",
|
||||
SimulationIds.RESOURCE_FOOD,
|
||||
1.0
|
||||
)
|
||||
return true
|
||||
|
||||
|
||||
func consume_completion_cost(npc: SimNPC, definition: ActionDefinition) -> bool:
|
||||
if definition == null or not definition.has_completion_cost():
|
||||
return true
|
||||
var resource_id := definition.completion_cost_resource_id
|
||||
var required_amount := definition.completion_cost_amount
|
||||
var storage := get_storage_for_resource(resource_id)
|
||||
var available := storage.get_amount(resource_id) if storage != null else 0.0
|
||||
if storage == null or available < required_amount:
|
||||
var reason := "%s: needs %.0f %s (%.1f available)" % [
|
||||
definition.display_name,
|
||||
required_amount,
|
||||
String(resource_id).capitalize(),
|
||||
available,
|
||||
]
|
||||
narrative_event_requested.emit(
|
||||
SimulationIds.EVENT_TASK_BLOCKED,
|
||||
npc.id,
|
||||
storage.get_storage_id() if storage != null else &"",
|
||||
reason
|
||||
)
|
||||
return false
|
||||
var consumed := storage.withdraw(resource_id, required_amount)
|
||||
sync_resource(resource_id)
|
||||
if consumed < required_amount:
|
||||
return false
|
||||
economic_event_requested.emit(
|
||||
SimulationIds.EVENT_ITEM_CONSUMED,
|
||||
npc.id,
|
||||
storage.get_storage_id(),
|
||||
&"consumed",
|
||||
resource_id,
|
||||
consumed
|
||||
)
|
||||
if debug_logs:
|
||||
print(
|
||||
"[VillageEconomy] %s consumed %.1f %s for %s"
|
||||
% [npc.npc_name, consumed, resource_id, definition.action_id]
|
||||
)
|
||||
return true
|
||||
|
||||
|
||||
func deposit_resource(resource_id: StringName, amount: float) -> float:
|
||||
var storage := get_storage_for_resource(resource_id)
|
||||
if storage == null:
|
||||
return 0.0
|
||||
var deposited := storage.deposit(resource_id, amount)
|
||||
sync_resource(resource_id)
|
||||
return deposited
|
||||
|
||||
|
||||
func withdraw_resource(resource_id: StringName, amount: float) -> float:
|
||||
var storage := get_storage_for_resource(resource_id)
|
||||
if storage == null:
|
||||
return 0.0
|
||||
var withdrawn := storage.withdraw(resource_id, amount)
|
||||
sync_resource(resource_id)
|
||||
return withdrawn
|
||||
|
||||
|
||||
func _refresh_village_scores() -> void:
|
||||
village.update_modifiers()
|
||||
village.update_priorities()
|
||||
@@ -0,0 +1 @@
|
||||
uid://boecv7w7txoj0
|
||||
@@ -0,0 +1,98 @@
|
||||
extends RefCounted
|
||||
|
||||
signal event_recorded(event: EconomicEventRecord)
|
||||
|
||||
const DEFAULT_RATE_WINDOW_TICKS := 200
|
||||
const DEFAULT_TICKS_PER_DAY := 200.0
|
||||
|
||||
var events: Array[EconomicEventRecord] = []
|
||||
var next_event_id := 0
|
||||
|
||||
|
||||
func record_economic(
|
||||
tick: int,
|
||||
event_type: StringName,
|
||||
actor_id: int,
|
||||
source_id: StringName,
|
||||
destination_id: StringName,
|
||||
item_id: StringName,
|
||||
amount: float
|
||||
) -> EconomicEventRecord:
|
||||
if amount <= 0.0:
|
||||
return null
|
||||
var event := EconomicEventRecord.create(
|
||||
next_event_id, event_type, tick, actor_id, source_id, destination_id, item_id, amount
|
||||
)
|
||||
_append(event)
|
||||
return event
|
||||
|
||||
|
||||
func record_narrative(
|
||||
tick: int,
|
||||
event_type: StringName,
|
||||
actor_id: int,
|
||||
source_id: StringName = &"",
|
||||
action_display: String = ""
|
||||
) -> EconomicEventRecord:
|
||||
var event := EconomicEventRecord.create_narrative(
|
||||
next_event_id, event_type, tick, actor_id, source_id, action_display
|
||||
)
|
||||
_append(event)
|
||||
return event
|
||||
|
||||
|
||||
func get_for_actor(actor_id: int, max_count: int = 8) -> Array[EconomicEventRecord]:
|
||||
var results: Array[EconomicEventRecord] = []
|
||||
for index in range(events.size() - 1, -1, -1):
|
||||
var event := events[index]
|
||||
if int(event.data["actor_id"]) != actor_id:
|
||||
continue
|
||||
results.append(event)
|
||||
if results.size() >= max_count:
|
||||
break
|
||||
results.reverse()
|
||||
return results
|
||||
|
||||
|
||||
func get_recent(max_count: int = 5) -> Array[EconomicEventRecord]:
|
||||
var results: Array[EconomicEventRecord] = []
|
||||
var start := maxi(events.size() - max_count, 0)
|
||||
for index in range(start, events.size()):
|
||||
results.append(events[index])
|
||||
return results
|
||||
|
||||
|
||||
func get_consumption_rates(
|
||||
current_tick: int,
|
||||
window_ticks: int = DEFAULT_RATE_WINDOW_TICKS,
|
||||
ticks_per_day: float = DEFAULT_TICKS_PER_DAY
|
||||
) -> Dictionary:
|
||||
var consumed := {
|
||||
SimulationIds.RESOURCE_FOOD: 0.0,
|
||||
SimulationIds.RESOURCE_WOOD: 0.0,
|
||||
}
|
||||
var first_tick := maxi(current_tick - window_ticks, 0)
|
||||
for event in events:
|
||||
if int(event.data["tick"]) < first_tick:
|
||||
continue
|
||||
if StringName(event.data["event_type"]) != SimulationIds.EVENT_ITEM_CONSUMED:
|
||||
continue
|
||||
var item_id := StringName(event.data["item_id"])
|
||||
if consumed.has(item_id):
|
||||
consumed[item_id] = float(consumed[item_id]) + float(event.data["amount"])
|
||||
var elapsed := maxi(current_tick - first_tick, 1)
|
||||
return {
|
||||
"food_per_day": float(consumed[SimulationIds.RESOURCE_FOOD]) / elapsed * ticks_per_day,
|
||||
"wood_per_day": float(consumed[SimulationIds.RESOURCE_WOOD]) / elapsed * ticks_per_day,
|
||||
}
|
||||
|
||||
|
||||
func restore(restored_events: Array[EconomicEventRecord], restored_next_id: int) -> void:
|
||||
events = restored_events.duplicate()
|
||||
next_event_id = restored_next_id
|
||||
|
||||
|
||||
func _append(event: EconomicEventRecord) -> void:
|
||||
next_event_id += 1
|
||||
events.append(event)
|
||||
event_recorded.emit(event)
|
||||
@@ -0,0 +1 @@
|
||||
uid://cjgpqcofspncx
|
||||
@@ -177,6 +177,7 @@ static func _migrate_legacy(legacy_data: Dictionary, version: int) -> Dictionary
|
||||
if version == LEGACY_SCHEMA_VERSION:
|
||||
var village_data: Dictionary = legacy_data.get("village", {})
|
||||
var initial_food := float(village_data.get("food", 0.0))
|
||||
var initial_wood := float(village_data.get("wood", 0.0))
|
||||
migrated["storages"] = [
|
||||
(
|
||||
StorageStateRecord
|
||||
@@ -185,6 +186,14 @@ static func _migrate_legacy(legacy_data: Dictionary, version: int) -> Dictionary
|
||||
{String(SimulationIds.RESOURCE_FOOD): initial_food}
|
||||
)
|
||||
. to_dictionary()
|
||||
),
|
||||
(
|
||||
StorageStateRecord
|
||||
. create(
|
||||
SimulationIds.STORAGE_VILLAGE_WOODPILE,
|
||||
{String(SimulationIds.RESOURCE_WOOD): initial_wood}
|
||||
)
|
||||
. to_dictionary()
|
||||
)
|
||||
]
|
||||
migrated["economic_events"] = []
|
||||
|
||||
@@ -57,10 +57,12 @@ func get_total_amount() -> float:
|
||||
return total
|
||||
|
||||
|
||||
func get_available_capacity() -> float:
|
||||
return maxf(float(data["capacity"]) - get_total_amount(), 0.0)
|
||||
|
||||
|
||||
func deposit(item_id: StringName, requested_amount: float) -> float:
|
||||
var accepted := minf(
|
||||
maxf(requested_amount, 0.0), maxf(float(data["capacity"]) - get_total_amount(), 0.0)
|
||||
)
|
||||
var accepted := minf(maxf(requested_amount, 0.0), get_available_capacity())
|
||||
if accepted <= 0.0:
|
||||
return 0.0
|
||||
data["amounts"][String(item_id)] = get_amount(item_id) + accepted
|
||||
|
||||
Reference in New Issue
Block a user