feat: add missing wood opportunities

This commit is contained in:
Rijad Zuzo
2026-07-16 01:50:19 +02:00
parent f82d9683de
commit 79dd503343
26 changed files with 1054 additions and 175 deletions
@@ -0,0 +1,303 @@
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()