class_name VillageOpportunitySystem extends RefCounted const CARE_HUNGER_THRESHOLD := 80.0 var opportunities: Array[OpportunityStateRecord] = [] var next_opportunity_id := 0 func consider_event( event: EconomicEventRecord, current_tick: int, pantry: StorageStateRecord, npcs: Array[SimNPC], knowledge_system: EventKnowledgeSystem, resource_states: Dictionary, woodpile: StorageStateRecord = null ) -> OpportunityStateRecord: if event == null or pantry == null or current_tick < 0: return null var event_tick := int(event.data["tick"]) if event_tick < 0 or event_tick > current_tick: return null var resolved := _try_resolve(event, pantry, woodpile, npcs, resource_states) if resolved != null: return resolved var opened := _try_open_food(event, current_tick, pantry, npcs, knowledge_system) if opened != null: return opened if woodpile == null: return null return _try_open_wood(event, current_tick, woodpile, npcs, knowledge_system) func maintain_open_opportunity( current_tick: int, max_age: int, npcs: Array[SimNPC], pantry: StorageStateRecord, woodpile: StorageStateRecord ) -> OpportunityStateRecord: var opportunity := get_open_opportunity() if opportunity == null or current_tick < opportunity.get_created_tick(): return null if opportunity.get_opportunity_type() != SimulationIds.OPPORTUNITY_SUPPLY_MISSING_WOOD: return null var interested := _find_npc(opportunity.get_interested_npc_id(), npcs) if interested == null or interested.is_dead: return ( opportunity if opportunity.invalidate( SimulationIds.OPPORTUNITY_INVALIDATED_INTERESTED_DIED, current_tick ) else null ) var target_storage := _get_target_storage(opportunity, pantry, woodpile) var condition_is_stale := ( target_storage == null or ( target_storage.get_amount(opportunity.get_resource_id()) >= opportunity.get_target_amount() ) or current_tick - opportunity.get_created_tick() >= maxi(max_age, 1) ) if not condition_is_stale: return null return ( opportunity if opportunity.invalidate( SimulationIds.OPPORTUNITY_INVALIDATED_EVIDENCE_STALE, current_tick ) else null ) func restore(records: Array[OpportunityStateRecord], restored_next_id: int) -> void: opportunities = records.duplicate() opportunities.sort_custom(_sort_by_id) next_opportunity_id = maxi(restored_next_id, 0) func get_all_sorted() -> Array[OpportunityStateRecord]: var sorted := opportunities.duplicate() sorted.sort_custom(_sort_by_id) return sorted func get_open_sorted() -> Array[OpportunityStateRecord]: var open_records: Array[OpportunityStateRecord] = [] for opportunity in opportunities: if opportunity.is_open(): open_records.append(opportunity) open_records.sort_custom(_sort_by_id) return open_records func get_open_opportunity() -> OpportunityStateRecord: var open_records := get_open_sorted() return open_records[0] if not open_records.is_empty() else null func get_latest() -> OpportunityStateRecord: var sorted := get_all_sorted() return sorted[-1] if not sorted.is_empty() else null func get_latest_for_npc(npc_id: int) -> OpportunityStateRecord: for index in range(opportunities.size() - 1, -1, -1): var opportunity := opportunities[index] if opportunity.get_interested_npc_id() == npc_id: return opportunity return null func _try_open_food( event: EconomicEventRecord, current_tick: int, pantry: StorageStateRecord, npcs: Array[SimNPC], knowledge_system: EventKnowledgeSystem ) -> OpportunityStateRecord: if knowledge_system == null or get_open_opportunity() != null: return null if pantry.get_amount(SimulationIds.RESOURCE_FOOD) > 0.0: return null if not _is_food_withdrawal(event): return null var interested_npc := _select_interested_npc( int(event.data["event_id"]), npcs, knowledge_system ) if interested_npc == null: return null var opportunity := OpportunityStateRecord.create( next_opportunity_id, current_tick, int(event.data["event_id"]), interested_npc.id ) next_opportunity_id += 1 opportunities.append(opportunity) return opportunity func _try_open_wood( event: EconomicEventRecord, current_tick: int, woodpile: StorageStateRecord, npcs: Array[SimNPC], knowledge_system: EventKnowledgeSystem ) -> OpportunityStateRecord: if knowledge_system == null or get_open_opportunity() != null: return null if not _is_missing_wood_block(event): return null var target_amount := float(event.data.get("required_amount", 0.0)) if woodpile.get_amount(SimulationIds.RESOURCE_WOOD) >= target_amount: return null var actor := _find_npc(int(event.data["actor_id"]), npcs) if ( actor == null or actor.is_dead or not knowledge_system.knows_event(actor.id, int(event.data["event_id"])) ): return null var opportunity := OpportunityStateRecord.create( next_opportunity_id, current_tick, int(event.data["event_id"]), actor.id, target_amount, SimulationIds.OPPORTUNITY_SUPPLY_MISSING_WOOD, SimulationIds.STORAGE_VILLAGE_WOODPILE, SimulationIds.RESOURCE_WOOD ) next_opportunity_id += 1 opportunities.append(opportunity) return opportunity func _try_resolve( event: EconomicEventRecord, pantry: StorageStateRecord, woodpile: StorageStateRecord, npcs: Array[SimNPC], resource_states: Dictionary ) -> OpportunityStateRecord: var opportunity := get_open_opportunity() if opportunity == null: return null var target_storage := _get_target_storage(opportunity, pantry, woodpile) if ( target_storage == null or ( target_storage.get_amount(opportunity.get_resource_id()) < opportunity.get_target_amount() ) ): return null if not _is_valid_supply(event, opportunity, npcs, resource_states): return null if int(event.data["tick"]) < opportunity.get_created_tick(): return null if int(event.data["event_id"]) <= opportunity.get_trigger_event_id(): return null if not opportunity.resolve(int(event.data["event_id"]), int(event.data["tick"])): return null return opportunity func _select_interested_npc( event_id: int, npcs: Array[SimNPC], knowledge_system: EventKnowledgeSystem ) -> SimNPC: var candidates: Array[SimNPC] = [] for npc in npcs: if npc.is_dead or npc.hunger < CARE_HUNGER_THRESHOLD: continue if not knowledge_system.knows_event(npc.id, event_id): continue candidates.append(npc) candidates.sort_custom(_sort_by_care) return candidates[0] if not candidates.is_empty() else null static func _is_food_withdrawal(event: EconomicEventRecord) -> bool: var actor_id := int(event.data["actor_id"]) return ( StringName(event.data["event_type"]) == SimulationIds.EVENT_STORAGE_WITHDRAWN and actor_id >= 0 and StringName(event.data["source_id"]) == SimulationIds.STORAGE_VILLAGE_PANTRY and StringName(event.data["destination_id"]) == SimulationIds.npc_inventory_id(actor_id) and StringName(event.data["item_id"]) == SimulationIds.RESOURCE_FOOD and float(event.data["amount"]) > 0.0 ) static func _is_missing_wood_block(event: EconomicEventRecord) -> bool: return ( StringName(event.data["event_type"]) == SimulationIds.EVENT_TASK_BLOCKED and int(event.data["actor_id"]) >= 0 and StringName(event.data["source_id"]) == SimulationIds.STORAGE_VILLAGE_WOODPILE and StringName(event.data["item_id"]) == SimulationIds.RESOURCE_WOOD and ( StringName(event.data.get("action_id", &"")) in [SimulationIds.ACTION_PATROL, SimulationIds.ACTION_STUDY] ) and float(event.data.get("required_amount", 0.0)) > 0.0 ) static func _is_valid_supply( event: EconomicEventRecord, opportunity: OpportunityStateRecord, npcs: Array[SimNPC], resource_states: Dictionary ) -> bool: if ( StringName(event.data["item_id"]) != opportunity.get_resource_id() or StringName(event.data["destination_id"]) != opportunity.get_target_id() or float(event.data["amount"]) <= 0.0 ): return false var event_type := StringName(event.data["event_type"]) var actor_id := int(event.data["actor_id"]) if event_type == SimulationIds.EVENT_STORAGE_DEPOSITED: return ( _find_npc(actor_id, npcs) != null and StringName(event.data["source_id"]) == SimulationIds.npc_inventory_id(actor_id) ) if event_type != SimulationIds.EVENT_RESOURCE_EXTRACTED or actor_id != -1: return false var resource_state := ( resource_states.get(StringName(event.data["source_id"])) as ResourceStateRecord ) return ( resource_state != null and resource_state.get_resource_id() == opportunity.get_resource_id() and resource_state.can_player_use_resource() ) static func _get_target_storage( opportunity: OpportunityStateRecord, pantry: StorageStateRecord, woodpile: StorageStateRecord ) -> StorageStateRecord: match opportunity.get_target_id(): SimulationIds.STORAGE_VILLAGE_PANTRY: return pantry SimulationIds.STORAGE_VILLAGE_WOODPILE: return woodpile return null static func _sort_by_care(first: SimNPC, second: SimNPC) -> bool: if first.hunger != second.hunger: return first.hunger > second.hunger return first.id < second.id static func _find_npc(npc_id: int, npcs: Array[SimNPC]) -> SimNPC: for npc in npcs: if npc.id == npc_id: return npc return null static func _sort_by_id(first: OpportunityStateRecord, second: OpportunityStateRecord) -> bool: return first.get_opportunity_id() < second.get_opportunity_id()