Files
gamedev-the-steward/simulation/SimNPC.gd
T
admin a8ae331f51 feat: synchronize authoritative npc position
- NpcVisual emits position_changed signal during _physics_process
- SimulationManager.synchronize_npc_position() writes active position
  into SimNPC state on movement, arrival, and navigation failure
- WorldViewManager forwards position_changed and syncs before
  despawn/reload
- SimNPC stores travel_target_position and has_travel_target as
  versioned state; NPCStateRecord v2 persists them with v1 migration
- WorldViewManager.spawn_npc_visual() resumes travel for traveling NPCs
  via SimulationManager.request_current_travel()
- WorldViewManager.despawn_npc_visual() syncs position before removal
- WorldViewManager.reload_npc_visual() spawns a visual from persisted
  state without rerolling target selection
- tests/npc_visual_lifecycle_test.gd proves unload/reload preserves
  action, target, reservation, position, RNG state, and checksum
- simulation_state_serialization_test adds legacy v1 NPC migration test
  that does not invent an active travel target
2026-07-05 17:21:17 +02:00

100 lines
2.4 KiB
GDScript

class_name SimNPC
extends RefCounted
const TASK_STATE_IDLE := &"idle"
const TASK_STATE_TRAVELING := &"traveling"
const TASK_STATE_WORKING := &"working"
const TASK_STATE_COMPLETE := &"complete"
var id: int
var npc_name: String
var profession: StringName
var hunger: float
var energy: float
var strength: float
var intelligence: float
var current_task: StringName = SimulationIds.ACTION_IDLE
var task_state: StringName = TASK_STATE_IDLE
var position: Vector3
var is_starving := false
var starvation_ticks := 0
var starvation_death_threshold := 20
var is_dead := false
var task_duration := 0.0
var task_progress := 0.0
var task_complete := true
var target_id: StringName = &""
var travel_target_position: Vector3
var has_travel_target := false
var last_task: StringName
var random_source: RandomNumberGenerator
var debug_logs := true
func _init(
_id: int,
_npc_name: String,
_profession: StringName,
_strength: float,
_intelligence: float,
_random_source: RandomNumberGenerator = null
) -> void:
id = _id
npc_name = _npc_name
profession = _profession
strength = _strength
intelligence = _intelligence
random_source = _random_source
if random_source == null:
random_source = RandomNumberGenerator.new()
random_source.seed = id + 1
hunger = random_source.randf_range(20.0, 80.0)
energy = random_source.randf_range(40.0, 100.0)
position = Vector3(
random_source.randf_range(-8.0, 8.0),
0.0,
random_source.randf_range(-8.0, 8.0)
)
func die_from_starvation() -> void:
is_dead = true
current_task = SimulationIds.ACTION_DEAD
task_state = TASK_STATE_IDLE
task_progress = 0.0
task_duration = 0.0
task_complete = true
has_travel_target = false
if debug_logs:
print("[SimNPC] ", npc_name, " died from starvation.")
func should_eat_immediately_after_task() -> bool:
return (
current_task == SimulationIds.ACTION_GATHER_FOOD
and is_starving
and not is_dead
)
func set_task(action_id: StringName, duration: float = -1.0) -> void:
var definition := SimulationDefinitions.get_action(action_id)
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_complete = false
task_state = TASK_STATE_TRAVELING
has_travel_target = false
func start_working() -> void:
if task_state != TASK_STATE_TRAVELING:
return
task_state = TASK_STATE_WORKING
task_progress = 0.0
task_complete = false