c61d286005
Wood now follows the same gather-carry-deposit pattern as food. NPCs put harvested wood into inventory, walk to the VillageWoodpile StorageNode, and deposit it. Village wood syncs from storage. Player harvesting also routes wood to the woodpile. ActiveWorldAdapter exposes woodpile routing for DEPOSIT_WOOD action targeting. WorldViewManager recognizes wood inventory changes. Tests verify woodpile node existence, storage ID, navigation reachability, and adapter routing.
235 lines
6.6 KiB
GDScript
235 lines
6.6 KiB
GDScript
class_name ActionSelectionSystem
|
|
extends RefCounted
|
|
|
|
enum SchedulePeriod {
|
|
WORK,
|
|
SLEEP,
|
|
MEAL,
|
|
DISCRETIONARY
|
|
}
|
|
|
|
const SLEEP_BEGIN := 0.88
|
|
const SLEEP_END := 0.15
|
|
const BREAKFAST_BEGIN := 0.25
|
|
const BREAKFAST_END := 0.35
|
|
const DINNER_BEGIN := 0.7
|
|
const DINNER_END := 0.8
|
|
const WORK_BEGIN := 0.28
|
|
const WORK_END := 0.85
|
|
|
|
|
|
func select_action(npc: SimNPC, village: SimVillage, time_of_day: float = 0.5) -> ActionSelectionResult:
|
|
if npc.is_dead:
|
|
return null
|
|
|
|
if npc.is_starving:
|
|
if npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD) >= 1.0:
|
|
return ActionSelectionResult.new(
|
|
SimulationIds.ACTION_EAT, 1.0, "Starving and carrying food"
|
|
)
|
|
if village.food > 0:
|
|
return ActionSelectionResult.new(
|
|
SimulationIds.ACTION_WITHDRAW_FOOD, 1.0, "Starving; pantry has food"
|
|
)
|
|
return ActionSelectionResult.new(
|
|
SimulationIds.ACTION_GATHER_FOOD, -1.0, "Starving; must find food"
|
|
)
|
|
if npc.hunger > 80.0:
|
|
if npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD) >= 1.0:
|
|
return ActionSelectionResult.new(
|
|
SimulationIds.ACTION_EAT, -1.0, "Critically hungry and carrying food"
|
|
)
|
|
if village.food > 0:
|
|
return ActionSelectionResult.new(
|
|
SimulationIds.ACTION_WITHDRAW_FOOD, -1.0, "Critically hungry; pantry has food"
|
|
)
|
|
return ActionSelectionResult.new(
|
|
SimulationIds.ACTION_GATHER_FOOD, -1.0, "Critically hungry; must find food"
|
|
)
|
|
|
|
var period := _get_period(time_of_day)
|
|
|
|
if period == SchedulePeriod.SLEEP:
|
|
if npc.energy < 60.0:
|
|
return ActionSelectionResult.new(
|
|
SimulationIds.ACTION_SLEEP, -1.0, "Night-time sleep; energy is low"
|
|
)
|
|
if npc.hunger > 75.0 and npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD) >= 1.0:
|
|
return ActionSelectionResult.new(
|
|
SimulationIds.ACTION_EAT, -1.0, "Night-time hunger; eating from inventory"
|
|
)
|
|
return ActionSelectionResult.new(
|
|
SimulationIds.ACTION_SLEEP, -1.0, "Night-time; going home"
|
|
)
|
|
|
|
if period == SchedulePeriod.MEAL:
|
|
if npc.hunger > 50.0:
|
|
if npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD) >= 1.0:
|
|
return ActionSelectionResult.new(
|
|
SimulationIds.ACTION_EAT, -1.0, "Meal-time hunger; eating from inventory"
|
|
)
|
|
if village.food > 0:
|
|
return ActionSelectionResult.new(
|
|
SimulationIds.ACTION_WITHDRAW_FOOD, -1.0, "Meal-time; pantry has food"
|
|
)
|
|
return ActionSelectionResult.new(
|
|
SimulationIds.ACTION_GATHER_FOOD, -1.0, "Meal-time hunger; pantry is empty"
|
|
)
|
|
|
|
if npc.is_starving:
|
|
if npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD) >= 1.0:
|
|
return ActionSelectionResult.new(
|
|
SimulationIds.ACTION_EAT, 1.0, "Starving and carrying food"
|
|
)
|
|
if village.food > 0:
|
|
return ActionSelectionResult.new(
|
|
SimulationIds.ACTION_WITHDRAW_FOOD, 1.0, "Starving; pantry has food"
|
|
)
|
|
return ActionSelectionResult.new(
|
|
SimulationIds.ACTION_GATHER_FOOD, 4.0, "Starving; pantry is empty"
|
|
)
|
|
if npc.hunger > 75.0:
|
|
if npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD) >= 1.0:
|
|
return ActionSelectionResult.new(
|
|
SimulationIds.ACTION_EAT, -1.0, "Hungry and carrying food"
|
|
)
|
|
if village.food > 0:
|
|
return ActionSelectionResult.new(
|
|
SimulationIds.ACTION_WITHDRAW_FOOD, -1.0, "Hungry; pantry has food"
|
|
)
|
|
return ActionSelectionResult.new(
|
|
SimulationIds.ACTION_GATHER_FOOD, -1.0, "Hungry; pantry is empty"
|
|
)
|
|
if npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD) > 0.0:
|
|
return ActionSelectionResult.new(
|
|
SimulationIds.ACTION_DEPOSIT_FOOD, -1.0, "Carrying food for storage"
|
|
)
|
|
if npc.get_inventory_amount(SimulationIds.RESOURCE_WOOD) > 0.0:
|
|
return ActionSelectionResult.new(
|
|
SimulationIds.ACTION_DEPOSIT_WOOD, -1.0, "Carrying wood for storage"
|
|
)
|
|
if npc.energy < 25.0:
|
|
return ActionSelectionResult.new(
|
|
SimulationIds.ACTION_REST, -1.0, "Energy is below the rest threshold"
|
|
)
|
|
|
|
if period == SchedulePeriod.DISCRETIONARY:
|
|
var roll := npc.random_source.randf()
|
|
if roll < 0.3:
|
|
return ActionSelectionResult.new(
|
|
SimulationIds.ACTION_WANDER, -1.0, "Discretionary wander"
|
|
)
|
|
|
|
return _choose_best_work_action(npc, village)
|
|
|
|
|
|
func _choose_best_work_action(npc: SimNPC, village: SimVillage) -> ActionSelectionResult:
|
|
var scores := {
|
|
SimulationIds.ACTION_GATHER_FOOD:
|
|
_calculate_score(
|
|
npc,
|
|
SimulationIds.ACTION_GATHER_FOOD,
|
|
village.food_priority,
|
|
village.food,
|
|
15.0,
|
|
30.0,
|
|
3.0,
|
|
1.5
|
|
),
|
|
SimulationIds.ACTION_GATHER_WOOD:
|
|
_calculate_score(
|
|
npc,
|
|
SimulationIds.ACTION_GATHER_WOOD,
|
|
village.wood_priority,
|
|
village.wood,
|
|
10.0,
|
|
20.0,
|
|
2.5,
|
|
1.0
|
|
),
|
|
SimulationIds.ACTION_PATROL:
|
|
_calculate_score(
|
|
npc,
|
|
SimulationIds.ACTION_PATROL,
|
|
village.safety_priority,
|
|
village.safety,
|
|
30.0,
|
|
50.0,
|
|
3.0,
|
|
1.5
|
|
),
|
|
SimulationIds.ACTION_STUDY:
|
|
_calculate_score(
|
|
npc,
|
|
SimulationIds.ACTION_STUDY,
|
|
village.knowledge_priority,
|
|
village.knowledge,
|
|
10.0,
|
|
25.0,
|
|
1.5,
|
|
0.75
|
|
)
|
|
}
|
|
var best_action := SimulationIds.ACTION_GATHER_FOOD
|
|
var best_score: float = scores[best_action]
|
|
for action_id in [
|
|
SimulationIds.ACTION_GATHER_WOOD, SimulationIds.ACTION_PATROL, SimulationIds.ACTION_STUDY
|
|
]:
|
|
var score: float = scores[action_id]
|
|
if score > best_score:
|
|
best_action = action_id
|
|
best_score = score
|
|
return ActionSelectionResult.new(
|
|
best_action, -1.0, "Highest current work utility", scores
|
|
)
|
|
|
|
|
|
func _calculate_score(
|
|
npc: SimNPC,
|
|
action_id: StringName,
|
|
base_priority: float,
|
|
resource_amount: float,
|
|
critical_threshold: float,
|
|
low_threshold: float,
|
|
critical_bonus: float,
|
|
low_bonus: float
|
|
) -> float:
|
|
var score := base_priority
|
|
if resource_amount < critical_threshold:
|
|
score += critical_bonus
|
|
elif resource_amount < low_threshold:
|
|
score += low_bonus
|
|
var definition := SimulationDefinitions.get_action(action_id)
|
|
if (
|
|
definition != null
|
|
and not definition.preferred_profession_id.is_empty()
|
|
and npc.profession == definition.preferred_profession_id
|
|
):
|
|
score += 2.0
|
|
if action_id == npc.last_task:
|
|
score -= 1.5
|
|
score += npc.random_source.randf_range(0.0, 0.5)
|
|
if npc.debug_logs:
|
|
print("[ActionSelection] ", npc.npc_name, " score ", action_id, " = ", score)
|
|
return score
|
|
|
|
|
|
static func _get_period(time_of_day: float) -> int:
|
|
if _in_wrap_range(time_of_day, SLEEP_BEGIN, SLEEP_END):
|
|
return SchedulePeriod.SLEEP
|
|
if _in_range(time_of_day, BREAKFAST_BEGIN, BREAKFAST_END):
|
|
return SchedulePeriod.MEAL
|
|
if _in_range(time_of_day, DINNER_BEGIN, DINNER_END):
|
|
return SchedulePeriod.MEAL
|
|
if _in_range(time_of_day, WORK_BEGIN, WORK_END):
|
|
return SchedulePeriod.WORK
|
|
return SchedulePeriod.DISCRETIONARY
|
|
|
|
|
|
static func _in_range(value: float, begin: float, end: float) -> bool:
|
|
return value >= begin and value <= end
|
|
|
|
|
|
static func _in_wrap_range(value: float, begin: float, end: float) -> bool:
|
|
return value >= begin or value <= end
|