595d67724f
NPCs who target the same resource or site concurrently gain mutual familiarity +0.1 per encounter, capped at 1.0. Action selection now receives the full NPC array and adds a +0.3 utility bonus for actions that familiar NPCs are currently performing - NPCs gravitate toward their housemates' professions. Inspector shows the highest-familiarity NPC with a percentage score under the resource lines.
245 lines
7.0 KiB
GDScript
245 lines
7.0 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, all_npcs: Array = []) -> 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, all_npcs)
|
|
|
|
|
|
func _choose_best_work_action(npc: SimNPC, village: SimVillage, all_npcs: Array) -> 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
|
|
)
|
|
}
|
|
for familiar_id in npc.familiarity:
|
|
if not familiar_id is int:
|
|
continue
|
|
var other_task: StringName
|
|
for other in all_npcs:
|
|
if other.id == familiar_id and not other.is_dead:
|
|
other_task = other.current_task
|
|
break
|
|
if other_task in scores:
|
|
scores[other_task] = float(scores[other_task]) + 0.3
|
|
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
|