Files
2026-08-13 00:08:30 +02:00

316 lines
9.7 KiB
GDScript

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,
action_id: StringName,
item_id: StringName,
required_amount: float
)
var village: SimVillage
var storage_states: Dictionary = {}
var debug_logs := false
var content_catalog: ContentCatalog
var storage_routing := StorageRoutingPolicy.new()
var _storage_id_by_item_id: Dictionary = {}
func configure(
village_state: SimVillage, should_debug: bool, configured_catalog: ContentCatalog = null
) -> void:
village = village_state
debug_logs = should_debug
content_catalog = (
configured_catalog if configured_catalog != null else ContentCatalog.create_core()
)
storage_routing = StorageRoutingPolicy.new(content_catalog)
_storage_id_by_item_id.clear()
func initialize_storage() -> void:
if village == null:
return
if content_catalog != null and content_catalog.is_valid():
for definition in content_catalog.get_storages():
if storage_states.has(definition.storage_id):
continue
var initial_amounts := {}
if definition.storage_id == SimulationIds.STORAGE_VILLAGE_PANTRY:
initial_amounts[String(SimulationIds.RESOURCE_FOOD)] = village.food
elif definition.storage_id == SimulationIds.STORAGE_VILLAGE_WOODPILE:
initial_amounts[String(SimulationIds.RESOURCE_WOOD)] = village.wood
var initial_total := 0.0
for amount in initial_amounts.values():
initial_total += float(amount)
storage_states[definition.storage_id] = StorageStateRecord.create(
definition.storage_id, initial_amounts, maxf(definition.capacity, initial_total)
)
_ensure_legacy_village_storage()
_rebuild_storage_route_cache()
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:
var storage_id := StringName(_storage_id_by_item_id.get(resource_id, &""))
return get_storage(storage_id) if not storage_id.is_empty() else null
func _ensure_legacy_village_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},
maxf(StorageStateRecord.DEFAULT_CAPACITY, 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},
maxf(StorageStateRecord.DEFAULT_CAPACITY, village.wood)
)
func _rebuild_storage_route_cache() -> void:
_storage_id_by_item_id.clear()
if storage_states.has(SimulationIds.STORAGE_VILLAGE_PANTRY):
_storage_id_by_item_id[SimulationIds.RESOURCE_FOOD] = (SimulationIds.STORAGE_VILLAGE_PANTRY)
if storage_states.has(SimulationIds.STORAGE_VILLAGE_WOODPILE):
_storage_id_by_item_id[SimulationIds.RESOURCE_WOOD] = (
SimulationIds.STORAGE_VILLAGE_WOODPILE
)
if content_catalog == null or not content_catalog.is_valid():
return
var available_ids: Array[StringName] = []
for storage_key in storage_states:
available_ids.append(StringName(storage_key))
for item in content_catalog.get_items():
var definition := storage_routing.resolve_definition(item.item_id, available_ids)
if definition != null:
_storage_id_by_item_id[item.item_id] = definition.storage_id
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 withdraw_exact_to_inventory(npc: SimNPC, item_id: StringName, amount: float) -> float:
if npc == null or not is_finite(amount) or amount <= 0.0:
return 0.0
var storage := get_storage_for_resource(item_id)
if storage == null or storage.get_amount(item_id) < amount:
return 0.0
return withdraw_to_inventory(npc, item_id, amount)
func remove_exact_from_inventory(npc: SimNPC, item_id: StringName, amount: float) -> float:
if (
npc == null
or not is_finite(amount)
or amount <= 0.0
or npc.get_inventory_amount(item_id) < amount
):
return 0.0
var removed := npc.remove_inventory(item_id, amount)
if not is_equal_approx(removed, amount):
npc.add_inventory(item_id, removed)
return 0.0
inventory_changed.emit(npc, item_id, npc.get_inventory_amount(item_id))
return removed
func restore_to_inventory(npc: SimNPC, item_id: StringName, amount: float) -> void:
if npc == null or not is_finite(amount) or amount <= 0.0:
return
npc.add_inventory(item_id, amount)
inventory_changed.emit(npc, item_id, npc.get_inventory_amount(item_id))
func consume_npc_food(npc: SimNPC) -> bool:
if npc == null:
return false
if npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD) < 1.0:
npc.hunger = minf(npc.hunger + 5.0, 100.0)
npc.is_starving = npc.hunger >= 90.0
return false
if remove_exact_from_inventory(npc, SimulationIds.RESOURCE_FOOD, 1.0) < 1.0:
return false
npc.hunger = maxf(npc.hunger - 55.0, 0.0)
npc.starvation_ticks = 0
npc.is_starving = npc.hunger >= 90.0
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,
definition.action_id,
resource_id,
required_amount
)
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()