feat: add simulation action definitions
This commit is contained in:
@@ -15,7 +15,7 @@ const DEBUG_LOGS := true
|
|||||||
var has_reported_arrival := false
|
var has_reported_arrival := false
|
||||||
var sim_id: int = -1
|
var sim_id: int = -1
|
||||||
var npc_name: String = ""
|
var npc_name: String = ""
|
||||||
var profession: String = ""
|
var profession: StringName
|
||||||
|
|
||||||
var current_path: PackedVector3Array = []
|
var current_path: PackedVector3Array = []
|
||||||
var path_index := 0
|
var path_index := 0
|
||||||
|
|||||||
+54
-56
@@ -1,20 +1,20 @@
|
|||||||
class_name SimNPC
|
class_name SimNPC
|
||||||
extends RefCounted
|
extends RefCounted
|
||||||
|
|
||||||
const TASK_STATE_IDLE := "idle"
|
const TASK_STATE_IDLE := &"idle"
|
||||||
const TASK_STATE_TRAVELING := "traveling"
|
const TASK_STATE_TRAVELING := &"traveling"
|
||||||
const TASK_STATE_WORKING := "working"
|
const TASK_STATE_WORKING := &"working"
|
||||||
const TASK_STATE_COMPLETE := "complete"
|
const TASK_STATE_COMPLETE := &"complete"
|
||||||
|
|
||||||
var id: int
|
var id: int
|
||||||
var npc_name: String
|
var npc_name: String
|
||||||
var profession: String
|
var profession: StringName
|
||||||
var hunger: float
|
var hunger: float
|
||||||
var energy: float
|
var energy: float
|
||||||
var strength: float
|
var strength: float
|
||||||
var intelligence: float
|
var intelligence: float
|
||||||
var current_task: String = "idle"
|
var current_task: StringName = SimulationIds.ACTION_IDLE
|
||||||
var task_state: String = TASK_STATE_IDLE
|
var task_state: StringName = TASK_STATE_IDLE
|
||||||
var position: Vector3
|
var position: Vector3
|
||||||
|
|
||||||
var is_starving := false
|
var is_starving := false
|
||||||
@@ -26,14 +26,14 @@ var task_duration := 0.0
|
|||||||
var task_progress := 0.0
|
var task_progress := 0.0
|
||||||
var task_complete := true
|
var task_complete := true
|
||||||
var target_id: StringName = &""
|
var target_id: StringName = &""
|
||||||
var last_task := ""
|
var last_task: StringName
|
||||||
var random_source: RandomNumberGenerator
|
var random_source: RandomNumberGenerator
|
||||||
var debug_logs := true
|
var debug_logs := true
|
||||||
|
|
||||||
func _init(
|
func _init(
|
||||||
_id: int,
|
_id: int,
|
||||||
_npc_name: String,
|
_npc_name: String,
|
||||||
_profession: String,
|
_profession: StringName,
|
||||||
_strength: float,
|
_strength: float,
|
||||||
_intelligence: float,
|
_intelligence: float,
|
||||||
_random_source: RandomNumberGenerator = null
|
_random_source: RandomNumberGenerator = null
|
||||||
@@ -87,7 +87,7 @@ func update_needs(village: SimVillage) -> void:
|
|||||||
TASK_STATE_TRAVELING:
|
TASK_STATE_TRAVELING:
|
||||||
energy -= 2.0
|
energy -= 2.0
|
||||||
TASK_STATE_WORKING:
|
TASK_STATE_WORKING:
|
||||||
if current_task != "rest":
|
if current_task != SimulationIds.ACTION_REST:
|
||||||
energy -= 3.0
|
energy -= 3.0
|
||||||
TASK_STATE_COMPLETE:
|
TASK_STATE_COMPLETE:
|
||||||
energy -= 0.25
|
energy -= 0.25
|
||||||
@@ -107,7 +107,7 @@ func update_needs(village: SimVillage) -> void:
|
|||||||
|
|
||||||
func die_from_starvation() -> void:
|
func die_from_starvation() -> void:
|
||||||
is_dead = true
|
is_dead = true
|
||||||
current_task = "dead"
|
current_task = SimulationIds.ACTION_DEAD
|
||||||
task_state = TASK_STATE_IDLE
|
task_state = TASK_STATE_IDLE
|
||||||
task_progress = 0.0
|
task_progress = 0.0
|
||||||
task_duration = 0.0
|
task_duration = 0.0
|
||||||
@@ -122,110 +122,97 @@ func choose_new_task(village: SimVillage) -> void:
|
|||||||
|
|
||||||
if is_starving:
|
if is_starving:
|
||||||
if village.food > 0:
|
if village.food > 0:
|
||||||
set_task("eat", 1.0)
|
set_task(SimulationIds.ACTION_EAT, 1.0)
|
||||||
else:
|
else:
|
||||||
set_task("gather_food", 4.0)
|
set_task(SimulationIds.ACTION_GATHER_FOOD, 4.0)
|
||||||
return
|
return
|
||||||
|
|
||||||
if hunger > 75.0:
|
if hunger > 75.0:
|
||||||
if village.food > 0:
|
if village.food > 0:
|
||||||
set_task("eat", 2.0)
|
set_task(SimulationIds.ACTION_EAT)
|
||||||
else:
|
else:
|
||||||
set_task("gather_food", 5.0)
|
set_task(SimulationIds.ACTION_GATHER_FOOD)
|
||||||
return
|
return
|
||||||
|
|
||||||
if energy < 25.0:
|
if energy < 25.0:
|
||||||
set_task("rest", 4.0)
|
set_task(SimulationIds.ACTION_REST)
|
||||||
return
|
return
|
||||||
|
|
||||||
var best_task := choose_best_work_task(village)
|
set_task(choose_best_work_task(village))
|
||||||
|
|
||||||
match best_task:
|
|
||||||
"gather_food":
|
|
||||||
set_task("gather_food", 5.0)
|
|
||||||
"gather_wood":
|
|
||||||
set_task("gather_wood", 5.0)
|
|
||||||
"patrol":
|
|
||||||
set_task("patrol", 6.0)
|
|
||||||
"study":
|
|
||||||
set_task("study", 6.0)
|
|
||||||
_:
|
|
||||||
set_task("wander", 4.0)
|
|
||||||
|
|
||||||
func should_eat_immediately_after_task() -> bool:
|
func should_eat_immediately_after_task() -> bool:
|
||||||
return current_task == "gather_food" and is_starving and not is_dead
|
return (
|
||||||
|
current_task == SimulationIds.ACTION_GATHER_FOOD
|
||||||
|
and is_starving
|
||||||
|
and not is_dead
|
||||||
|
)
|
||||||
|
|
||||||
func choose_best_work_task(village: SimVillage) -> String:
|
func choose_best_work_task(village: SimVillage) -> StringName:
|
||||||
var food_score := calculate_task_score(
|
var food_score := calculate_task_score(
|
||||||
"gather_food",
|
SimulationIds.ACTION_GATHER_FOOD,
|
||||||
village.food_priority,
|
village.food_priority,
|
||||||
village.food,
|
village.food,
|
||||||
15.0,
|
15.0,
|
||||||
30.0,
|
30.0,
|
||||||
3.0,
|
3.0,
|
||||||
1.5,
|
1.5
|
||||||
"farmer"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var wood_score := calculate_task_score(
|
var wood_score := calculate_task_score(
|
||||||
"gather_wood",
|
SimulationIds.ACTION_GATHER_WOOD,
|
||||||
village.wood_priority,
|
village.wood_priority,
|
||||||
village.wood,
|
village.wood,
|
||||||
10.0,
|
10.0,
|
||||||
20.0,
|
20.0,
|
||||||
2.5,
|
2.5,
|
||||||
1.0,
|
1.0
|
||||||
"woodcutter"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var safety_score := calculate_task_score(
|
var safety_score := calculate_task_score(
|
||||||
"patrol",
|
SimulationIds.ACTION_PATROL,
|
||||||
village.safety_priority,
|
village.safety_priority,
|
||||||
village.safety,
|
village.safety,
|
||||||
30.0,
|
30.0,
|
||||||
50.0,
|
50.0,
|
||||||
3.0,
|
3.0,
|
||||||
1.5,
|
1.5
|
||||||
"guard"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var knowledge_score := calculate_task_score(
|
var knowledge_score := calculate_task_score(
|
||||||
"study",
|
SimulationIds.ACTION_STUDY,
|
||||||
village.knowledge_priority,
|
village.knowledge_priority,
|
||||||
village.knowledge,
|
village.knowledge,
|
||||||
10.0,
|
10.0,
|
||||||
25.0,
|
25.0,
|
||||||
1.5,
|
1.5,
|
||||||
0.75,
|
0.75
|
||||||
"scholar"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var best_task := "gather_food"
|
var best_task := SimulationIds.ACTION_GATHER_FOOD
|
||||||
var best_score := food_score
|
var best_score := food_score
|
||||||
|
|
||||||
if wood_score > best_score:
|
if wood_score > best_score:
|
||||||
best_task = "gather_wood"
|
best_task = SimulationIds.ACTION_GATHER_WOOD
|
||||||
best_score = wood_score
|
best_score = wood_score
|
||||||
|
|
||||||
if safety_score > best_score:
|
if safety_score > best_score:
|
||||||
best_task = "patrol"
|
best_task = SimulationIds.ACTION_PATROL
|
||||||
best_score = safety_score
|
best_score = safety_score
|
||||||
|
|
||||||
if knowledge_score > best_score:
|
if knowledge_score > best_score:
|
||||||
best_task = "study"
|
best_task = SimulationIds.ACTION_STUDY
|
||||||
best_score = knowledge_score
|
best_score = knowledge_score
|
||||||
|
|
||||||
return best_task
|
return best_task
|
||||||
|
|
||||||
func calculate_task_score(
|
func calculate_task_score(
|
||||||
task_name: String,
|
action_id: StringName,
|
||||||
base_priority: float,
|
base_priority: float,
|
||||||
resource_amount: float,
|
resource_amount: float,
|
||||||
critical_threshold: float,
|
critical_threshold: float,
|
||||||
low_threshold: float,
|
low_threshold: float,
|
||||||
critical_bonus: float,
|
critical_bonus: float,
|
||||||
low_bonus: float,
|
low_bonus: float
|
||||||
preferred_profession: String
|
|
||||||
) -> float:
|
) -> float:
|
||||||
var score := base_priority
|
var score := base_priority
|
||||||
|
|
||||||
@@ -234,10 +221,15 @@ func calculate_task_score(
|
|||||||
elif resource_amount < low_threshold:
|
elif resource_amount < low_threshold:
|
||||||
score += low_bonus
|
score += low_bonus
|
||||||
|
|
||||||
if profession == preferred_profession:
|
var definition := SimulationDefinitions.get_action(action_id)
|
||||||
|
if (
|
||||||
|
definition != null
|
||||||
|
and not definition.preferred_profession_id.is_empty()
|
||||||
|
and profession == definition.preferred_profession_id
|
||||||
|
):
|
||||||
score += 2.0
|
score += 2.0
|
||||||
|
|
||||||
if task_name == last_task:
|
if action_id == last_task:
|
||||||
score -= 1.5
|
score -= 1.5
|
||||||
|
|
||||||
score += random_source.randf_range(0.0, 0.5)
|
score += random_source.randf_range(0.0, 0.5)
|
||||||
@@ -247,16 +239,22 @@ func calculate_task_score(
|
|||||||
"[SimNPC] ",
|
"[SimNPC] ",
|
||||||
npc_name,
|
npc_name,
|
||||||
" score ",
|
" score ",
|
||||||
task_name,
|
action_id,
|
||||||
" = ",
|
" = ",
|
||||||
score
|
score
|
||||||
)
|
)
|
||||||
|
|
||||||
return score
|
return score
|
||||||
|
|
||||||
func set_task(task: String, duration: float) -> void:
|
func set_task(action_id: StringName, duration: float = -1.0) -> void:
|
||||||
current_task = task
|
var definition := SimulationDefinitions.get_action(action_id)
|
||||||
task_duration = duration
|
if definition == null:
|
||||||
|
push_error("SimNPC: unknown action_id '%s'" % action_id)
|
||||||
|
return
|
||||||
|
current_task = action_id
|
||||||
|
task_duration = (
|
||||||
|
duration if duration >= 0.0 else definition.default_duration
|
||||||
|
)
|
||||||
task_progress = 0.0
|
task_progress = 0.0
|
||||||
task_complete = false
|
task_complete = false
|
||||||
task_state = TASK_STATE_TRAVELING
|
task_state = TASK_STATE_TRAVELING
|
||||||
|
|||||||
@@ -101,15 +101,15 @@ func apply_npc_task(npc: SimNPC) -> void:
|
|||||||
productivity = 0.5
|
productivity = 0.5
|
||||||
|
|
||||||
match npc.current_task:
|
match npc.current_task:
|
||||||
"gather_food":
|
SimulationIds.ACTION_GATHER_FOOD:
|
||||||
food += 2.0 * productivity
|
food += 2.0 * productivity
|
||||||
"gather_wood":
|
SimulationIds.ACTION_GATHER_WOOD:
|
||||||
wood += 2.0 * productivity
|
wood += 2.0 * productivity
|
||||||
"patrol":
|
SimulationIds.ACTION_PATROL:
|
||||||
safety += 1.0 * productivity
|
safety += 1.0 * productivity
|
||||||
"study":
|
SimulationIds.ACTION_STUDY:
|
||||||
knowledge += 1.0 * productivity
|
knowledge += 1.0 * productivity
|
||||||
"eat":
|
SimulationIds.ACTION_EAT:
|
||||||
if food > 0:
|
if food > 0:
|
||||||
food -= 1.0
|
food -= 1.0
|
||||||
npc.hunger = max(npc.hunger - 55.0, 0.0)
|
npc.hunger = max(npc.hunger - 55.0, 0.0)
|
||||||
@@ -118,14 +118,14 @@ func apply_npc_task(npc: SimNPC) -> void:
|
|||||||
else:
|
else:
|
||||||
npc.hunger = min(npc.hunger + 5.0, 100.0)
|
npc.hunger = min(npc.hunger + 5.0, 100.0)
|
||||||
npc.is_starving = npc.hunger >= 90.0
|
npc.is_starving = npc.hunger >= 90.0
|
||||||
"rest":
|
SimulationIds.ACTION_REST:
|
||||||
if npc.is_starving:
|
if npc.is_starving:
|
||||||
npc.energy = min(npc.energy + 2.0, 100.0)
|
npc.energy = min(npc.energy + 2.0, 100.0)
|
||||||
elif npc.hunger > 75.0:
|
elif npc.hunger > 75.0:
|
||||||
npc.energy = min(npc.energy + 12.0, 100.0)
|
npc.energy = min(npc.energy + 12.0, 100.0)
|
||||||
else:
|
else:
|
||||||
npc.energy = min(npc.energy + 35.0, 100.0)
|
npc.energy = min(npc.energy + 35.0, 100.0)
|
||||||
"wander":
|
SimulationIds.ACTION_WANDER:
|
||||||
safety -= 0.2
|
safety -= 0.2
|
||||||
|
|
||||||
food = max(food, 0.0)
|
food = max(food, 0.0)
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
extends Node
|
extends Node
|
||||||
|
|
||||||
signal npc_task_changed(npc: SimNPC, old_task: String, new_task: String)
|
signal npc_task_changed(
|
||||||
|
npc: SimNPC,
|
||||||
|
old_task: StringName,
|
||||||
|
new_task: StringName
|
||||||
|
)
|
||||||
signal village_changed(village: SimVillage)
|
signal village_changed(village: SimVillage)
|
||||||
signal npc_died(npc: SimNPC)
|
signal npc_died(npc: SimNPC)
|
||||||
|
|
||||||
@@ -20,16 +24,14 @@ var names := [
|
|||||||
"Amina", "Tarik", "Jasmin"
|
"Amina", "Tarik", "Jasmin"
|
||||||
]
|
]
|
||||||
|
|
||||||
var professions := [
|
|
||||||
"farmer",
|
|
||||||
"woodcutter",
|
|
||||||
"guard",
|
|
||||||
"scholar",
|
|
||||||
"wanderer"
|
|
||||||
]
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
add_to_group("simulation_manager")
|
add_to_group("simulation_manager")
|
||||||
|
var definition_errors := SimulationDefinitions.validate()
|
||||||
|
if not definition_errors.is_empty():
|
||||||
|
for error in definition_errors:
|
||||||
|
push_error("SimulationManager: " + error)
|
||||||
|
set_process(false)
|
||||||
|
return
|
||||||
clock = SimulationClock.new(tick_interval)
|
clock = SimulationClock.new(tick_interval)
|
||||||
village.debug_logs = debug_logs
|
village.debug_logs = debug_logs
|
||||||
village.update_modifiers()
|
village.update_modifiers()
|
||||||
@@ -45,13 +47,17 @@ func _process(delta: float) -> void:
|
|||||||
simulate_tick()
|
simulate_tick()
|
||||||
|
|
||||||
func generate_npcs() -> void:
|
func generate_npcs() -> void:
|
||||||
|
var profession_ids := SimulationDefinitions.get_profession_ids()
|
||||||
for i in range(names.size()):
|
for i in range(names.size()):
|
||||||
var npc_random := _create_random_source(i, 0)
|
var npc_random := _create_random_source(i, 0)
|
||||||
var profession_index := npc_random.randi_range(0, professions.size() - 1)
|
var profession_index := npc_random.randi_range(
|
||||||
|
0,
|
||||||
|
profession_ids.size() - 1
|
||||||
|
)
|
||||||
var npc := SimNPC.new(
|
var npc := SimNPC.new(
|
||||||
i,
|
i,
|
||||||
names[i],
|
names[i],
|
||||||
professions[profession_index],
|
profession_ids[profession_index],
|
||||||
npc_random.randf_range(1.0, 10.0),
|
npc_random.randf_range(1.0, 10.0),
|
||||||
npc_random.randf_range(1.0, 10.0),
|
npc_random.randf_range(1.0, 10.0),
|
||||||
npc_random
|
npc_random
|
||||||
@@ -138,7 +144,10 @@ func simulate_tick() -> void:
|
|||||||
elif debug_logs:
|
elif debug_logs:
|
||||||
print("[SimulationManager] ", npc.npc_name, " could not complete ", completed_task, ": target reservation was invalid")
|
print("[SimulationManager] ", npc.npc_name, " could not complete ", completed_task, ": target reservation was invalid")
|
||||||
release_npc_reservation(npc.id)
|
release_npc_reservation(npc.id)
|
||||||
elif completed_task not in ["gather_food", "gather_wood"]:
|
elif completed_task not in [
|
||||||
|
SimulationIds.ACTION_GATHER_FOOD,
|
||||||
|
SimulationIds.ACTION_GATHER_WOOD
|
||||||
|
]:
|
||||||
village.apply_npc_task(npc)
|
village.apply_npc_task(npc)
|
||||||
elif debug_logs:
|
elif debug_logs:
|
||||||
print("[SimulationManager] ", npc.npc_name, " could not complete ", completed_task, ": no resource target")
|
print("[SimulationManager] ", npc.npc_name, " could not complete ", completed_task, ": no resource target")
|
||||||
@@ -148,10 +157,10 @@ func simulate_tick() -> void:
|
|||||||
npc.task_complete = true
|
npc.task_complete = true
|
||||||
npc.task_state = SimNPC.TASK_STATE_IDLE
|
npc.task_state = SimNPC.TASK_STATE_IDLE
|
||||||
npc.last_task = completed_task
|
npc.last_task = completed_task
|
||||||
npc.current_task = "idle"
|
npc.current_task = SimulationIds.ACTION_IDLE
|
||||||
|
|
||||||
if needs_immediate_food_after_gather and village.food > 0:
|
if needs_immediate_food_after_gather and village.food > 0:
|
||||||
npc.set_task("eat", 1.0)
|
npc.set_task(SimulationIds.ACTION_EAT, 1.0)
|
||||||
npc_task_changed.emit(npc, completed_task, npc.current_task)
|
npc_task_changed.emit(npc, completed_task, npc.current_task)
|
||||||
|
|
||||||
if debug_logs:
|
if debug_logs:
|
||||||
@@ -244,7 +253,7 @@ func notify_npc_navigation_failed(npc_id: int) -> void:
|
|||||||
var failed_task := npc.current_task
|
var failed_task := npc.current_task
|
||||||
release_npc_reservation(npc.id)
|
release_npc_reservation(npc.id)
|
||||||
npc.last_task = failed_task
|
npc.last_task = failed_task
|
||||||
npc.set_task("wander", 2.0)
|
npc.set_task(SimulationIds.ACTION_WANDER, 2.0)
|
||||||
npc_task_changed.emit(npc, failed_task, npc.current_task)
|
npc_task_changed.emit(npc, failed_task, npc.current_task)
|
||||||
|
|
||||||
if debug_logs:
|
if debug_logs:
|
||||||
@@ -261,6 +270,17 @@ func register_loaded_resource_nodes() -> void:
|
|||||||
func register_resource_node(node: ResourceNode) -> bool:
|
func register_resource_node(node: ResourceNode) -> bool:
|
||||||
if node == null or node.node_id.is_empty():
|
if node == null or node.node_id.is_empty():
|
||||||
return false
|
return false
|
||||||
|
var action_definition := SimulationDefinitions.get_action(node.action_id)
|
||||||
|
if (
|
||||||
|
action_definition == null
|
||||||
|
or action_definition.target_type != SimulationIds.TARGET_RESOURCE
|
||||||
|
or action_definition.resource_action_id != node.action_id
|
||||||
|
):
|
||||||
|
push_error(
|
||||||
|
"SimulationManager: ResourceNode '%s' has invalid action_id '%s'"
|
||||||
|
% [node.node_id, node.action_id]
|
||||||
|
)
|
||||||
|
return false
|
||||||
var resource_state := get_resource_state(node.node_id)
|
var resource_state := get_resource_state(node.node_id)
|
||||||
if resource_state == null:
|
if resource_state == null:
|
||||||
resource_state = ResourceStateRecord.create_from_node(node)
|
resource_state = ResourceStateRecord.create_from_node(node)
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
class_name ActionDefinition
|
||||||
|
extends Resource
|
||||||
|
|
||||||
|
@export var action_id: StringName
|
||||||
|
@export var display_name: String
|
||||||
|
@export_range(0.0, 1000.0, 0.1) var default_duration := 1.0
|
||||||
|
@export var preferred_profession_id: StringName
|
||||||
|
@export var target_type: StringName = SimulationIds.TARGET_ACTIVITY
|
||||||
|
@export var resource_action_id: StringName
|
||||||
|
|
||||||
|
func validate() -> Array[String]:
|
||||||
|
var errors: Array[String] = []
|
||||||
|
if action_id.is_empty():
|
||||||
|
errors.append("action_id is empty")
|
||||||
|
if display_name.is_empty():
|
||||||
|
errors.append("display_name is empty for '%s'" % action_id)
|
||||||
|
if default_duration <= 0.0:
|
||||||
|
errors.append("default_duration must be positive for '%s'" % action_id)
|
||||||
|
if target_type not in [
|
||||||
|
SimulationIds.TARGET_RESOURCE,
|
||||||
|
SimulationIds.TARGET_ACTIVITY,
|
||||||
|
SimulationIds.TARGET_FREE
|
||||||
|
]:
|
||||||
|
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)
|
||||||
|
return errors
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://b1utbqqel3pyw
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
class_name ProfessionDefinition
|
||||||
|
extends Resource
|
||||||
|
|
||||||
|
@export var profession_id: StringName
|
||||||
|
@export var display_name: String
|
||||||
|
|
||||||
|
func validate() -> Array[String]:
|
||||||
|
var errors: Array[String] = []
|
||||||
|
if profession_id.is_empty():
|
||||||
|
errors.append("profession_id is empty")
|
||||||
|
if display_name.is_empty():
|
||||||
|
errors.append("display_name is empty for '%s'" % profession_id)
|
||||||
|
return errors
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cimqtsrs3huq0
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
class_name SimulationDefinitions
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
const ACTION_PATHS := [
|
||||||
|
"res://simulation/definitions/actions/gather_food.tres",
|
||||||
|
"res://simulation/definitions/actions/gather_wood.tres",
|
||||||
|
"res://simulation/definitions/actions/patrol.tres",
|
||||||
|
"res://simulation/definitions/actions/study.tres",
|
||||||
|
"res://simulation/definitions/actions/eat.tres",
|
||||||
|
"res://simulation/definitions/actions/rest.tres",
|
||||||
|
"res://simulation/definitions/actions/wander.tres"
|
||||||
|
]
|
||||||
|
|
||||||
|
const PROFESSION_PATHS := [
|
||||||
|
"res://simulation/definitions/professions/farmer.tres",
|
||||||
|
"res://simulation/definitions/professions/woodcutter.tres",
|
||||||
|
"res://simulation/definitions/professions/guard.tres",
|
||||||
|
"res://simulation/definitions/professions/scholar.tres",
|
||||||
|
"res://simulation/definitions/professions/wanderer.tres"
|
||||||
|
]
|
||||||
|
|
||||||
|
static func get_actions() -> Array[ActionDefinition]:
|
||||||
|
var definitions: Array[ActionDefinition] = []
|
||||||
|
for path in ACTION_PATHS:
|
||||||
|
var definition := load(path) as ActionDefinition
|
||||||
|
if definition != null:
|
||||||
|
definitions.append(definition)
|
||||||
|
return definitions
|
||||||
|
|
||||||
|
static func get_professions() -> Array[ProfessionDefinition]:
|
||||||
|
var definitions: Array[ProfessionDefinition] = []
|
||||||
|
for path in PROFESSION_PATHS:
|
||||||
|
var definition := load(path) as ProfessionDefinition
|
||||||
|
if definition != null:
|
||||||
|
definitions.append(definition)
|
||||||
|
return definitions
|
||||||
|
|
||||||
|
static func get_action(action_id: StringName) -> ActionDefinition:
|
||||||
|
for definition in get_actions():
|
||||||
|
if definition.action_id == action_id:
|
||||||
|
return definition
|
||||||
|
return null
|
||||||
|
|
||||||
|
static func get_profession(
|
||||||
|
profession_id: StringName
|
||||||
|
) -> ProfessionDefinition:
|
||||||
|
for definition in get_professions():
|
||||||
|
if definition.profession_id == profession_id:
|
||||||
|
return definition
|
||||||
|
return null
|
||||||
|
|
||||||
|
static func get_profession_ids() -> Array[StringName]:
|
||||||
|
var ids: Array[StringName] = []
|
||||||
|
for definition in get_professions():
|
||||||
|
ids.append(definition.profession_id)
|
||||||
|
return ids
|
||||||
|
|
||||||
|
static func validate() -> Array[String]:
|
||||||
|
var errors: Array[String] = []
|
||||||
|
if get_actions().size() != ACTION_PATHS.size():
|
||||||
|
errors.append("One or more ActionDefinition resources failed to load")
|
||||||
|
if get_professions().size() != PROFESSION_PATHS.size():
|
||||||
|
errors.append("One or more ProfessionDefinition resources failed to load")
|
||||||
|
var action_ids := {}
|
||||||
|
for definition in get_actions():
|
||||||
|
for error in definition.validate():
|
||||||
|
errors.append("ActionDefinition: " + error)
|
||||||
|
if action_ids.has(definition.action_id):
|
||||||
|
errors.append("Duplicate action_id '%s'" % definition.action_id)
|
||||||
|
action_ids[definition.action_id] = true
|
||||||
|
|
||||||
|
var profession_ids := {}
|
||||||
|
for definition in get_professions():
|
||||||
|
for error in definition.validate():
|
||||||
|
errors.append("ProfessionDefinition: " + error)
|
||||||
|
if profession_ids.has(definition.profession_id):
|
||||||
|
errors.append("Duplicate profession_id '%s'" % definition.profession_id)
|
||||||
|
profession_ids[definition.profession_id] = true
|
||||||
|
|
||||||
|
for definition in get_actions():
|
||||||
|
if (
|
||||||
|
not definition.preferred_profession_id.is_empty()
|
||||||
|
and not profession_ids.has(definition.preferred_profession_id)
|
||||||
|
):
|
||||||
|
errors.append(
|
||||||
|
"Action '%s' references unknown profession '%s'"
|
||||||
|
% [definition.action_id, definition.preferred_profession_id]
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
definition.target_type == SimulationIds.TARGET_RESOURCE
|
||||||
|
and not action_ids.has(definition.resource_action_id)
|
||||||
|
):
|
||||||
|
errors.append(
|
||||||
|
"Action '%s' references unknown resource action '%s'"
|
||||||
|
% [definition.action_id, definition.resource_action_id]
|
||||||
|
)
|
||||||
|
return errors
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://v1sohi6bbuga
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
class_name SimulationIds
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
const ACTION_IDLE := &"idle"
|
||||||
|
const ACTION_DEAD := &"dead"
|
||||||
|
const ACTION_GATHER_FOOD := &"gather_food"
|
||||||
|
const ACTION_GATHER_WOOD := &"gather_wood"
|
||||||
|
const ACTION_PATROL := &"patrol"
|
||||||
|
const ACTION_STUDY := &"study"
|
||||||
|
const ACTION_EAT := &"eat"
|
||||||
|
const ACTION_REST := &"rest"
|
||||||
|
const ACTION_WANDER := &"wander"
|
||||||
|
|
||||||
|
const PROFESSION_FARMER := &"farmer"
|
||||||
|
const PROFESSION_WOODCUTTER := &"woodcutter"
|
||||||
|
const PROFESSION_GUARD := &"guard"
|
||||||
|
const PROFESSION_SCHOLAR := &"scholar"
|
||||||
|
const PROFESSION_WANDERER := &"wanderer"
|
||||||
|
|
||||||
|
const TARGET_RESOURCE := &"resource"
|
||||||
|
const TARGET_ACTIVITY := &"activity"
|
||||||
|
const TARGET_FREE := &"free"
|
||||||
|
|
||||||
|
const RESOURCE_FOOD := &"food"
|
||||||
|
const RESOURCE_WOOD := &"wood"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://brf7tywgsj6v7
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
[gd_resource type="Resource" script_class="ActionDefinition" load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://simulation/definitions/ActionDefinition.gd" id="1"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1")
|
||||||
|
action_id = &"eat"
|
||||||
|
display_name = "Eat"
|
||||||
|
default_duration = 2.0
|
||||||
|
target_type = &"activity"
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
[gd_resource type="Resource" script_class="ActionDefinition" load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://simulation/definitions/ActionDefinition.gd" id="1"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1")
|
||||||
|
action_id = &"gather_food"
|
||||||
|
display_name = "Gather Food"
|
||||||
|
default_duration = 5.0
|
||||||
|
preferred_profession_id = &"farmer"
|
||||||
|
target_type = &"resource"
|
||||||
|
resource_action_id = &"gather_food"
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
[gd_resource type="Resource" script_class="ActionDefinition" load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://simulation/definitions/ActionDefinition.gd" id="1"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1")
|
||||||
|
action_id = &"gather_wood"
|
||||||
|
display_name = "Gather Wood"
|
||||||
|
default_duration = 5.0
|
||||||
|
preferred_profession_id = &"woodcutter"
|
||||||
|
target_type = &"resource"
|
||||||
|
resource_action_id = &"gather_wood"
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[gd_resource type="Resource" script_class="ActionDefinition" load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://simulation/definitions/ActionDefinition.gd" id="1"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1")
|
||||||
|
action_id = &"patrol"
|
||||||
|
display_name = "Patrol"
|
||||||
|
default_duration = 6.0
|
||||||
|
preferred_profession_id = &"guard"
|
||||||
|
target_type = &"activity"
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
[gd_resource type="Resource" script_class="ActionDefinition" load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://simulation/definitions/ActionDefinition.gd" id="1"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1")
|
||||||
|
action_id = &"rest"
|
||||||
|
display_name = "Rest"
|
||||||
|
default_duration = 4.0
|
||||||
|
target_type = &"activity"
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[gd_resource type="Resource" script_class="ActionDefinition" load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://simulation/definitions/ActionDefinition.gd" id="1"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1")
|
||||||
|
action_id = &"study"
|
||||||
|
display_name = "Study"
|
||||||
|
default_duration = 6.0
|
||||||
|
preferred_profession_id = &"scholar"
|
||||||
|
target_type = &"activity"
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
[gd_resource type="Resource" script_class="ActionDefinition" load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://simulation/definitions/ActionDefinition.gd" id="1"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1")
|
||||||
|
action_id = &"wander"
|
||||||
|
display_name = "Wander"
|
||||||
|
default_duration = 4.0
|
||||||
|
target_type = &"free"
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
[gd_resource type="Resource" script_class="ProfessionDefinition" load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://simulation/definitions/ProfessionDefinition.gd" id="1"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1")
|
||||||
|
profession_id = &"farmer"
|
||||||
|
display_name = "Farmer"
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
[gd_resource type="Resource" script_class="ProfessionDefinition" load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://simulation/definitions/ProfessionDefinition.gd" id="1"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1")
|
||||||
|
profession_id = &"guard"
|
||||||
|
display_name = "Guard"
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
[gd_resource type="Resource" script_class="ProfessionDefinition" load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://simulation/definitions/ProfessionDefinition.gd" id="1"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1")
|
||||||
|
profession_id = &"scholar"
|
||||||
|
display_name = "Scholar"
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
[gd_resource type="Resource" script_class="ProfessionDefinition" load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://simulation/definitions/ProfessionDefinition.gd" id="1"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1")
|
||||||
|
profession_id = &"wanderer"
|
||||||
|
display_name = "Wanderer"
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
[gd_resource type="Resource" script_class="ProfessionDefinition" load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://simulation/definitions/ProfessionDefinition.gd" id="1"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1")
|
||||||
|
profession_id = &"woodcutter"
|
||||||
|
display_name = "Woodcutter"
|
||||||
@@ -13,13 +13,13 @@ static func capture(npc: SimNPC) -> NPCStateRecord:
|
|||||||
"schema_version": SCHEMA_VERSION,
|
"schema_version": SCHEMA_VERSION,
|
||||||
"id": npc.id,
|
"id": npc.id,
|
||||||
"name": npc.npc_name,
|
"name": npc.npc_name,
|
||||||
"profession": npc.profession,
|
"profession": String(npc.profession),
|
||||||
"hunger": npc.hunger,
|
"hunger": npc.hunger,
|
||||||
"energy": npc.energy,
|
"energy": npc.energy,
|
||||||
"strength": npc.strength,
|
"strength": npc.strength,
|
||||||
"intelligence": npc.intelligence,
|
"intelligence": npc.intelligence,
|
||||||
"current_task": npc.current_task,
|
"current_task": String(npc.current_task),
|
||||||
"task_state": npc.task_state,
|
"task_state": String(npc.task_state),
|
||||||
"position": [npc.position.x, npc.position.y, npc.position.z],
|
"position": [npc.position.x, npc.position.y, npc.position.z],
|
||||||
"is_starving": npc.is_starving,
|
"is_starving": npc.is_starving,
|
||||||
"starvation_ticks": npc.starvation_ticks,
|
"starvation_ticks": npc.starvation_ticks,
|
||||||
@@ -29,7 +29,7 @@ static func capture(npc: SimNPC) -> NPCStateRecord:
|
|||||||
"task_progress": npc.task_progress,
|
"task_progress": npc.task_progress,
|
||||||
"task_complete": npc.task_complete,
|
"task_complete": npc.task_complete,
|
||||||
"target_id": String(npc.target_id),
|
"target_id": String(npc.target_id),
|
||||||
"last_task": npc.last_task,
|
"last_task": String(npc.last_task),
|
||||||
"random_seed": str(npc.random_source.seed),
|
"random_seed": str(npc.random_source.seed),
|
||||||
"random_state": str(npc.random_source.state)
|
"random_state": str(npc.random_source.state)
|
||||||
})
|
})
|
||||||
@@ -48,6 +48,24 @@ static func from_dictionary(record_data: Dictionary) -> NPCStateRecord:
|
|||||||
var saved_position = record_data["position"]
|
var saved_position = record_data["position"]
|
||||||
if not saved_position is Array or saved_position.size() != 3:
|
if not saved_position is Array or saved_position.size() != 3:
|
||||||
return null
|
return null
|
||||||
|
var profession_id := StringName(record_data["profession"])
|
||||||
|
if SimulationDefinitions.get_profession(profession_id) == null:
|
||||||
|
return null
|
||||||
|
var action_id := StringName(record_data["current_task"])
|
||||||
|
if (
|
||||||
|
action_id not in [
|
||||||
|
SimulationIds.ACTION_IDLE,
|
||||||
|
SimulationIds.ACTION_DEAD
|
||||||
|
]
|
||||||
|
and SimulationDefinitions.get_action(action_id) == null
|
||||||
|
):
|
||||||
|
return null
|
||||||
|
var last_action_id := StringName(record_data["last_task"])
|
||||||
|
if (
|
||||||
|
not last_action_id.is_empty()
|
||||||
|
and SimulationDefinitions.get_action(last_action_id) == null
|
||||||
|
):
|
||||||
|
return null
|
||||||
return NPCStateRecord.new(record_data)
|
return NPCStateRecord.new(record_data)
|
||||||
|
|
||||||
func restore(debug_logs: bool) -> SimNPC:
|
func restore(debug_logs: bool) -> SimNPC:
|
||||||
@@ -56,7 +74,7 @@ func restore(debug_logs: bool) -> SimNPC:
|
|||||||
var npc := SimNPC.new(
|
var npc := SimNPC.new(
|
||||||
int(data["id"]),
|
int(data["id"]),
|
||||||
String(data["name"]),
|
String(data["name"]),
|
||||||
String(data["profession"]),
|
StringName(data["profession"]),
|
||||||
float(data["strength"]),
|
float(data["strength"]),
|
||||||
float(data["intelligence"]),
|
float(data["intelligence"]),
|
||||||
random_source
|
random_source
|
||||||
@@ -64,8 +82,8 @@ func restore(debug_logs: bool) -> SimNPC:
|
|||||||
var saved_position: Array = data["position"]
|
var saved_position: Array = data["position"]
|
||||||
npc.hunger = float(data["hunger"])
|
npc.hunger = float(data["hunger"])
|
||||||
npc.energy = float(data["energy"])
|
npc.energy = float(data["energy"])
|
||||||
npc.current_task = String(data["current_task"])
|
npc.current_task = StringName(data["current_task"])
|
||||||
npc.task_state = String(data["task_state"])
|
npc.task_state = StringName(data["task_state"])
|
||||||
npc.position = Vector3(
|
npc.position = Vector3(
|
||||||
float(saved_position[0]),
|
float(saved_position[0]),
|
||||||
float(saved_position[1]),
|
float(saved_position[1]),
|
||||||
@@ -79,7 +97,7 @@ func restore(debug_logs: bool) -> SimNPC:
|
|||||||
npc.task_progress = float(data["task_progress"])
|
npc.task_progress = float(data["task_progress"])
|
||||||
npc.task_complete = bool(data["task_complete"])
|
npc.task_complete = bool(data["task_complete"])
|
||||||
npc.target_id = StringName(data["target_id"])
|
npc.target_id = StringName(data["target_id"])
|
||||||
npc.last_task = String(data["last_task"])
|
npc.last_task = StringName(data["last_task"])
|
||||||
npc.random_source.state = String(data["random_state"]).to_int()
|
npc.random_source.state = String(data["random_state"]).to_int()
|
||||||
npc.debug_logs = debug_logs
|
npc.debug_logs = debug_logs
|
||||||
return npc
|
return npc
|
||||||
|
|||||||
@@ -41,6 +41,12 @@ static func from_dictionary(record_data: Dictionary) -> ResourceStateRecord:
|
|||||||
return null
|
return null
|
||||||
if String(record_data["node_id"]).is_empty():
|
if String(record_data["node_id"]).is_empty():
|
||||||
return null
|
return null
|
||||||
|
var action_id := StringName(record_data["action_id"])
|
||||||
|
if (
|
||||||
|
not action_id.is_empty()
|
||||||
|
and SimulationDefinitions.get_action(action_id) == null
|
||||||
|
):
|
||||||
|
return null
|
||||||
return ResourceStateRecord.new(record_data)
|
return ResourceStateRecord.new(record_data)
|
||||||
|
|
||||||
static func _migrate_v1(legacy_data: Dictionary) -> Dictionary:
|
static func _migrate_v1(legacy_data: Dictionary) -> Dictionary:
|
||||||
|
|||||||
@@ -52,14 +52,26 @@ func _run() -> void:
|
|||||||
)
|
)
|
||||||
|
|
||||||
var village := SimVillage.new()
|
var village := SimVillage.new()
|
||||||
var worker := SimNPC.new(100, "BaselineWorker", "scholar", 5.0, 5.0)
|
var worker := SimNPC.new(
|
||||||
worker.set_task("study", 2.0)
|
100,
|
||||||
|
"BaselineWorker",
|
||||||
|
SimulationIds.PROFESSION_SCHOLAR,
|
||||||
|
5.0,
|
||||||
|
5.0
|
||||||
|
)
|
||||||
|
worker.set_task(SimulationIds.ACTION_STUDY, 2.0)
|
||||||
worker.start_working()
|
worker.start_working()
|
||||||
worker.simulate_tick(village)
|
worker.simulate_tick(village)
|
||||||
worker.simulate_tick(village)
|
worker.simulate_tick(village)
|
||||||
_check(worker.task_complete, "A two-tick working task should complete")
|
_check(worker.task_complete, "A two-tick working task should complete")
|
||||||
|
|
||||||
var starving := SimNPC.new(101, "BaselineStarving", "wanderer", 5.0, 5.0)
|
var starving := SimNPC.new(
|
||||||
|
101,
|
||||||
|
"BaselineStarving",
|
||||||
|
SimulationIds.PROFESSION_WANDERER,
|
||||||
|
5.0,
|
||||||
|
5.0
|
||||||
|
)
|
||||||
starving.hunger = 100.0
|
starving.hunger = 100.0
|
||||||
starving.starvation_death_threshold = 1
|
starving.starvation_death_threshold = 1
|
||||||
starving.simulate_tick(village)
|
starving.simulate_tick(village)
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ func _run() -> void:
|
|||||||
manager.set_process(false)
|
manager.set_process(false)
|
||||||
await process_frame
|
await process_frame
|
||||||
var selected: ResourceNode = manager.acquire_resource_target(
|
var selected: ResourceNode = manager.acquire_resource_target(
|
||||||
&"gather_food",
|
SimulationIds.ACTION_GATHER_FOOD,
|
||||||
777,
|
777,
|
||||||
Vector3.ZERO
|
Vector3.ZERO
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
var failures: Array[String] = []
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
call_deferred("_run")
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
_test_registry_integrity()
|
||||||
|
_test_definition_backed_behavior()
|
||||||
|
_test_state_reference_validation()
|
||||||
|
_test_id_round_trip()
|
||||||
|
|
||||||
|
if failures.is_empty():
|
||||||
|
print("[TEST] Simulation definitions passed")
|
||||||
|
quit(0)
|
||||||
|
return
|
||||||
|
|
||||||
|
for failure in failures:
|
||||||
|
push_error("[TEST] " + failure)
|
||||||
|
quit(1)
|
||||||
|
|
||||||
|
func _test_registry_integrity() -> void:
|
||||||
|
var errors := SimulationDefinitions.validate()
|
||||||
|
_check(
|
||||||
|
errors.is_empty(),
|
||||||
|
"Definition registry should validate: %s" % [errors]
|
||||||
|
)
|
||||||
|
|
||||||
|
var action_ids: Array[StringName] = []
|
||||||
|
for definition in SimulationDefinitions.get_actions():
|
||||||
|
action_ids.append(definition.action_id)
|
||||||
|
_check(
|
||||||
|
SimulationDefinitions.get_action(definition.action_id) == definition,
|
||||||
|
"Action lookup should return '%s'" % definition.action_id
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
action_ids.size() == 7,
|
||||||
|
"Registry should contain all seven executable prototype actions"
|
||||||
|
)
|
||||||
|
|
||||||
|
var profession_ids := SimulationDefinitions.get_profession_ids()
|
||||||
|
_check(
|
||||||
|
profession_ids.size() == 5,
|
||||||
|
"Registry should contain all five prototype professions"
|
||||||
|
)
|
||||||
|
for profession_id in profession_ids:
|
||||||
|
_check(
|
||||||
|
SimulationDefinitions.get_profession(profession_id) != null,
|
||||||
|
"Profession lookup should return '%s'" % profession_id
|
||||||
|
)
|
||||||
|
|
||||||
|
func _test_definition_backed_behavior() -> void:
|
||||||
|
var npc := SimNPC.new(
|
||||||
|
500,
|
||||||
|
"DefinitionWorker",
|
||||||
|
SimulationIds.PROFESSION_SCHOLAR,
|
||||||
|
5.0,
|
||||||
|
5.0
|
||||||
|
)
|
||||||
|
npc.set_task(SimulationIds.ACTION_STUDY)
|
||||||
|
var study := SimulationDefinitions.get_action(SimulationIds.ACTION_STUDY)
|
||||||
|
_check(
|
||||||
|
is_equal_approx(npc.task_duration, study.default_duration),
|
||||||
|
"SimNPC should use the action definition's default duration"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
study.preferred_profession_id == SimulationIds.PROFESSION_SCHOLAR,
|
||||||
|
"Study should reference the stable scholar profession ID"
|
||||||
|
)
|
||||||
|
var gather := SimulationDefinitions.get_action(
|
||||||
|
SimulationIds.ACTION_GATHER_FOOD
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
gather.target_type == SimulationIds.TARGET_RESOURCE
|
||||||
|
and gather.resource_action_id == SimulationIds.ACTION_GATHER_FOOD,
|
||||||
|
"Gather-food target metadata should come from its definition"
|
||||||
|
)
|
||||||
|
|
||||||
|
func _test_state_reference_validation() -> void:
|
||||||
|
var npc := SimNPC.new(
|
||||||
|
501,
|
||||||
|
"ValidationWorker",
|
||||||
|
SimulationIds.PROFESSION_FARMER,
|
||||||
|
5.0,
|
||||||
|
5.0
|
||||||
|
)
|
||||||
|
var valid_data := NPCStateRecord.capture(npc).to_dictionary()
|
||||||
|
var invalid_profession := valid_data.duplicate(true)
|
||||||
|
invalid_profession["profession"] = "missing_profession"
|
||||||
|
_check(
|
||||||
|
NPCStateRecord.from_dictionary(invalid_profession) == null,
|
||||||
|
"NPC state should reject an unknown profession ID"
|
||||||
|
)
|
||||||
|
var invalid_action := valid_data.duplicate(true)
|
||||||
|
invalid_action["current_task"] = "missing_action"
|
||||||
|
_check(
|
||||||
|
NPCStateRecord.from_dictionary(invalid_action) == null,
|
||||||
|
"NPC state should reject an unknown action ID"
|
||||||
|
)
|
||||||
|
|
||||||
|
func _test_id_round_trip() -> void:
|
||||||
|
var manager: Node = load("res://simulation/SimulationManager.gd").new()
|
||||||
|
manager.debug_logs = false
|
||||||
|
root.add_child(manager)
|
||||||
|
manager.set_process(false)
|
||||||
|
var json_state: String = manager.serialize_state()
|
||||||
|
|
||||||
|
var restored: Node = load("res://simulation/SimulationManager.gd").new()
|
||||||
|
restored.debug_logs = false
|
||||||
|
root.add_child(restored)
|
||||||
|
restored.set_process(false)
|
||||||
|
_check(
|
||||||
|
restored.restore_state_from_json(json_state),
|
||||||
|
"Definition-backed IDs should restore from serialized state"
|
||||||
|
)
|
||||||
|
for npc in restored.npcs:
|
||||||
|
_check(
|
||||||
|
typeof(npc.profession) == TYPE_STRING_NAME,
|
||||||
|
"Profession should restore as StringName"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
typeof(npc.current_task) == TYPE_STRING_NAME,
|
||||||
|
"Current action should restore as StringName"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
SimulationDefinitions.get_profession(npc.profession) != null,
|
||||||
|
"Restored profession should resolve through the registry"
|
||||||
|
)
|
||||||
|
|
||||||
|
manager.free()
|
||||||
|
restored.free()
|
||||||
|
|
||||||
|
func _check(condition: bool, message: String) -> void:
|
||||||
|
if not condition:
|
||||||
|
failures.append(message)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://c4iri1xd4ts4
|
||||||
@@ -8,8 +8,8 @@ signal reservation_changed(node_id: StringName, agent_id: int)
|
|||||||
static var _all: Array[ResourceNode] = []
|
static var _all: Array[ResourceNode] = []
|
||||||
|
|
||||||
@export var node_id: StringName
|
@export var node_id: StringName
|
||||||
@export var action_id: StringName = &"gather_food"
|
@export var action_id: StringName = SimulationIds.ACTION_GATHER_FOOD
|
||||||
@export var resource_id: StringName = &"food"
|
@export var resource_id: StringName = SimulationIds.RESOURCE_FOOD
|
||||||
@export var initial_amount: float = 10.0
|
@export var initial_amount: float = 10.0
|
||||||
@export var yield_per_action: float = 2.0
|
@export var yield_per_action: float = 2.0
|
||||||
@export var initial_enabled: bool = true
|
@export var initial_enabled: bool = true
|
||||||
|
|||||||
+39
-20
@@ -87,7 +87,11 @@ func update_npc_targets() -> void:
|
|||||||
|
|
||||||
var visual = active_npc_visuals[npc.id]
|
var visual = active_npc_visuals[npc.id]
|
||||||
var task = npc.current_task
|
var task = npc.current_task
|
||||||
if task in ["", "idle", "dead"]:
|
if task in [
|
||||||
|
&"",
|
||||||
|
SimulationIds.ACTION_IDLE,
|
||||||
|
SimulationIds.ACTION_DEAD
|
||||||
|
]:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
var resource_pos = _try_get_resource_target(npc, npc.current_task)
|
var resource_pos = _try_get_resource_target(npc, npc.current_task)
|
||||||
@@ -95,12 +99,16 @@ func update_npc_targets() -> void:
|
|||||||
visual.set_target_position(resource_pos)
|
visual.set_target_position(resource_pos)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if task == "wander":
|
if task == SimulationIds.ACTION_WANDER:
|
||||||
var offset := _get_wander_offset(npc.id)
|
var offset := _get_wander_offset(npc.id)
|
||||||
visual.set_target_position(visual.global_position + offset)
|
visual.set_target_position(visual.global_position + offset)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if task in ["gather_food", "gather_wood"]:
|
var definition := SimulationDefinitions.get_action(task)
|
||||||
|
if (
|
||||||
|
definition != null
|
||||||
|
and definition.target_type == SimulationIds.TARGET_RESOURCE
|
||||||
|
):
|
||||||
if simulation_manager.has_method("notify_npc_target_unavailable"):
|
if simulation_manager.has_method("notify_npc_target_unavailable"):
|
||||||
simulation_manager.notify_npc_target_unavailable(npc.id)
|
simulation_manager.notify_npc_target_unavailable(npc.id)
|
||||||
continue
|
continue
|
||||||
@@ -113,26 +121,25 @@ func update_npc_targets() -> void:
|
|||||||
|
|
||||||
visual.set_target_position(target.global_position)
|
visual.set_target_position(target.global_position)
|
||||||
|
|
||||||
func get_target_for_task(task: String) -> Marker3D:
|
func get_target_for_task(task: StringName) -> Marker3D:
|
||||||
match task:
|
match task:
|
||||||
"patrol":
|
SimulationIds.ACTION_PATROL:
|
||||||
return guard_zone
|
return guard_zone
|
||||||
"study":
|
SimulationIds.ACTION_STUDY:
|
||||||
return study_zone
|
return study_zone
|
||||||
"rest":
|
SimulationIds.ACTION_REST:
|
||||||
return rest_zone
|
return rest_zone
|
||||||
"eat":
|
SimulationIds.ACTION_EAT:
|
||||||
return food_zone
|
return food_zone
|
||||||
_:
|
_:
|
||||||
return rest_zone
|
return rest_zone
|
||||||
|
|
||||||
func _try_get_resource_target(npc: SimNPC, task: String) -> Variant:
|
func _try_get_resource_target(npc: SimNPC, task: StringName) -> Variant:
|
||||||
var action_map := {
|
var definition := SimulationDefinitions.get_action(task)
|
||||||
"gather_food": &"gather_food",
|
if (
|
||||||
"gather_wood": &"gather_wood"
|
definition == null
|
||||||
}
|
or definition.target_type != SimulationIds.TARGET_RESOURCE
|
||||||
var action_id = action_map.get(task)
|
):
|
||||||
if action_id == null:
|
|
||||||
return null
|
return null
|
||||||
|
|
||||||
var visual = active_npc_visuals.get(npc.id) as Node3D
|
var visual = active_npc_visuals.get(npc.id) as Node3D
|
||||||
@@ -143,7 +150,7 @@ func _try_get_resource_target(npc: SimNPC, task: String) -> Variant:
|
|||||||
push_error("WorldViewManager: SimulationManager cannot acquire resources")
|
push_error("WorldViewManager: SimulationManager cannot acquire resources")
|
||||||
return null
|
return null
|
||||||
var node: ResourceNode = simulation_manager.acquire_resource_target(
|
var node: ResourceNode = simulation_manager.acquire_resource_target(
|
||||||
action_id,
|
definition.resource_action_id,
|
||||||
npc.id,
|
npc.id,
|
||||||
visual.global_position
|
visual.global_position
|
||||||
)
|
)
|
||||||
@@ -155,14 +162,22 @@ func _try_get_resource_target(npc: SimNPC, task: String) -> Variant:
|
|||||||
debug_log("%s reserved %s for %s" % [npc.npc_name, node.node_id, task])
|
debug_log("%s reserved %s for %s" % [npc.npc_name, node.node_id, task])
|
||||||
return node.interaction_point.global_position
|
return node.interaction_point.global_position
|
||||||
|
|
||||||
func _on_npc_task_changed(npc: SimNPC, old_task: String, new_task: String) -> void:
|
func _on_npc_task_changed(
|
||||||
|
npc: SimNPC,
|
||||||
|
old_task: StringName,
|
||||||
|
new_task: StringName
|
||||||
|
) -> void:
|
||||||
if DEBUG_LOGS:
|
if DEBUG_LOGS:
|
||||||
debug_log("%s changed task: %s -> %s" % [npc.npc_name, old_task, new_task])
|
debug_log("%s changed task: %s -> %s" % [npc.npc_name, old_task, new_task])
|
||||||
|
|
||||||
if not active_npc_visuals.has(npc.id):
|
if not active_npc_visuals.has(npc.id):
|
||||||
return
|
return
|
||||||
|
|
||||||
if new_task in ["", "idle", "dead"]:
|
if new_task in [
|
||||||
|
&"",
|
||||||
|
SimulationIds.ACTION_IDLE,
|
||||||
|
SimulationIds.ACTION_DEAD
|
||||||
|
]:
|
||||||
return
|
return
|
||||||
|
|
||||||
var visual = active_npc_visuals[npc.id]
|
var visual = active_npc_visuals[npc.id]
|
||||||
@@ -171,12 +186,16 @@ func _on_npc_task_changed(npc: SimNPC, old_task: String, new_task: String) -> vo
|
|||||||
visual.set_target_position(resource_pos)
|
visual.set_target_position(resource_pos)
|
||||||
return
|
return
|
||||||
|
|
||||||
if new_task == "wander":
|
if new_task == SimulationIds.ACTION_WANDER:
|
||||||
var offset := _get_wander_offset(npc.id)
|
var offset := _get_wander_offset(npc.id)
|
||||||
visual.set_target_position(visual.global_position + offset)
|
visual.set_target_position(visual.global_position + offset)
|
||||||
return
|
return
|
||||||
|
|
||||||
if new_task in ["gather_food", "gather_wood"]:
|
var definition := SimulationDefinitions.get_action(new_task)
|
||||||
|
if (
|
||||||
|
definition != null
|
||||||
|
and definition.target_type == SimulationIds.TARGET_RESOURCE
|
||||||
|
):
|
||||||
if simulation_manager.has_method("notify_npc_target_unavailable"):
|
if simulation_manager.has_method("notify_npc_target_unavailable"):
|
||||||
simulation_manager.notify_npc_target_unavailable(npc.id)
|
simulation_manager.notify_npc_target_unavailable(npc.id)
|
||||||
else:
|
else:
|
||||||
|
|||||||
Reference in New Issue
Block a user