feat: add weak-villager and damaged-roof village needs

This commit is contained in:
Rijad Zuzo
2026-08-11 00:32:22 +02:00
parent 37f262fb84
commit 82e44a9f5d
10 changed files with 547 additions and 45 deletions
+14
View File
@@ -232,6 +232,8 @@ func _select_action_if_idle(npc: SimNPC, previous_state: StringName) -> void:
return
if npc.task_state not in [SimNPC.TASK_STATE_IDLE, SimNPC.TASK_STATE_COMPLETE]:
return
if _is_weak_villager_state(npc):
record_narrative_event(SimulationIds.EVENT_VILLAGER_WEAK, npc.id)
var helper := get_active_opportunity_helper()
var animal_feed_available := animal_care.has_available_feed_target(npc.id)
var selection := action_selector.select_action(
@@ -249,6 +251,16 @@ func _select_action_if_idle(npc: SimNPC, previous_state: StringName) -> void:
record_narrative_event(SimulationIds.EVENT_TASK_STARTED, npc.id, &"", display_name)
func _is_weak_villager_state(npc: SimNPC) -> bool:
return (
npc.is_starving
and npc.energy < 25.0
and village.food <= 0.0
and npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD) < 1.0
and opportunity_system.get_open_opportunity() == null
)
func _handle_npc_death(npc: SimNPC, previous_task: StringName, previous_target: StringName) -> void:
if not previous_target.is_empty():
release_npc_reservation(npc.id)
@@ -301,6 +313,8 @@ func _apply_action_completion(
npc.energy = minf(npc.energy + 40.0, 100.0)
npc.position = npc.home_position
record_narrative_event(SimulationIds.EVENT_NPC_SLEPT, npc.id)
if village.safety < 25.0 and opportunity_system.get_open_opportunity() == null:
record_narrative_event(SimulationIds.EVENT_HOME_DAMAGED, npc.id)
SimulationIds.ACTION_FEED_ANIMAL:
pass
SimulationIds.ACTION_PATROL, SimulationIds.ACTION_STUDY, SimulationIds.ACTION_REST, SimulationIds.ACTION_WANDER:
+4
View File
@@ -53,9 +53,13 @@ const EVENT_TASK_STARTED := &"task_started"
const EVENT_TASK_BLOCKED := &"task_blocked"
const EVENT_RESOURCE_DEPLETED := &"resource_depleted"
const EVENT_ANIMAL_FED := &"animal_fed"
const EVENT_VILLAGER_WEAK := &"villager_weak"
const EVENT_HOME_DAMAGED := &"home_damaged"
const OPPORTUNITY_RESTOCK_EMPTY_PANTRY := &"restock_empty_pantry"
const OPPORTUNITY_SUPPLY_MISSING_WOOD := &"supply_missing_wood"
const OPPORTUNITY_FEED_WEAK_VILLAGER := &"feed_weak_villager"
const OPPORTUNITY_REPAIR_HOME_ROOF := &"repair_home_roof"
const OPPORTUNITY_STATUS_OPEN := &"open"
const OPPORTUNITY_STATUS_RESOLVED := &"resolved"
const OPPORTUNITY_STATUS_INVALIDATED := &"invalidated"
@@ -248,6 +248,14 @@ static func is_knowable_event(event: EconomicEventRecord) -> bool:
)
and float(event.data.get("required_amount", 0.0)) > 0.0
)
if (
event_type
in [
SimulationIds.EVENT_VILLAGER_WEAK,
SimulationIds.EVENT_HOME_DAMAGED,
]
):
return int(event.data["actor_id"]) >= 0
if float(event.data["amount"]) <= 0.0:
return false
if event_type == SimulationIds.EVENT_STORAGE_DEPOSITED:
@@ -2,6 +2,7 @@ class_name VillageOpportunitySystem
extends RefCounted
const CARE_HUNGER_THRESHOLD := 80.0
const WEAK_ENERGY_THRESHOLD := 25.0
var opportunities: Array[OpportunityStateRecord] = []
var next_opportunity_id := 0
@@ -29,7 +30,13 @@ func consider_event(
return opened
if woodpile == null:
return null
return _try_open_wood(event, current_tick, woodpile, npcs, knowledge_system)
opened = _try_open_wood(event, current_tick, woodpile, npcs, knowledge_system)
if opened != null:
return opened
opened = _try_open_weak(event, current_tick, pantry, npcs, knowledge_system)
if opened != null:
return opened
return _try_open_roof(event, current_tick, woodpile, npcs, knowledge_system)
func maintain_open_opportunity(
@@ -42,7 +49,14 @@ func maintain_open_opportunity(
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:
if (
opportunity.get_opportunity_type()
not in [
SimulationIds.OPPORTUNITY_SUPPLY_MISSING_WOOD,
SimulationIds.OPPORTUNITY_FEED_WEAK_VILLAGER,
SimulationIds.OPPORTUNITY_REPAIR_HOME_ROOF,
]
):
return null
var interested := _find_npc(opportunity.get_interested_npc_id(), npcs)
if interested == null or interested.is_dead:
@@ -53,15 +67,17 @@ func maintain_open_opportunity(
)
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()
var condition_is_stale := current_tick - opportunity.get_created_tick() >= maxi(max_age, 1)
if opportunity.get_opportunity_type() != SimulationIds.OPPORTUNITY_FEED_WEAK_VILLAGER:
var target_storage := _get_target_storage(opportunity, pantry, woodpile)
condition_is_stale = (
condition_is_stale
or 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 (
@@ -452,6 +468,95 @@ func _try_open_wood(
return opportunity
func _try_open_weak(
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 not _is_weak_trigger(event):
return null
if pantry.get_amount(SimulationIds.RESOURCE_FOOD) > 0.0:
return null
var actor := _find_npc(int(event.data["actor_id"]), npcs)
if (
actor == null
or actor.is_dead
or actor.hunger < CARE_HUNGER_THRESHOLD
or actor.energy >= WEAK_ENERGY_THRESHOLD
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,
OpportunityStateRecord.DEFAULT_TARGET_AMOUNT,
SimulationIds.OPPORTUNITY_FEED_WEAK_VILLAGER,
SimulationIds.STORAGE_VILLAGE_PANTRY,
SimulationIds.RESOURCE_FOOD
)
next_opportunity_id += 1
opportunities.append(opportunity)
return opportunity
func _try_open_roof(
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_home_damaged_trigger(event):
return null
if (
woodpile.get_amount(SimulationIds.RESOURCE_WOOD)
>= OpportunityStateRecord.DEFAULT_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,
OpportunityStateRecord.DEFAULT_TARGET_AMOUNT,
SimulationIds.OPPORTUNITY_REPAIR_HOME_ROOF,
SimulationIds.STORAGE_VILLAGE_WOODPILE,
SimulationIds.RESOURCE_WOOD
)
next_opportunity_id += 1
opportunities.append(opportunity)
return opportunity
static func _is_weak_trigger(event: EconomicEventRecord) -> bool:
return (
StringName(event.data["event_type"]) == SimulationIds.EVENT_VILLAGER_WEAK
and int(event.data["actor_id"]) >= 0
)
static func _is_home_damaged_trigger(event: EconomicEventRecord) -> bool:
return (
StringName(event.data["event_type"]) == SimulationIds.EVENT_HOME_DAMAGED
and int(event.data["actor_id"]) >= 0
)
func _try_resolve(
event: EconomicEventRecord,
pantry: StorageStateRecord,
@@ -462,6 +567,17 @@ func _try_resolve(
var opportunity := get_open_opportunity()
if opportunity == null:
return null
if (
opportunity.get_opportunity_type() == SimulationIds.OPPORTUNITY_FEED_WEAK_VILLAGER
and _is_valid_supply(event, opportunity, npcs, resource_states)
):
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
var target_storage := _get_target_storage(opportunity, pantry, woodpile)
if (
target_storage == null
@@ -528,6 +644,13 @@ static func _is_valid_supply(
npcs: Array[SimNPC],
resource_states: Dictionary
) -> bool:
if opportunity.get_opportunity_type() == SimulationIds.OPPORTUNITY_FEED_WEAK_VILLAGER:
return (
StringName(event.data["event_type"]) == SimulationIds.EVENT_ITEM_CONSUMED
and int(event.data["actor_id"]) == opportunity.get_interested_npc_id()
and StringName(event.data["item_id"]) == SimulationIds.RESOURCE_FOOD
and float(event.data["amount"]) > 0.0
)
if (
StringName(event.data["item_id"]) != opportunity.get_resource_id()
or StringName(event.data["destination_id"]) != opportunity.get_target_id()
+2 -2
View File
@@ -95,13 +95,13 @@ static func from_dictionary(record_data: Dictionary) -> OpportunityStateRecord:
if opportunity_id < 0 or created_tick < 0 or trigger_event_id < 0 or interested_npc_id < 0:
return null
match opportunity_type:
SimulationIds.OPPORTUNITY_RESTOCK_EMPTY_PANTRY:
SimulationIds.OPPORTUNITY_RESTOCK_EMPTY_PANTRY, SimulationIds.OPPORTUNITY_FEED_WEAK_VILLAGER:
if (
target_id != SimulationIds.STORAGE_VILLAGE_PANTRY
or resource_id != SimulationIds.RESOURCE_FOOD
):
return null
SimulationIds.OPPORTUNITY_SUPPLY_MISSING_WOOD:
SimulationIds.OPPORTUNITY_SUPPLY_MISSING_WOOD, SimulationIds.OPPORTUNITY_REPAIR_HOME_ROOF:
if (
target_id != SimulationIds.STORAGE_VILLAGE_WOODPILE
or resource_id != SimulationIds.RESOURCE_WOOD
+61 -10
View File
@@ -573,6 +573,12 @@ static func _is_valid_opportunity(
SimulationIds.OPPORTUNITY_SUPPLY_MISSING_WOOD:
if not _is_valid_missing_wood_trigger(trigger_event, opportunity, npc_ids):
return false
SimulationIds.OPPORTUNITY_FEED_WEAK_VILLAGER:
if not _is_valid_weak_trigger(trigger_event, opportunity, npc_ids):
return false
SimulationIds.OPPORTUNITY_REPAIR_HOME_ROOF:
if not _is_valid_home_damaged_trigger(trigger_event, opportunity, npc_ids):
return false
_:
return false
var trigger_tick := int(trigger_event.data["tick"])
@@ -594,25 +600,41 @@ static func _is_valid_opportunity(
"%d:%d" % [opportunity.get_interested_npc_id(), opportunity.get_trigger_event_id()]
)
var evidence := knowledge_records_by_key.get(evidence_key) as KnownEventStateRecord
var is_weak_need := (
opportunity.get_opportunity_type() == SimulationIds.OPPORTUNITY_FEED_WEAK_VILLAGER
)
var interested_alive := (
interested_record != null and not bool(interested_record.data["is_dead"])
)
var storage_satisfied := (
is_weak_need
or (
target_storage != null
and (
target_storage.get_amount(opportunity.get_resource_id())
< opportunity.get_target_amount()
)
)
)
return (
resolution_event_id == OpportunityStateRecord.NO_EVENT_ID
and resolved_tick == -1
and interested_record != null
and (
opportunity.get_opportunity_type() != SimulationIds.OPPORTUNITY_SUPPLY_MISSING_WOOD
or not bool(interested_record.data["is_dead"])
)
and interested_alive
and target_storage != null
and (
target_storage.get_amount(opportunity.get_resource_id())
< opportunity.get_target_amount()
)
and storage_satisfied
and evidence != null
and evidence.get_acquired_tick() <= created_tick
)
if status == OpportunityStateRecord.STATUS_INVALIDATED:
if (
opportunity.get_opportunity_type() != SimulationIds.OPPORTUNITY_SUPPLY_MISSING_WOOD
(
opportunity.get_opportunity_type()
not in [
SimulationIds.OPPORTUNITY_SUPPLY_MISSING_WOOD,
SimulationIds.OPPORTUNITY_FEED_WEAK_VILLAGER,
SimulationIds.OPPORTUNITY_REPAIR_HOME_ROOF,
]
)
or resolution_event_id != OpportunityStateRecord.NO_EVENT_ID
or resolved_tick != -1
or opportunity.get_closed_tick() < created_tick
@@ -681,6 +703,26 @@ static func _is_valid_pantry_empty_trigger(event: EconomicEventRecord, npc_ids:
)
static func _is_valid_weak_trigger(
event: EconomicEventRecord, opportunity: OpportunityStateRecord, npc_ids: Dictionary
) -> bool:
return (
npc_ids.has(int(event.data["actor_id"]))
and int(event.data["actor_id"]) == opportunity.get_interested_npc_id()
and StringName(event.data["event_type"]) == SimulationIds.EVENT_VILLAGER_WEAK
)
static func _is_valid_home_damaged_trigger(
event: EconomicEventRecord, opportunity: OpportunityStateRecord, npc_ids: Dictionary
) -> bool:
return (
npc_ids.has(int(event.data["actor_id"]))
and int(event.data["actor_id"]) == opportunity.get_interested_npc_id()
and StringName(event.data["event_type"]) == SimulationIds.EVENT_HOME_DAMAGED
)
static func _is_valid_relationship_cause(
cause_event: EconomicEventRecord, subject_id: int, is_player_subject: bool
) -> bool:
@@ -721,6 +763,15 @@ static func _is_valid_supply_resolution(
) -> bool:
var actor_id := int(event.data["actor_id"])
var event_type := StringName(event.data["event_type"])
if (
opportunity.get_opportunity_type() == SimulationIds.OPPORTUNITY_FEED_WEAK_VILLAGER
and event_type == SimulationIds.EVENT_ITEM_CONSUMED
):
return (
actor_id == opportunity.get_interested_npc_id()
and StringName(event.data["item_id"]) == SimulationIds.RESOURCE_FOOD
and float(event.data["amount"]) > 0.0
)
if (
StringName(event.data["destination_id"]) != opportunity.get_target_id()
or StringName(event.data["item_id"]) != opportunity.get_resource_id()