feat: define action completion costs

This commit is contained in:
Rijad Zuzo
2026-07-10 10:36:08 +02:00
parent 39989002ca
commit 0f7b12080e
19 changed files with 213 additions and 45 deletions
+55 -16
View File
@@ -179,7 +179,13 @@ func simulate_tick() -> void:
if not was_complete and npc.task_complete and not npc.is_dead:
var completed_task := npc.current_task
var completed_definition := SimulationDefinitions.get_action(completed_task)
if (
var completion_cost_paid := _consume_action_completion_cost(
npc, completed_definition
)
if not completion_cost_paid:
if not npc.target_id.is_empty():
release_npc_reservation(npc.id)
elif (
npc.target_id != &""
and completed_definition != null
and completed_definition.target_type == SimulationIds.TARGET_RESOURCE
@@ -271,11 +277,9 @@ func simulate_tick() -> void:
):
village.apply_npc_task(npc)
elif completed_task == SimulationIds.ACTION_PATROL:
if _consume_wood_for_work(npc, completed_task):
village.apply_npc_task(npc)
village.apply_npc_task(npc)
elif completed_task == SimulationIds.ACTION_STUDY:
if _consume_wood_for_work(npc, completed_task):
village.apply_npc_task(npc)
village.apply_npc_task(npc)
elif debug_logs:
print(
"[SimulationManager] ",
@@ -635,6 +639,15 @@ func get_woodpile() -> StorageStateRecord:
return storage_states.get(SimulationIds.STORAGE_VILLAGE_WOODPILE) as StorageStateRecord
func get_storage_for_resource(resource_id: StringName) -> StorageStateRecord:
match resource_id:
SimulationIds.RESOURCE_FOOD:
return get_pantry()
SimulationIds.RESOURCE_WOOD:
return get_woodpile()
return null
func register_loaded_storage_nodes() -> void:
for candidate in StorageNode.get_all():
var node := candidate as StorageNode
@@ -670,6 +683,14 @@ func _sync_village_wood() -> void:
village.update_priorities()
func _sync_village_resource(resource_id: StringName) -> void:
match resource_id:
SimulationIds.RESOURCE_FOOD:
_sync_village_food()
SimulationIds.RESOURCE_WOOD:
_sync_village_wood()
func deposit_npc_inventory(npc: SimNPC, item_id: StringName) -> float:
var available := npc.get_inventory_amount(item_id)
var deposited := get_pantry().deposit(item_id, available)
@@ -711,26 +732,44 @@ func deposit_npc_wood(npc: SimNPC) -> float:
return deposited
func _consume_wood_for_work(npc: SimNPC, action_id: StringName) -> bool:
var woodpile := get_woodpile()
if woodpile == null:
func _consume_action_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,
]
record_narrative_event(
SimulationIds.EVENT_TASK_BLOCKED,
npc.id,
storage.get_storage_id() if storage != null else &"",
reason
)
return false
if woodpile.get_amount(SimulationIds.RESOURCE_WOOD) < 1.0:
return false
var consumed := woodpile.withdraw(SimulationIds.RESOURCE_WOOD, 1.0)
_sync_village_wood()
if consumed < 1.0:
var consumed := storage.withdraw(resource_id, required_amount)
_sync_village_resource(resource_id)
if consumed < required_amount:
return false
_record_economic_event(
SimulationIds.EVENT_ITEM_CONSUMED,
npc.id,
SimulationIds.STORAGE_VILLAGE_WOODPILE,
storage.get_storage_id(),
&"consumed",
SimulationIds.RESOURCE_WOOD,
resource_id,
consumed
)
if debug_logs:
print("[SimulationManager] %s consumed wood for %s" % [npc.npc_name, action_id])
print(
"[SimulationManager] %s consumed %.1f %s for %s"
% [npc.npc_name, consumed, resource_id, definition.action_id]
)
return true
+4 -1
View File
@@ -5,15 +5,18 @@ var action_id: StringName
var duration_override := -1.0
var reason: String
var scores: Dictionary
var rejections: Dictionary
func _init(
selected_action_id: StringName,
selected_duration_override: float = -1.0,
decision_reason: String = "",
decision_scores: Dictionary = {}
decision_scores: Dictionary = {},
decision_rejections: Dictionary = {}
) -> void:
action_id = selected_action_id
duration_override = selected_duration_override
reason = decision_reason
scores = decision_scores.duplicate(true)
rejections = decision_rejections.duplicate(true)
+30 -4
View File
@@ -182,9 +182,13 @@ func _choose_best_work_action(npc: SimNPC, village: SimVillage, all_npcs: Array)
0.75
)
}
if village.wood < 1.0:
scores[SimulationIds.ACTION_PATROL] = UNAVAILABLE_ACTION_SCORE
scores[SimulationIds.ACTION_STUDY] = UNAVAILABLE_ACTION_SCORE
var rejections := {}
for action_id in scores:
var definition := SimulationDefinitions.get_action(action_id)
var rejection := _get_cost_rejection(definition, village)
if not rejection.is_empty():
scores[action_id] = UNAVAILABLE_ACTION_SCORE
rejections[action_id] = rejection
for familiar_id in npc.familiarity:
if not familiar_id is int:
continue
@@ -205,10 +209,32 @@ func _choose_best_work_action(npc: SimNPC, village: SimVillage, all_npcs: Array)
best_action = action_id
best_score = score
return ActionSelectionResult.new(
best_action, -1.0, "Highest current work utility", scores
best_action, -1.0, "Highest current work utility", scores, rejections
)
func _get_cost_rejection(definition: ActionDefinition, village: SimVillage) -> String:
if definition == null or not definition.has_completion_cost():
return ""
var available := _get_village_resource_amount(village, definition.completion_cost_resource_id)
if available >= definition.completion_cost_amount:
return ""
return "Needs %.0f %s (%.1f available)" % [
definition.completion_cost_amount,
String(definition.completion_cost_resource_id).capitalize(),
available,
]
func _get_village_resource_amount(village: SimVillage, resource_id: StringName) -> float:
match resource_id:
SimulationIds.RESOURCE_FOOD:
return village.food
SimulationIds.RESOURCE_WOOD:
return village.wood
return 0.0
func _calculate_score(
npc: SimNPC,
action_id: StringName,
@@ -7,6 +7,8 @@ extends Resource
@export var preferred_profession_id: StringName
@export var target_type: StringName = SimulationIds.TARGET_ACTIVITY
@export var resource_action_id: StringName
@export var completion_cost_resource_id: StringName
@export_range(0.0, 1000.0, 0.1) var completion_cost_amount := 0.0
func validate() -> Array[String]:
@@ -26,4 +28,12 @@ func validate() -> Array[String]:
errors.append("target_type '%s' is invalid for '%s'" % [target_type, action_id])
if target_type == SimulationIds.TARGET_RESOURCE and resource_action_id.is_empty():
errors.append("resource_action_id is required for '%s'" % action_id)
if completion_cost_resource_id.is_empty() and completion_cost_amount > 0.0:
errors.append("completion cost resource is required for '%s'" % action_id)
if not completion_cost_resource_id.is_empty() and completion_cost_amount <= 0.0:
errors.append("completion cost amount must be positive for '%s'" % action_id)
return errors
func has_completion_cost() -> bool:
return not completion_cost_resource_id.is_empty() and completion_cost_amount > 0.0
@@ -129,4 +129,15 @@ static func validate() -> Array[String]:
% [definition.action_id, definition.resource_action_id]
)
)
if (
not definition.completion_cost_resource_id.is_empty()
and definition.completion_cost_resource_id
not in [SimulationIds.RESOURCE_FOOD, SimulationIds.RESOURCE_WOOD]
):
errors.append(
(
"Action '%s' references unknown completion cost resource '%s'"
% [definition.action_id, definition.completion_cost_resource_id]
)
)
return errors
+1
View File
@@ -38,4 +38,5 @@ const EVENT_ITEM_CONSUMED := &"item_consumed"
const EVENT_NPC_SLEPT := &"npc_slept"
const EVENT_NPC_DIED := &"npc_died"
const EVENT_TASK_STARTED := &"task_started"
const EVENT_TASK_BLOCKED := &"task_blocked"
const EVENT_RESOURCE_DEPLETED := &"resource_depleted"
@@ -9,3 +9,5 @@ display_name = "Patrol"
default_duration = 6.0
preferred_profession_id = &"guard"
target_type = &"activity"
completion_cost_resource_id = &"wood"
completion_cost_amount = 1.0
@@ -9,3 +9,5 @@ display_name = "Study"
default_duration = 6.0
preferred_profession_id = &"scholar"
target_type = &"activity"
completion_cost_resource_id = &"wood"
completion_cost_amount = 1.0
+2
View File
@@ -121,6 +121,8 @@ func description(npc_names: Dictionary = {}) -> String:
return "%s died from starvation" % actor_name
"task_started":
return "%s began %s" % [actor_name, action_name if not action_name.is_empty() else item]
"task_blocked":
return "%s could not complete %s" % [actor_name, action_name]
"resource_depleted":
return "%s was depleted" % source
_: