316 lines
9.7 KiB
GDScript
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()
|