Compare commits
16 Commits
4604dc6d5a
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| d6520c426a | |||
| 6981c7e9bc | |||
| 4b5fc858f8 | |||
| 8a955f3c29 | |||
| 2c3890502a | |||
| 595d67724f | |||
| fce5c533fe | |||
| 8094975094 | |||
| 8a1913c5ae | |||
| c61d286005 | |||
| 5ea83b06ed | |||
| ae0b3dbf89 | |||
| 7aa5c1c979 | |||
| 6008b1023e | |||
| adedf28417 | |||
| cab732a977 |
@@ -652,14 +652,14 @@ Completed after the architecture gate:
|
|||||||
|
|
||||||
The practical next sequence is:
|
The practical next sequence is:
|
||||||
|
|
||||||
1. Continue growing resource discovery with finite `ResourceNode` instances
|
1. Expand the structured event feed with actor/target/cause metadata so the
|
||||||
placed in meaningful foliage, animal-camp, berry, and village-stockpile
|
inspector can show a readable timeline per NPC. Narrative events for task
|
||||||
contexts. The first distance/safety/comfort scoring pass, twelve authored
|
starts, sleep, and death are recorded; next should add depletion notices,
|
||||||
resources, and placement validation exist; next additions should increase
|
relationship seeds, and village-level event summaries filtered by relevance.
|
||||||
authored variety only when they preserve reachability, stable IDs, scoring
|
2. Seed relationship dimensions (familiarity, trust, obligation) from shared
|
||||||
metadata, and clear player interaction ranges.
|
work, meal, and sleep proximity, then expose them through utility
|
||||||
2. Expand persistence only when schedules, player state, or a real save menu
|
considerations so NPCs begin to prefer known colleagues and familiar routes.
|
||||||
creates a concrete requirement.
|
Start with one lightweight dimension before building the full graph.
|
||||||
|
|
||||||
Recently completed:
|
Recently completed:
|
||||||
|
|
||||||
@@ -693,6 +693,24 @@ Recently completed:
|
|||||||
multi-layer UV flow, fresnel edges, foam highlights, and sparkle; wider river
|
multi-layer UV flow, fresnel edges, foam highlights, and sparkle; wider river
|
||||||
and waterfall; fortress with crenellations, three turrets, and prominent
|
and waterfall; fortress with crenellations, three turrets, and prominent
|
||||||
ridge banner.
|
ridge banner.
|
||||||
|
- Resource discovery expanded to 18 finite ResourceNodes across farming,
|
||||||
|
deep-forest, river-bank, and mill-adjacent contexts with scored metadata.
|
||||||
|
- NPC schedules implemented: SLEEP/MEAL/WORK/DISCRETIONARY periods driven by
|
||||||
|
simulation clock time-of-day. DayNightCycle synced to simulation clock. NPCs
|
||||||
|
walk home to designated house positions at night, sleep to restore energy,
|
||||||
|
eat at meal times, and work during the day. Starving NPCs always override
|
||||||
|
sleep to seek food.
|
||||||
|
- Starvation balance reworked: hunger ~25/day, death threshold ~3 days of
|
||||||
|
starvation (realistic ~6-7 day survival without food). Energy drain uses
|
||||||
|
exact binary fractions for lossless JSON serialization.
|
||||||
|
- Wood inventory and woodpile storage: wood now follows the same
|
||||||
|
gather→carry→deposit pattern as food. VillageWoodpile StorageNode receives
|
||||||
|
wood deposits. Village wood syncs from storage. Economic events track all
|
||||||
|
transfers.
|
||||||
|
- Structured event feed: EconomicEventRecord now supports narrative events
|
||||||
|
(task started, NPC slept, NPC died) alongside economic transfers.
|
||||||
|
Per-NPC event history with human-readable descriptions exposed in the
|
||||||
|
inspector under a "Recent" timeline.
|
||||||
|
|
||||||
This order strengthens the simulation while regularly producing visible
|
This order strengthens the simulation while regularly producing visible
|
||||||
progress suitable for public development updates.
|
progress suitable for public development updates.
|
||||||
|
|||||||
@@ -7,10 +7,10 @@
|
|||||||
[ext_resource type="PackedScene" uid="uid://dhxxyprqflotq" path="res://player/npc/NpcVisual.tscn" id="5_lquwl"]
|
[ext_resource type="PackedScene" uid="uid://dhxxyprqflotq" path="res://player/npc/NpcVisual.tscn" id="5_lquwl"]
|
||||||
[ext_resource type="Script" uid="uid://cjvbgqx8rao0t" path="res://world/ui/ui.gd" id="6_7mycd"]
|
[ext_resource type="Script" uid="uid://cjvbgqx8rao0t" path="res://world/ui/ui.gd" id="6_7mycd"]
|
||||||
[ext_resource type="Script" uid="uid://bbwol3mrjgn2x" path="res://world/active_world_adapter.gd" id="9_adapter"]
|
[ext_resource type="Script" uid="uid://bbwol3mrjgn2x" path="res://world/active_world_adapter.gd" id="9_adapter"]
|
||||||
[ext_resource type="Script" path="res://simulation/persistence/SaveSlotController.gd" id="10_save"]
|
[ext_resource type="Script" uid="uid://cwvdljsh148wh" path="res://simulation/persistence/SaveSlotController.gd" id="10_save"]
|
||||||
[ext_resource type="PackedScene" path="res://world/jajce/JajceWorld.tscn" id="11_jajce"]
|
[ext_resource type="PackedScene" path="res://world/jajce/JajceWorld.tscn" id="11_jajce"]
|
||||||
[ext_resource type="Script" path="res://world/ui/time_dial.gd" id="12_timedial"]
|
[ext_resource type="Script" uid="uid://cascjhf8lsvay" path="res://world/ui/time_dial.gd" id="12_timedial"]
|
||||||
[ext_resource type="Script" path="res://world/demo/DemoController.gd" id="13_demo"]
|
[ext_resource type="Script" uid="uid://bivrsk5ukgnoo" path="res://world/demo/DemoController.gd" id="13_demo"]
|
||||||
|
|
||||||
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_o5qli"]
|
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_o5qli"]
|
||||||
radius = 0.4
|
radius = 0.4
|
||||||
@@ -22,7 +22,7 @@ height = 1.7
|
|||||||
|
|
||||||
[node name="Main" type="Node3D" unique_id=1850341560]
|
[node name="Main" type="Node3D" unique_id=1850341560]
|
||||||
|
|
||||||
[node name="JajceWorld" parent="." instance=ExtResource("11_jajce")]
|
[node name="JajceWorld" parent="." unique_id=1023795383 instance=ExtResource("11_jajce")]
|
||||||
|
|
||||||
[node name="Player" type="CharacterBody3D" parent="." unique_id=2022843760 node_paths=PackedStringArray("camera_rig", "simulation_manager", "pantry_storage", "guard_site", "study_site")]
|
[node name="Player" type="CharacterBody3D" parent="." unique_id=2022843760 node_paths=PackedStringArray("camera_rig", "simulation_manager", "pantry_storage", "guard_site", "study_site")]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.05, 0)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.05, 0)
|
||||||
@@ -59,15 +59,17 @@ max_zoom_offset = Vector3(0, 20, 15)
|
|||||||
current = true
|
current = true
|
||||||
fov = 65.0
|
fov = 65.0
|
||||||
|
|
||||||
[node name="ActiveWorldAdapter" type="Node" parent="." node_paths=PackedStringArray("pantry_storage")]
|
[node name="ActiveWorldAdapter" type="Node" parent="." unique_id=1773902505 node_paths=PackedStringArray("pantry_storage", "woodpile_storage")]
|
||||||
script = ExtResource("9_adapter")
|
script = ExtResource("9_adapter")
|
||||||
pantry_storage = NodePath("../JajceWorld/WorldObjects/StorageSites/VillagePantry")
|
pantry_storage = NodePath("../JajceWorld/WorldObjects/StorageSites/VillagePantry")
|
||||||
|
woodpile_storage = NodePath("../JajceWorld/WorldObjects/StorageSites/VillageWoodpile")
|
||||||
|
|
||||||
[node name="SimulationManager" type="Node" parent="." unique_id=1099676814 node_paths=PackedStringArray("active_world_adapter")]
|
[node name="SimulationManager" type="Node" parent="." unique_id=1099676814 node_paths=PackedStringArray("active_world_adapter")]
|
||||||
script = ExtResource("3_lquwl")
|
script = ExtResource("3_lquwl")
|
||||||
active_world_adapter = NodePath("../ActiveWorldAdapter")
|
active_world_adapter = NodePath("../ActiveWorldAdapter")
|
||||||
|
home_positions = Array[Vector3]([Vector3(-15, 2, 8.5), Vector3(-13.5, 2, 6.5), Vector3(-8.5, 2, 3), Vector3(-6.5, 2, 1.5), Vector3(-15, 2, -2.5), Vector3(-13.5, 2, -5)])
|
||||||
|
|
||||||
[node name="SaveSlotController" type="Node" parent="." node_paths=PackedStringArray("simulation_manager")]
|
[node name="SaveSlotController" type="Node" parent="." unique_id=1281154019 node_paths=PackedStringArray("simulation_manager")]
|
||||||
script = ExtResource("10_save")
|
script = ExtResource("10_save")
|
||||||
simulation_manager = NodePath("../SimulationManager")
|
simulation_manager = NodePath("../SimulationManager")
|
||||||
|
|
||||||
@@ -85,11 +87,6 @@ active_npcs_parent = NodePath("../ActiveNPCs")
|
|||||||
script = ExtResource("6_7mycd")
|
script = ExtResource("6_7mycd")
|
||||||
simulation_manager = NodePath("../SimulationManager")
|
simulation_manager = NodePath("../SimulationManager")
|
||||||
|
|
||||||
[node name="DemoController" type="Node" parent="." node_paths=PackedStringArray("ui", "active_npcs_parent")]
|
|
||||||
script = ExtResource("13_demo")
|
|
||||||
ui = NodePath("../UI")
|
|
||||||
active_npcs_parent = NodePath("../ActiveNPCs")
|
|
||||||
|
|
||||||
[node name="VillagePanel" type="PanelContainer" parent="UI" unique_id=482126250]
|
[node name="VillagePanel" type="PanelContainer" parent="UI" unique_id=482126250]
|
||||||
offset_left = 20.0
|
offset_left = 20.0
|
||||||
offset_top = 20.0
|
offset_top = 20.0
|
||||||
@@ -107,7 +104,7 @@ theme_override_constants/margin_bottom = 4
|
|||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
theme_override_font_sizes/font_size = 10
|
theme_override_font_sizes/font_size = 10
|
||||||
|
|
||||||
[node name="NpcInspectorPanel" type="PanelContainer" parent="UI"]
|
[node name="NpcInspectorPanel" type="PanelContainer" parent="UI" unique_id=1165556983]
|
||||||
anchors_preset = 1
|
anchors_preset = 1
|
||||||
anchor_left = 1.0
|
anchor_left = 1.0
|
||||||
anchor_right = 1.0
|
anchor_right = 1.0
|
||||||
@@ -117,24 +114,29 @@ offset_right = -20.0
|
|||||||
offset_bottom = 330.0
|
offset_bottom = 330.0
|
||||||
grow_horizontal = 0
|
grow_horizontal = 0
|
||||||
|
|
||||||
[node name="MarginContainer" type="MarginContainer" parent="UI/NpcInspectorPanel"]
|
[node name="MarginContainer" type="MarginContainer" parent="UI/NpcInspectorPanel" unique_id=1227722850]
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
theme_override_constants/margin_left = 16
|
theme_override_constants/margin_left = 16
|
||||||
theme_override_constants/margin_top = 12
|
theme_override_constants/margin_top = 12
|
||||||
theme_override_constants/margin_right = 16
|
theme_override_constants/margin_right = 16
|
||||||
theme_override_constants/margin_bottom = 12
|
theme_override_constants/margin_bottom = 12
|
||||||
|
|
||||||
[node name="NpcInspectorLabel" type="Label" parent="UI/NpcInspectorPanel/MarginContainer"]
|
[node name="NpcInspectorLabel" type="Label" parent="UI/NpcInspectorPanel/MarginContainer" unique_id=1036784952]
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
theme_override_colors/font_color = Color(0.96, 0.91, 0.78, 1)
|
theme_override_colors/font_color = Color(0.96, 0.91, 0.78, 1)
|
||||||
theme_override_font_sizes/font_size = 15
|
theme_override_font_sizes/font_size = 15
|
||||||
text = "Villager inspector"
|
text = "Villager inspector"
|
||||||
|
|
||||||
[node name="TimeDial" type="Control" parent="UI" node_paths=PackedStringArray("day_night_cycle")]
|
[node name="TimeDial" type="Control" parent="UI" unique_id=191345668]
|
||||||
layout_mode = 0
|
layout_mode = 3
|
||||||
|
anchors_preset = 0
|
||||||
offset_left = 880.0
|
offset_left = 880.0
|
||||||
offset_top = 6.0
|
offset_top = 6.0
|
||||||
offset_right = 940.0
|
offset_right = 940.0
|
||||||
offset_bottom = 66.0
|
offset_bottom = 66.0
|
||||||
script = ExtResource("12_timedial")
|
script = ExtResource("12_timedial")
|
||||||
day_night_cycle = NodePath("../JajceWorld/DayNightCycle")
|
|
||||||
|
[node name="DemoController" type="Node" parent="." unique_id=644713371 node_paths=PackedStringArray("ui", "active_npcs_parent")]
|
||||||
|
script = ExtResource("13_demo")
|
||||||
|
ui = NodePath("../UI")
|
||||||
|
active_npcs_parent = NodePath("../ActiveNPCs")
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ var home_position: Vector3
|
|||||||
|
|
||||||
var is_starving := false
|
var is_starving := false
|
||||||
var starvation_ticks := 0
|
var starvation_ticks := 0
|
||||||
var starvation_death_threshold := 20
|
var starvation_death_threshold := 600
|
||||||
var is_dead := false
|
var is_dead := false
|
||||||
|
|
||||||
var task_duration := 0.0
|
var task_duration := 0.0
|
||||||
@@ -32,6 +32,8 @@ var has_travel_target := false
|
|||||||
var inventory: Dictionary = {}
|
var inventory: Dictionary = {}
|
||||||
var last_task: StringName
|
var last_task: StringName
|
||||||
var random_source: RandomNumberGenerator
|
var random_source: RandomNumberGenerator
|
||||||
|
var familiarity: Dictionary = {}
|
||||||
|
var mourning_ticks := 0
|
||||||
var debug_logs := true
|
var debug_logs := true
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ var npcs: Array[SimNPC] = []
|
|||||||
@export var cycle_duration_seconds := 240.0
|
@export var cycle_duration_seconds := 240.0
|
||||||
@export var debug_logs := true
|
@export var debug_logs := true
|
||||||
@export var active_world_adapter: Node
|
@export var active_world_adapter: Node
|
||||||
|
@export var home_positions: Array[Vector3] = []
|
||||||
|
|
||||||
var clock: SimulationClock
|
var clock: SimulationClock
|
||||||
var tick_count := 0
|
var tick_count := 0
|
||||||
@@ -30,6 +31,9 @@ var latest_decisions: Dictionary = {}
|
|||||||
var action_selector := ActionSelectionSystem.new()
|
var action_selector := ActionSelectionSystem.new()
|
||||||
var action_executor := ActionExecutionSystem.new()
|
var action_executor := ActionExecutionSystem.new()
|
||||||
var target_resolver := ActionTargetResolver.new()
|
var target_resolver := ActionTargetResolver.new()
|
||||||
|
var speed_index := 2
|
||||||
|
const SPEED_LEVELS := [0.25, 0.5, 1.0, 2.0, 4.0, 10.0]
|
||||||
|
signal speed_changed(multiplier: float)
|
||||||
|
|
||||||
var names := ["Amina", "Tarik", "Jasmin", "Elma", "Mirza", "Lejla"]
|
var names := ["Amina", "Tarik", "Jasmin", "Elma", "Mirza", "Lejla"]
|
||||||
|
|
||||||
@@ -44,6 +48,7 @@ func _ready() -> void:
|
|||||||
return
|
return
|
||||||
clock = SimulationClock.new(tick_interval)
|
clock = SimulationClock.new(tick_interval)
|
||||||
clock.cycle_duration_seconds = cycle_duration_seconds
|
clock.cycle_duration_seconds = cycle_duration_seconds
|
||||||
|
clock.elapsed_ticks = int(0.25 * cycle_duration_seconds / tick_interval)
|
||||||
village.debug_logs = debug_logs
|
village.debug_logs = debug_logs
|
||||||
_initialize_storage()
|
_initialize_storage()
|
||||||
village.update_modifiers()
|
village.update_modifiers()
|
||||||
@@ -56,11 +61,26 @@ func _ready() -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
func _process(delta: float) -> void:
|
||||||
var ticks_due := clock.advance(delta)
|
var ticks_due := clock.advance(delta * SPEED_LEVELS[speed_index])
|
||||||
for tick in ticks_due:
|
for tick in ticks_due:
|
||||||
simulate_tick()
|
simulate_tick()
|
||||||
|
|
||||||
|
|
||||||
|
func _input(event: InputEvent) -> void:
|
||||||
|
if not event is InputEventKey or not event.pressed or event.echo:
|
||||||
|
return
|
||||||
|
if event.keycode == KEY_BRACKETLEFT:
|
||||||
|
speed_index = maxi(speed_index - 1, 0)
|
||||||
|
speed_changed.emit(SPEED_LEVELS[speed_index])
|
||||||
|
if debug_logs:
|
||||||
|
print("[SimulationManager] Speed: %.2fx" % SPEED_LEVELS[speed_index])
|
||||||
|
elif event.keycode == KEY_BRACKETRIGHT:
|
||||||
|
speed_index = mini(speed_index + 1, SPEED_LEVELS.size() - 1)
|
||||||
|
speed_changed.emit(SPEED_LEVELS[speed_index])
|
||||||
|
if debug_logs:
|
||||||
|
print("[SimulationManager] Speed: %.2fx" % SPEED_LEVELS[speed_index])
|
||||||
|
|
||||||
|
|
||||||
func generate_npcs() -> void:
|
func generate_npcs() -> void:
|
||||||
var profession_ids := SimulationDefinitions.get_profession_ids()
|
var profession_ids := SimulationDefinitions.get_profession_ids()
|
||||||
for i in range(names.size()):
|
for i in range(names.size()):
|
||||||
@@ -79,6 +99,17 @@ func generate_npcs() -> void:
|
|||||||
npcs.append(npc)
|
npcs.append(npc)
|
||||||
wander_random_sources[i] = _create_random_source(i, 1)
|
wander_random_sources[i] = _create_random_source(i, 1)
|
||||||
|
|
||||||
|
var home_count := home_positions.size()
|
||||||
|
if home_count > 0:
|
||||||
|
for i in range(npcs.size()):
|
||||||
|
npcs[i].home_position = home_positions[i % home_count]
|
||||||
|
|
||||||
|
for i in range(npcs.size()):
|
||||||
|
for j in range(i + 1, npcs.size()):
|
||||||
|
if npcs[i].home_position.distance_to(npcs[j].home_position) < 4.0:
|
||||||
|
npcs[i].familiarity[npcs[j].id] = 0.5
|
||||||
|
npcs[j].familiarity[npcs[i].id] = 0.5
|
||||||
|
|
||||||
|
|
||||||
func _create_random_source(npc_id: int, stream_id: int) -> RandomNumberGenerator:
|
func _create_random_source(npc_id: int, stream_id: int) -> RandomNumberGenerator:
|
||||||
var source := RandomNumberGenerator.new()
|
var source := RandomNumberGenerator.new()
|
||||||
@@ -114,11 +145,18 @@ func simulate_tick() -> void:
|
|||||||
and old_state in [SimNPC.TASK_STATE_IDLE, SimNPC.TASK_STATE_COMPLETE]
|
and old_state in [SimNPC.TASK_STATE_IDLE, SimNPC.TASK_STATE_COMPLETE]
|
||||||
and npc.task_state in [SimNPC.TASK_STATE_IDLE, SimNPC.TASK_STATE_COMPLETE]
|
and npc.task_state in [SimNPC.TASK_STATE_IDLE, SimNPC.TASK_STATE_COMPLETE]
|
||||||
):
|
):
|
||||||
var selection := action_selector.select_action(npc, village, clock.time_of_day())
|
var selection := action_selector.select_action(npc, village, clock.time_of_day(), npcs)
|
||||||
if selection != null:
|
if selection != null:
|
||||||
latest_decisions[npc.id] = selection
|
latest_decisions[npc.id] = selection
|
||||||
npc_decision_recorded.emit(npc, selection)
|
npc_decision_recorded.emit(npc, selection)
|
||||||
npc.set_task(selection.action_id, selection.duration_override)
|
npc.set_task(selection.action_id, selection.duration_override)
|
||||||
|
var action_def := SimulationDefinitions.get_action(selection.action_id)
|
||||||
|
var action_display := String(selection.action_id)
|
||||||
|
if action_def != null:
|
||||||
|
action_display = action_def.display_name
|
||||||
|
record_narrative_event(
|
||||||
|
SimulationIds.EVENT_TASK_STARTED, npc.id, &"", action_display
|
||||||
|
)
|
||||||
|
|
||||||
if old_task != npc.current_task and old_target != &"":
|
if old_task != npc.current_task and old_target != &"":
|
||||||
release_npc_reservation(npc.id)
|
release_npc_reservation(npc.id)
|
||||||
@@ -128,6 +166,8 @@ func simulate_tick() -> void:
|
|||||||
release_npc_reservation(npc.id)
|
release_npc_reservation(npc.id)
|
||||||
npc_died.emit(npc)
|
npc_died.emit(npc)
|
||||||
npc_task_changed.emit(npc, old_task, npc.current_task)
|
npc_task_changed.emit(npc, old_task, npc.current_task)
|
||||||
|
record_narrative_event(SimulationIds.EVENT_NPC_DIED, npc.id)
|
||||||
|
_notify_mourning(npc)
|
||||||
|
|
||||||
if debug_logs:
|
if debug_logs:
|
||||||
print("[SimulationManager] NPC died: ", npc.npc_name)
|
print("[SimulationManager] NPC died: ", npc.npc_name)
|
||||||
@@ -155,6 +195,13 @@ func simulate_tick() -> void:
|
|||||||
SimulationIds.RESOURCE_FOOD,
|
SimulationIds.RESOURCE_FOOD,
|
||||||
npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD)
|
npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD)
|
||||||
)
|
)
|
||||||
|
elif resource_state.get_resource_id() == SimulationIds.RESOURCE_WOOD:
|
||||||
|
npc.add_inventory(SimulationIds.RESOURCE_WOOD, extracted)
|
||||||
|
npc_inventory_changed.emit(
|
||||||
|
npc,
|
||||||
|
SimulationIds.RESOURCE_WOOD,
|
||||||
|
npc.get_inventory_amount(SimulationIds.RESOURCE_WOOD)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
village.apply_resource_delta(
|
village.apply_resource_delta(
|
||||||
resource_state.get_resource_id(), extracted
|
resource_state.get_resource_id(), extracted
|
||||||
@@ -165,12 +212,20 @@ func simulate_tick() -> void:
|
|||||||
resource_state.get_node_id(),
|
resource_state.get_node_id(),
|
||||||
(
|
(
|
||||||
_npc_inventory_id(npc.id)
|
_npc_inventory_id(npc.id)
|
||||||
if resource_state.get_resource_id() == SimulationIds.RESOURCE_FOOD
|
if resource_state.get_resource_id() in [
|
||||||
|
SimulationIds.RESOURCE_FOOD, SimulationIds.RESOURCE_WOOD
|
||||||
|
]
|
||||||
else &"village"
|
else &"village"
|
||||||
),
|
),
|
||||||
resource_state.get_resource_id(),
|
resource_state.get_resource_id(),
|
||||||
extracted
|
extracted
|
||||||
)
|
)
|
||||||
|
if resource_state.get_amount_remaining() <= 0.0:
|
||||||
|
record_narrative_event(
|
||||||
|
SimulationIds.EVENT_RESOURCE_DEPLETED,
|
||||||
|
npc.id,
|
||||||
|
resource_state.get_node_id()
|
||||||
|
)
|
||||||
if debug_logs:
|
if debug_logs:
|
||||||
print(
|
print(
|
||||||
"[SimulationManager] ",
|
"[SimulationManager] ",
|
||||||
@@ -193,6 +248,8 @@ func simulate_tick() -> void:
|
|||||||
release_npc_reservation(npc.id)
|
release_npc_reservation(npc.id)
|
||||||
elif completed_task == SimulationIds.ACTION_DEPOSIT_FOOD:
|
elif completed_task == SimulationIds.ACTION_DEPOSIT_FOOD:
|
||||||
deposit_npc_inventory(npc, SimulationIds.RESOURCE_FOOD)
|
deposit_npc_inventory(npc, SimulationIds.RESOURCE_FOOD)
|
||||||
|
elif completed_task == SimulationIds.ACTION_DEPOSIT_WOOD:
|
||||||
|
deposit_npc_wood(npc)
|
||||||
elif completed_task == SimulationIds.ACTION_WITHDRAW_FOOD:
|
elif completed_task == SimulationIds.ACTION_WITHDRAW_FOOD:
|
||||||
withdraw_to_npc(npc, SimulationIds.RESOURCE_FOOD, 1.0)
|
withdraw_to_npc(npc, SimulationIds.RESOURCE_FOOD, 1.0)
|
||||||
elif completed_task == SimulationIds.ACTION_EAT:
|
elif completed_task == SimulationIds.ACTION_EAT:
|
||||||
@@ -200,13 +257,25 @@ func simulate_tick() -> void:
|
|||||||
elif completed_task == SimulationIds.ACTION_SLEEP:
|
elif completed_task == SimulationIds.ACTION_SLEEP:
|
||||||
npc.energy = minf(npc.energy + 40.0, 100.0)
|
npc.energy = minf(npc.energy + 40.0, 100.0)
|
||||||
npc.position = npc.home_position
|
npc.position = npc.home_position
|
||||||
|
record_narrative_event(SimulationIds.EVENT_NPC_SLEPT, npc.id)
|
||||||
if debug_logs:
|
if debug_logs:
|
||||||
print("[SimulationManager] ", npc.npc_name, " completed sleep at home")
|
print("[SimulationManager] ", npc.npc_name, " completed sleep at home")
|
||||||
elif (
|
elif (
|
||||||
completed_task
|
completed_task
|
||||||
not in [SimulationIds.ACTION_GATHER_FOOD, SimulationIds.ACTION_GATHER_WOOD]
|
not in [
|
||||||
|
SimulationIds.ACTION_GATHER_FOOD,
|
||||||
|
SimulationIds.ACTION_GATHER_WOOD,
|
||||||
|
SimulationIds.ACTION_PATROL,
|
||||||
|
SimulationIds.ACTION_STUDY
|
||||||
|
]
|
||||||
):
|
):
|
||||||
village.apply_npc_task(npc)
|
village.apply_npc_task(npc)
|
||||||
|
elif completed_task == SimulationIds.ACTION_PATROL:
|
||||||
|
_consume_wood_for_work(npc, completed_task)
|
||||||
|
village.apply_npc_task(npc)
|
||||||
|
elif completed_task == SimulationIds.ACTION_STUDY:
|
||||||
|
_consume_wood_for_work(npc, completed_task)
|
||||||
|
village.apply_npc_task(npc)
|
||||||
elif debug_logs:
|
elif debug_logs:
|
||||||
print(
|
print(
|
||||||
"[SimulationManager] ",
|
"[SimulationManager] ",
|
||||||
@@ -321,6 +390,23 @@ func notify_npc_arrived(npc_id: int) -> void:
|
|||||||
npc.has_travel_target = false
|
npc.has_travel_target = false
|
||||||
npc.start_working()
|
npc.start_working()
|
||||||
|
|
||||||
|
if npc.target_id != &"":
|
||||||
|
for other in npcs:
|
||||||
|
if other.id == npc.id or other.is_dead:
|
||||||
|
continue
|
||||||
|
if (
|
||||||
|
other.target_id == npc.target_id
|
||||||
|
and other.task_state in [
|
||||||
|
SimNPC.TASK_STATE_TRAVELING, SimNPC.TASK_STATE_WORKING
|
||||||
|
]
|
||||||
|
):
|
||||||
|
npc.familiarity[other.id] = minf(
|
||||||
|
float(npc.familiarity.get(other.id, 0.0)) + 0.1, 1.0
|
||||||
|
)
|
||||||
|
other.familiarity[npc.id] = minf(
|
||||||
|
float(other.familiarity.get(npc.id, 0.0)) + 0.1, 1.0
|
||||||
|
)
|
||||||
|
|
||||||
if debug_logs:
|
if debug_logs:
|
||||||
print(
|
print(
|
||||||
"[SimulationManager] ",
|
"[SimulationManager] ",
|
||||||
@@ -424,6 +510,30 @@ func _npc_inventory_id(npc_id: int) -> StringName:
|
|||||||
return StringName("npc_%d_inventory" % npc_id)
|
return StringName("npc_%d_inventory" % npc_id)
|
||||||
|
|
||||||
|
|
||||||
|
func _notify_mourning(dead_npc: SimNPC) -> void:
|
||||||
|
var best_id := -1
|
||||||
|
var best_score := -1.0
|
||||||
|
for key in dead_npc.familiarity:
|
||||||
|
var other_id: int = int(key)
|
||||||
|
var score: float = float(dead_npc.familiarity[key])
|
||||||
|
if score > best_score:
|
||||||
|
best_score = score
|
||||||
|
best_id = other_id
|
||||||
|
if best_id < 0:
|
||||||
|
return
|
||||||
|
for npc in npcs:
|
||||||
|
if npc.id == best_id and not npc.is_dead:
|
||||||
|
npc.mourning_ticks = 100
|
||||||
|
if debug_logs:
|
||||||
|
print(
|
||||||
|
"[SimulationManager] ",
|
||||||
|
npc.npc_name,
|
||||||
|
" is mourning ",
|
||||||
|
dead_npc.npc_name
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
func _record_economic_event(
|
func _record_economic_event(
|
||||||
event_type: StringName,
|
event_type: StringName,
|
||||||
actor_id: int,
|
actor_id: int,
|
||||||
@@ -442,20 +552,89 @@ func _record_economic_event(
|
|||||||
economic_event_recorded.emit(event)
|
economic_event_recorded.emit(event)
|
||||||
|
|
||||||
|
|
||||||
|
func record_narrative_event(
|
||||||
|
event_type: StringName,
|
||||||
|
actor_id: int,
|
||||||
|
source_id: StringName = &"",
|
||||||
|
action_display: String = ""
|
||||||
|
) -> void:
|
||||||
|
var event := EconomicEventRecord.create_narrative(
|
||||||
|
next_event_id, event_type, tick_count, actor_id, source_id, action_display
|
||||||
|
)
|
||||||
|
next_event_id += 1
|
||||||
|
economic_events.append(event)
|
||||||
|
economic_event_recorded.emit(event)
|
||||||
|
|
||||||
|
|
||||||
|
func get_npc_events(npc_id: int, max_count: int = 8) -> Array[EconomicEventRecord]:
|
||||||
|
var results: Array[EconomicEventRecord] = []
|
||||||
|
for i in range(economic_events.size() - 1, -1, -1):
|
||||||
|
var event: EconomicEventRecord = economic_events[i]
|
||||||
|
if event.data["actor_id"] == npc_id:
|
||||||
|
results.append(event)
|
||||||
|
if results.size() >= max_count:
|
||||||
|
break
|
||||||
|
results.reverse()
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
func get_recent_events(max_count: int = 5) -> Array[EconomicEventRecord]:
|
||||||
|
var results: Array[EconomicEventRecord] = []
|
||||||
|
var start := maxi(economic_events.size() - max_count, 0)
|
||||||
|
for i in range(start, economic_events.size()):
|
||||||
|
results.append(economic_events[i])
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
func get_current_speed() -> String:
|
||||||
|
return "%.2fx" % SPEED_LEVELS[speed_index]
|
||||||
|
|
||||||
|
|
||||||
|
func get_resource_rates() -> Dictionary:
|
||||||
|
var food_consumed := 0.0
|
||||||
|
var wood_consumed := 0.0
|
||||||
|
var recent_ticks := maxi(tick_count - 200, 0)
|
||||||
|
for event in economic_events:
|
||||||
|
if int(event.data["tick"]) < recent_ticks:
|
||||||
|
continue
|
||||||
|
if String(event.data["event_type"]) != "item_consumed":
|
||||||
|
continue
|
||||||
|
if float(event.data["amount"]) <= 0.0:
|
||||||
|
continue
|
||||||
|
if String(event.data["item_id"]) == "food":
|
||||||
|
food_consumed += float(event.data["amount"])
|
||||||
|
elif String(event.data["item_id"]) == "wood":
|
||||||
|
wood_consumed += float(event.data["amount"])
|
||||||
|
var elapsed := maxi(tick_count - recent_ticks, 1)
|
||||||
|
return {
|
||||||
|
"food_per_day": food_consumed / elapsed * 200.0,
|
||||||
|
"wood_per_day": wood_consumed / elapsed * 200.0
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
func _initialize_storage() -> void:
|
func _initialize_storage() -> void:
|
||||||
if storage_states.has(SimulationIds.STORAGE_VILLAGE_PANTRY):
|
if not storage_states.has(SimulationIds.STORAGE_VILLAGE_PANTRY):
|
||||||
_sync_village_food()
|
|
||||||
return
|
|
||||||
storage_states[SimulationIds.STORAGE_VILLAGE_PANTRY] = (StorageStateRecord.create(
|
storage_states[SimulationIds.STORAGE_VILLAGE_PANTRY] = (StorageStateRecord.create(
|
||||||
SimulationIds.STORAGE_VILLAGE_PANTRY, {String(SimulationIds.RESOURCE_FOOD): village.food}
|
SimulationIds.STORAGE_VILLAGE_PANTRY,
|
||||||
|
{String(SimulationIds.RESOURCE_FOOD): village.food}
|
||||||
|
))
|
||||||
|
if not storage_states.has(SimulationIds.STORAGE_VILLAGE_WOODPILE):
|
||||||
|
storage_states[SimulationIds.STORAGE_VILLAGE_WOODPILE] = (StorageStateRecord.create(
|
||||||
|
SimulationIds.STORAGE_VILLAGE_WOODPILE,
|
||||||
|
{String(SimulationIds.RESOURCE_WOOD): village.wood}
|
||||||
))
|
))
|
||||||
_sync_village_food()
|
_sync_village_food()
|
||||||
|
_sync_village_wood()
|
||||||
|
|
||||||
|
|
||||||
func get_pantry() -> StorageStateRecord:
|
func get_pantry() -> StorageStateRecord:
|
||||||
return storage_states.get(SimulationIds.STORAGE_VILLAGE_PANTRY) as StorageStateRecord
|
return storage_states.get(SimulationIds.STORAGE_VILLAGE_PANTRY) as StorageStateRecord
|
||||||
|
|
||||||
|
|
||||||
|
func get_woodpile() -> StorageStateRecord:
|
||||||
|
return storage_states.get(SimulationIds.STORAGE_VILLAGE_WOODPILE) as StorageStateRecord
|
||||||
|
|
||||||
|
|
||||||
func register_loaded_storage_nodes() -> void:
|
func register_loaded_storage_nodes() -> void:
|
||||||
for candidate in StorageNode.get_all():
|
for candidate in StorageNode.get_all():
|
||||||
var node := candidate as StorageNode
|
var node := candidate as StorageNode
|
||||||
@@ -483,6 +662,14 @@ func _sync_village_food() -> void:
|
|||||||
village.update_priorities()
|
village.update_priorities()
|
||||||
|
|
||||||
|
|
||||||
|
func _sync_village_wood() -> void:
|
||||||
|
var woodpile := get_woodpile()
|
||||||
|
if woodpile != null:
|
||||||
|
village.wood = woodpile.get_amount(SimulationIds.RESOURCE_WOOD)
|
||||||
|
village.update_modifiers()
|
||||||
|
village.update_priorities()
|
||||||
|
|
||||||
|
|
||||||
func deposit_npc_inventory(npc: SimNPC, item_id: StringName) -> float:
|
func deposit_npc_inventory(npc: SimNPC, item_id: StringName) -> float:
|
||||||
var available := npc.get_inventory_amount(item_id)
|
var available := npc.get_inventory_amount(item_id)
|
||||||
var deposited := get_pantry().deposit(item_id, available)
|
var deposited := get_pantry().deposit(item_id, available)
|
||||||
@@ -501,6 +688,48 @@ func deposit_npc_inventory(npc: SimNPC, item_id: StringName) -> float:
|
|||||||
return deposited
|
return deposited
|
||||||
|
|
||||||
|
|
||||||
|
func deposit_npc_wood(npc: SimNPC) -> float:
|
||||||
|
var available := npc.get_inventory_amount(SimulationIds.RESOURCE_WOOD)
|
||||||
|
var woodpile := get_woodpile()
|
||||||
|
if woodpile == null:
|
||||||
|
return 0.0
|
||||||
|
var deposited := woodpile.deposit(SimulationIds.RESOURCE_WOOD, available)
|
||||||
|
npc.remove_inventory(SimulationIds.RESOURCE_WOOD, deposited)
|
||||||
|
_sync_village_wood()
|
||||||
|
if deposited > 0.0:
|
||||||
|
npc_inventory_changed.emit(
|
||||||
|
npc, SimulationIds.RESOURCE_WOOD, npc.get_inventory_amount(SimulationIds.RESOURCE_WOOD)
|
||||||
|
)
|
||||||
|
_record_economic_event(
|
||||||
|
SimulationIds.EVENT_STORAGE_DEPOSITED,
|
||||||
|
npc.id,
|
||||||
|
_npc_inventory_id(npc.id),
|
||||||
|
SimulationIds.STORAGE_VILLAGE_WOODPILE,
|
||||||
|
SimulationIds.RESOURCE_WOOD,
|
||||||
|
deposited
|
||||||
|
)
|
||||||
|
return deposited
|
||||||
|
|
||||||
|
|
||||||
|
func _consume_wood_for_work(npc: SimNPC, action_id: StringName) -> void:
|
||||||
|
var woodpile := get_woodpile()
|
||||||
|
if woodpile == null:
|
||||||
|
return
|
||||||
|
var consumed := woodpile.withdraw(SimulationIds.RESOURCE_WOOD, 1.0)
|
||||||
|
_sync_village_wood()
|
||||||
|
if consumed >= 1.0:
|
||||||
|
_record_economic_event(
|
||||||
|
SimulationIds.EVENT_ITEM_CONSUMED,
|
||||||
|
npc.id,
|
||||||
|
SimulationIds.STORAGE_VILLAGE_WOODPILE,
|
||||||
|
&"consumed",
|
||||||
|
SimulationIds.RESOURCE_WOOD,
|
||||||
|
consumed
|
||||||
|
)
|
||||||
|
if debug_logs:
|
||||||
|
print("[SimulationManager] %s consumed wood for %s" % [npc.npc_name, action_id])
|
||||||
|
|
||||||
|
|
||||||
func withdraw_to_npc(npc: SimNPC, item_id: StringName, amount: float) -> float:
|
func withdraw_to_npc(npc: SimNPC, item_id: StringName, amount: float) -> float:
|
||||||
var withdrawn := get_pantry().withdraw(item_id, amount)
|
var withdrawn := get_pantry().withdraw(item_id, amount)
|
||||||
npc.add_inventory(item_id, withdrawn)
|
npc.add_inventory(item_id, withdrawn)
|
||||||
@@ -613,6 +842,9 @@ func harvest_resource_node(node: ResourceNode) -> float:
|
|||||||
if resource_state.get_resource_id() == SimulationIds.RESOURCE_FOOD:
|
if resource_state.get_resource_id() == SimulationIds.RESOURCE_FOOD:
|
||||||
get_pantry().deposit(SimulationIds.RESOURCE_FOOD, extracted)
|
get_pantry().deposit(SimulationIds.RESOURCE_FOOD, extracted)
|
||||||
_sync_village_food()
|
_sync_village_food()
|
||||||
|
elif resource_state.get_resource_id() == SimulationIds.RESOURCE_WOOD:
|
||||||
|
get_woodpile().deposit(SimulationIds.RESOURCE_WOOD, extracted)
|
||||||
|
_sync_village_wood()
|
||||||
else:
|
else:
|
||||||
village.apply_resource_delta(resource_state.get_resource_id(), extracted)
|
village.apply_resource_delta(resource_state.get_resource_id(), extracted)
|
||||||
_record_economic_event(
|
_record_economic_event(
|
||||||
@@ -622,11 +854,19 @@ func harvest_resource_node(node: ResourceNode) -> float:
|
|||||||
(
|
(
|
||||||
SimulationIds.STORAGE_VILLAGE_PANTRY
|
SimulationIds.STORAGE_VILLAGE_PANTRY
|
||||||
if resource_state.get_resource_id() == SimulationIds.RESOURCE_FOOD
|
if resource_state.get_resource_id() == SimulationIds.RESOURCE_FOOD
|
||||||
|
else (
|
||||||
|
SimulationIds.STORAGE_VILLAGE_WOODPILE
|
||||||
|
if resource_state.get_resource_id() == SimulationIds.RESOURCE_WOOD
|
||||||
else &"village"
|
else &"village"
|
||||||
|
)
|
||||||
),
|
),
|
||||||
resource_state.get_resource_id(),
|
resource_state.get_resource_id(),
|
||||||
extracted
|
extracted
|
||||||
)
|
)
|
||||||
|
if resource_state.get_amount_remaining() <= 0.0:
|
||||||
|
record_narrative_event(
|
||||||
|
SimulationIds.EVENT_RESOURCE_DEPLETED, -1, resource_state.get_node_id()
|
||||||
|
)
|
||||||
village_changed.emit(village)
|
village_changed.emit(village)
|
||||||
|
|
||||||
if debug_logs:
|
if debug_logs:
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ func advance_npc(npc: SimNPC, village: SimVillage) -> void:
|
|||||||
if npc.is_dead:
|
if npc.is_dead:
|
||||||
return
|
return
|
||||||
_update_needs(npc, village)
|
_update_needs(npc, village)
|
||||||
|
if npc.mourning_ticks > 0:
|
||||||
|
npc.mourning_ticks -= 1
|
||||||
if npc.is_dead or npc.task_state != SimNPC.TASK_STATE_WORKING:
|
if npc.is_dead or npc.task_state != SimNPC.TASK_STATE_WORKING:
|
||||||
return
|
return
|
||||||
npc.task_progress += 1.0
|
npc.task_progress += 1.0
|
||||||
@@ -15,19 +17,21 @@ func advance_npc(npc: SimNPC, village: SimVillage) -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _update_needs(npc: SimNPC, village: SimVillage) -> void:
|
func _update_needs(npc: SimNPC, village: SimVillage) -> void:
|
||||||
npc.hunger += 3.0 * village.food_modifier
|
npc.hunger += 0.125 * village.food_modifier
|
||||||
match npc.task_state:
|
match npc.task_state:
|
||||||
SimNPC.TASK_STATE_IDLE:
|
SimNPC.TASK_STATE_IDLE:
|
||||||
npc.energy -= 0.5
|
npc.energy -= 0.125
|
||||||
SimNPC.TASK_STATE_TRAVELING:
|
SimNPC.TASK_STATE_TRAVELING:
|
||||||
npc.energy -= 2.0
|
npc.energy -= 0.5
|
||||||
SimNPC.TASK_STATE_WORKING:
|
SimNPC.TASK_STATE_WORKING:
|
||||||
if npc.current_task == SimulationIds.ACTION_SLEEP:
|
if npc.current_task == SimulationIds.ACTION_SLEEP:
|
||||||
npc.energy = minf(npc.energy + 5.0, 100.0)
|
npc.energy = minf(npc.energy + 1.0, 100.0)
|
||||||
elif npc.current_task != SimulationIds.ACTION_REST:
|
elif npc.current_task == SimulationIds.ACTION_REST:
|
||||||
npc.energy -= 3.0
|
pass
|
||||||
|
else:
|
||||||
|
npc.energy -= 0.5
|
||||||
SimNPC.TASK_STATE_COMPLETE:
|
SimNPC.TASK_STATE_COMPLETE:
|
||||||
npc.energy -= 0.25
|
npc.energy -= 0.0625
|
||||||
npc.hunger = clamp(npc.hunger, 0.0, 100.0)
|
npc.hunger = clamp(npc.hunger, 0.0, 100.0)
|
||||||
npc.energy = clamp(npc.energy, 0.0, 100.0)
|
npc.energy = clamp(npc.energy, 0.0, 100.0)
|
||||||
npc.is_starving = npc.hunger >= 90.0
|
npc.is_starving = npc.hunger >= 90.0
|
||||||
|
|||||||
@@ -18,11 +18,47 @@ const WORK_BEGIN := 0.28
|
|||||||
const WORK_END := 0.85
|
const WORK_END := 0.85
|
||||||
|
|
||||||
|
|
||||||
func select_action(npc: SimNPC, village: SimVillage, time_of_day: float = 0.5) -> ActionSelectionResult:
|
func select_action(npc: SimNPC, village: SimVillage, time_of_day: float = 0.5, all_npcs: Array = []) -> ActionSelectionResult:
|
||||||
if npc.is_dead:
|
if npc.is_dead:
|
||||||
return null
|
return null
|
||||||
|
|
||||||
var period := _get_period(time_of_day)
|
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.mourning_ticks > 0:
|
||||||
|
if npc.energy < 40.0:
|
||||||
|
return ActionSelectionResult.new(
|
||||||
|
SimulationIds.ACTION_REST, -1.0, "Mourning; seeking rest"
|
||||||
|
)
|
||||||
|
return ActionSelectionResult.new(
|
||||||
|
SimulationIds.ACTION_WANDER, -1.0, "Mourning; wandering near home"
|
||||||
|
)
|
||||||
|
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 profession_def := SimulationDefinitions.get_profession(npc.profession)
|
||||||
|
var offset: float = profession_def.schedule_offset if profession_def != null else 0.0
|
||||||
|
var local_time := fmod(time_of_day + offset + 1.0, 1.0)
|
||||||
|
var period := _get_period(local_time)
|
||||||
|
|
||||||
if period == SchedulePeriod.SLEEP:
|
if period == SchedulePeriod.SLEEP:
|
||||||
if npc.energy < 60.0:
|
if npc.energy < 60.0:
|
||||||
@@ -79,6 +115,10 @@ func select_action(npc: SimNPC, village: SimVillage, time_of_day: float = 0.5) -
|
|||||||
return ActionSelectionResult.new(
|
return ActionSelectionResult.new(
|
||||||
SimulationIds.ACTION_DEPOSIT_FOOD, -1.0, "Carrying food for storage"
|
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:
|
if npc.energy < 25.0:
|
||||||
return ActionSelectionResult.new(
|
return ActionSelectionResult.new(
|
||||||
SimulationIds.ACTION_REST, -1.0, "Energy is below the rest threshold"
|
SimulationIds.ACTION_REST, -1.0, "Energy is below the rest threshold"
|
||||||
@@ -91,10 +131,10 @@ func select_action(npc: SimNPC, village: SimVillage, time_of_day: float = 0.5) -
|
|||||||
SimulationIds.ACTION_WANDER, -1.0, "Discretionary wander"
|
SimulationIds.ACTION_WANDER, -1.0, "Discretionary wander"
|
||||||
)
|
)
|
||||||
|
|
||||||
return _choose_best_work_action(npc, village)
|
return _choose_best_work_action(npc, village, all_npcs)
|
||||||
|
|
||||||
|
|
||||||
func _choose_best_work_action(npc: SimNPC, village: SimVillage) -> ActionSelectionResult:
|
func _choose_best_work_action(npc: SimNPC, village: SimVillage, all_npcs: Array) -> ActionSelectionResult:
|
||||||
var scores := {
|
var scores := {
|
||||||
SimulationIds.ACTION_GATHER_FOOD:
|
SimulationIds.ACTION_GATHER_FOOD:
|
||||||
_calculate_score(
|
_calculate_score(
|
||||||
@@ -141,6 +181,16 @@ func _choose_best_work_action(npc: SimNPC, village: SimVillage) -> ActionSelecti
|
|||||||
0.75
|
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_action := SimulationIds.ACTION_GATHER_FOOD
|
||||||
var best_score: float = scores[best_action]
|
var best_score: float = scores[best_action]
|
||||||
for action_id in [
|
for action_id in [
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ extends Resource
|
|||||||
@export var display_name: String
|
@export var display_name: String
|
||||||
@export var visual_color := Color.WHITE
|
@export var visual_color := Color.WHITE
|
||||||
@export var prop_color := Color(0.35, 0.25, 0.15, 1.0)
|
@export var prop_color := Color(0.35, 0.25, 0.15, 1.0)
|
||||||
|
@export_range(-0.12, 0.12, 0.005) var schedule_offset := 0.0
|
||||||
|
|
||||||
|
|
||||||
func validate() -> Array[String]:
|
func validate() -> Array[String]:
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ const ACTION_PATHS := [
|
|||||||
"res://simulation/definitions/actions/wander.tres",
|
"res://simulation/definitions/actions/wander.tres",
|
||||||
"res://simulation/definitions/actions/deposit_food.tres",
|
"res://simulation/definitions/actions/deposit_food.tres",
|
||||||
"res://simulation/definitions/actions/withdraw_food.tres",
|
"res://simulation/definitions/actions/withdraw_food.tres",
|
||||||
"res://simulation/definitions/actions/sleep.tres"
|
"res://simulation/definitions/actions/sleep.tres",
|
||||||
|
"res://simulation/definitions/actions/deposit_wood.tres"
|
||||||
]
|
]
|
||||||
|
|
||||||
const PROFESSION_PATHS := [
|
const PROFESSION_PATHS := [
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const ACTION_REST := &"rest"
|
|||||||
const ACTION_WANDER := &"wander"
|
const ACTION_WANDER := &"wander"
|
||||||
const ACTION_DEPOSIT_FOOD := &"deposit_food"
|
const ACTION_DEPOSIT_FOOD := &"deposit_food"
|
||||||
const ACTION_WITHDRAW_FOOD := &"withdraw_food"
|
const ACTION_WITHDRAW_FOOD := &"withdraw_food"
|
||||||
|
const ACTION_DEPOSIT_WOOD := &"deposit_wood"
|
||||||
|
|
||||||
const PROFESSION_FARMER := &"farmer"
|
const PROFESSION_FARMER := &"farmer"
|
||||||
const PROFESSION_WOODCUTTER := &"woodcutter"
|
const PROFESSION_WOODCUTTER := &"woodcutter"
|
||||||
@@ -28,8 +29,13 @@ const RESOURCE_FOOD := &"food"
|
|||||||
const RESOURCE_WOOD := &"wood"
|
const RESOURCE_WOOD := &"wood"
|
||||||
|
|
||||||
const STORAGE_VILLAGE_PANTRY := &"village_pantry"
|
const STORAGE_VILLAGE_PANTRY := &"village_pantry"
|
||||||
|
const STORAGE_VILLAGE_WOODPILE := &"village_woodpile"
|
||||||
|
|
||||||
const EVENT_RESOURCE_EXTRACTED := &"resource_extracted"
|
const EVENT_RESOURCE_EXTRACTED := &"resource_extracted"
|
||||||
const EVENT_STORAGE_DEPOSITED := &"storage_deposited"
|
const EVENT_STORAGE_DEPOSITED := &"storage_deposited"
|
||||||
const EVENT_STORAGE_WITHDRAWN := &"storage_withdrawn"
|
const EVENT_STORAGE_WITHDRAWN := &"storage_withdrawn"
|
||||||
const EVENT_ITEM_CONSUMED := &"item_consumed"
|
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_RESOURCE_DEPLETED := &"resource_depleted"
|
||||||
|
|||||||
@@ -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 = &"deposit_wood"
|
||||||
|
display_name = "Deposit Wood"
|
||||||
|
default_duration = 1.0
|
||||||
|
target_type = &"activity"
|
||||||
@@ -8,3 +8,4 @@ profession_id = &"farmer"
|
|||||||
display_name = "Farmer"
|
display_name = "Farmer"
|
||||||
visual_color = Color(0.36, 0.62, 0.28, 1)
|
visual_color = Color(0.36, 0.62, 0.28, 1)
|
||||||
prop_color = Color(0.65, 0.45, 0.2, 1)
|
prop_color = Color(0.65, 0.45, 0.2, 1)
|
||||||
|
schedule_offset = -0.04
|
||||||
|
|||||||
@@ -8,3 +8,4 @@ profession_id = &"guard"
|
|||||||
display_name = "Guard"
|
display_name = "Guard"
|
||||||
visual_color = Color(0.25, 0.42, 0.68, 1)
|
visual_color = Color(0.25, 0.42, 0.68, 1)
|
||||||
prop_color = Color(0.72, 0.75, 0.78, 1)
|
prop_color = Color(0.72, 0.75, 0.78, 1)
|
||||||
|
schedule_offset = 0.02
|
||||||
|
|||||||
@@ -8,3 +8,4 @@ profession_id = &"scholar"
|
|||||||
display_name = "Scholar"
|
display_name = "Scholar"
|
||||||
visual_color = Color(0.52, 0.34, 0.68, 1)
|
visual_color = Color(0.52, 0.34, 0.68, 1)
|
||||||
prop_color = Color(0.78, 0.66, 0.28, 1)
|
prop_color = Color(0.78, 0.66, 0.28, 1)
|
||||||
|
schedule_offset = 0.05
|
||||||
|
|||||||
@@ -8,3 +8,4 @@ profession_id = &"woodcutter"
|
|||||||
display_name = "Woodcutter"
|
display_name = "Woodcutter"
|
||||||
visual_color = Color(0.67, 0.34, 0.2, 1)
|
visual_color = Color(0.67, 0.34, 0.2, 1)
|
||||||
prop_color = Color(0.34, 0.22, 0.14, 1)
|
prop_color = Color(0.34, 0.22, 0.14, 1)
|
||||||
|
schedule_offset = -0.02
|
||||||
|
|||||||
@@ -35,6 +35,30 @@ static func create(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
static func create_narrative(
|
||||||
|
event_id: int,
|
||||||
|
event_type: StringName,
|
||||||
|
tick: int,
|
||||||
|
actor_id: int,
|
||||||
|
source_id: StringName,
|
||||||
|
action_display: String = ""
|
||||||
|
) -> EconomicEventRecord:
|
||||||
|
return EconomicEventRecord.new(
|
||||||
|
{
|
||||||
|
"schema_version": SCHEMA_VERSION,
|
||||||
|
"event_id": event_id,
|
||||||
|
"event_type": String(event_type),
|
||||||
|
"tick": tick,
|
||||||
|
"actor_id": actor_id,
|
||||||
|
"source_id": String(source_id),
|
||||||
|
"destination_id": "",
|
||||||
|
"item_id": "",
|
||||||
|
"amount": 0.0,
|
||||||
|
"action_name": action_display
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
static func from_dictionary(record_data: Dictionary) -> EconomicEventRecord:
|
static func from_dictionary(record_data: Dictionary) -> EconomicEventRecord:
|
||||||
if int(record_data.get("schema_version", -1)) != SCHEMA_VERSION:
|
if int(record_data.get("schema_version", -1)) != SCHEMA_VERSION:
|
||||||
return null
|
return null
|
||||||
@@ -65,10 +89,6 @@ static func from_dictionary(record_data: Dictionary) -> EconomicEventRecord:
|
|||||||
normalized["event_id"] < 0
|
normalized["event_id"] < 0
|
||||||
or normalized["tick"] < 0
|
or normalized["tick"] < 0
|
||||||
or normalized["event_type"].is_empty()
|
or normalized["event_type"].is_empty()
|
||||||
or normalized["source_id"].is_empty()
|
|
||||||
or normalized["destination_id"].is_empty()
|
|
||||||
or normalized["item_id"].is_empty()
|
|
||||||
or normalized["amount"] <= 0.0
|
|
||||||
):
|
):
|
||||||
return null
|
return null
|
||||||
return EconomicEventRecord.new(normalized)
|
return EconomicEventRecord.new(normalized)
|
||||||
@@ -76,3 +96,32 @@ static func from_dictionary(record_data: Dictionary) -> EconomicEventRecord:
|
|||||||
|
|
||||||
func to_dictionary() -> Dictionary:
|
func to_dictionary() -> Dictionary:
|
||||||
return data.duplicate(true)
|
return data.duplicate(true)
|
||||||
|
|
||||||
|
|
||||||
|
func description(npc_names: Dictionary = {}) -> String:
|
||||||
|
var actor_name: String = npc_names.get(int(data["actor_id"]), "Someone")
|
||||||
|
var item: String = str(data["item_id"])
|
||||||
|
var amount: float = float(data["amount"])
|
||||||
|
var event_type: String = str(data["event_type"])
|
||||||
|
var source: String = str(data["source_id"])
|
||||||
|
var destination: String = str(data["destination_id"])
|
||||||
|
var action_name: String = str(data.get("action_name", ""))
|
||||||
|
match event_type:
|
||||||
|
"resource_extracted":
|
||||||
|
return "%s gathered %.0f %s from %s" % [actor_name, amount, item, source]
|
||||||
|
"storage_deposited":
|
||||||
|
return "%s deposited %.0f %s into %s" % [actor_name, amount, item, destination]
|
||||||
|
"storage_withdrawn":
|
||||||
|
return "%s withdrew %.0f %s from %s" % [actor_name, amount, item, source]
|
||||||
|
"item_consumed":
|
||||||
|
return "%s consumed %.0f %s" % [actor_name, amount, item]
|
||||||
|
"npc_slept":
|
||||||
|
return "%s slept and recovered energy" % actor_name
|
||||||
|
"npc_died":
|
||||||
|
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]
|
||||||
|
"resource_depleted":
|
||||||
|
return "%s was depleted" % source
|
||||||
|
_:
|
||||||
|
return "tick %d: %s" % [int(data["tick"]), event_type]
|
||||||
|
|||||||
@@ -45,7 +45,9 @@ static func capture(npc: SimNPC) -> NPCStateRecord:
|
|||||||
"inventory": npc.inventory.duplicate(true),
|
"inventory": npc.inventory.duplicate(true),
|
||||||
"last_task": String(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),
|
||||||
|
"familiarity": _sorted_familiarity(npc.familiarity),
|
||||||
|
"mourning_ticks": npc.mourning_ticks
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -160,8 +162,28 @@ func restore(debug_logs: bool) -> SimNPC:
|
|||||||
npc.last_task = StringName(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
|
||||||
|
var saved_familiarity = data.get("familiarity", {})
|
||||||
|
if saved_familiarity is Array:
|
||||||
|
for pair in saved_familiarity:
|
||||||
|
var pair_dict: Dictionary = pair
|
||||||
|
npc.familiarity[int(pair_dict["id"])] = float(pair_dict["score"])
|
||||||
|
elif saved_familiarity is Dictionary:
|
||||||
|
npc.familiarity = saved_familiarity.duplicate(true)
|
||||||
|
npc.mourning_ticks = int(data.get("mourning_ticks", 0))
|
||||||
return npc
|
return npc
|
||||||
|
|
||||||
|
|
||||||
func to_dictionary() -> Dictionary:
|
func to_dictionary() -> Dictionary:
|
||||||
return data.duplicate(true)
|
return data.duplicate(true)
|
||||||
|
|
||||||
|
|
||||||
|
static func _sorted_familiarity(familiarity: Dictionary) -> Array:
|
||||||
|
var pairs: Array[Dictionary] = []
|
||||||
|
for key in familiarity:
|
||||||
|
pairs.append({"id": int(key), "score": float(familiarity[key])})
|
||||||
|
pairs.sort_custom(_familiarity_sort)
|
||||||
|
return pairs
|
||||||
|
|
||||||
|
|
||||||
|
static func _familiarity_sort(a: Dictionary, b: Dictionary) -> bool:
|
||||||
|
return int(a["id"]) < int(b["id"])
|
||||||
|
|||||||
@@ -1,16 +1,33 @@
|
|||||||
[gd_resource type="Terrain3DAssets" load_steps=14 format=3]
|
[gd_resource type="Terrain3DAssets" format=3 uid="uid://bdv3tfrg08t8d"]
|
||||||
|
|
||||||
[ext_resource type="Texture2D" path="res://terrain/jajce/textures/rock_albedo.png" id="1_rock"]
|
[ext_resource type="Texture2D" uid="uid://cf16txygbbyqb" path="res://terrain/jajce/textures/rock_albedo.png" id="1_rock"]
|
||||||
[ext_resource type="Texture2D" path="res://terrain/jajce/textures/grass_albedo.png" id="2_grass"]
|
[ext_resource type="Texture2D" uid="uid://d0ihxulegsl2w" path="res://terrain/jajce/textures/grass_albedo.png" id="2_grass"]
|
||||||
[ext_resource type="Texture2D" path="res://terrain/jajce/textures/soil_albedo.png" id="3_soil"]
|
[ext_resource type="Texture2D" uid="uid://b75t0ego2sdja" path="res://terrain/jajce/textures/soil_albedo.png" id="3_soil"]
|
||||||
[ext_resource type="Texture2D" path="res://terrain/jajce/textures/dirt_path_albedo.png" id="4_dirt"]
|
[ext_resource type="Texture2D" uid="uid://ca7nmo3t6rhtj" path="res://terrain/jajce/textures/dirt_path_albedo.png" id="4_dirt"]
|
||||||
[ext_resource type="Texture2D" path="res://terrain/jajce/textures/riverbank_stone_albedo.png" id="5_river"]
|
[ext_resource type="Texture2D" uid="uid://bsl74sdmhj270" path="res://terrain/jajce/textures/riverbank_stone_albedo.png" id="5_river"]
|
||||||
[ext_resource type="Texture2D" path="res://terrain/jajce/textures/compacted_ground_albedo.png" id="6_compact"]
|
[ext_resource type="Texture2D" uid="uid://e0ahjmsbpagw" path="res://terrain/jajce/textures/compacted_ground_albedo.png" id="6_compact"]
|
||||||
[ext_resource type="Texture2D" path="res://terrain/jajce/textures/neutral_normal.png" id="7_normal"]
|
[ext_resource type="Texture2D" uid="uid://bjcbxamqe7yji" path="res://terrain/jajce/textures/neutral_normal.png" id="7_normal"]
|
||||||
|
|
||||||
|
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_whuny"]
|
||||||
|
transparency = 4
|
||||||
|
cull_mode = 2
|
||||||
|
vertex_color_use_as_albedo = true
|
||||||
|
backlight_enabled = true
|
||||||
|
backlight = Color(0.5, 0.5, 0.5, 1)
|
||||||
|
distance_fade_mode = 1
|
||||||
|
distance_fade_min_distance = 128.0
|
||||||
|
distance_fade_max_distance = 96.0
|
||||||
|
|
||||||
|
[sub_resource type="Terrain3DMeshAsset" id="Terrain3DMeshAsset_2g08h"]
|
||||||
|
generated_type = 1
|
||||||
|
height_offset = 0.5
|
||||||
|
material_override = SubResource("StandardMaterial3D_whuny")
|
||||||
|
last_lod = 0
|
||||||
|
last_shadow_lod = 0
|
||||||
|
lod0_range = 128.0
|
||||||
|
|
||||||
[sub_resource type="Terrain3DTextureAsset" id="Texture_rock"]
|
[sub_resource type="Terrain3DTextureAsset" id="Texture_rock"]
|
||||||
name = "Limestone"
|
name = "Limestone"
|
||||||
id = 0
|
|
||||||
albedo_texture = ExtResource("1_rock")
|
albedo_texture = ExtResource("1_rock")
|
||||||
normal_texture = ExtResource("7_normal")
|
normal_texture = ExtResource("7_normal")
|
||||||
normal_depth = 0.0
|
normal_depth = 0.0
|
||||||
@@ -63,4 +80,5 @@ uv_scale = 0.2
|
|||||||
detiling_rotation = 0.08
|
detiling_rotation = 0.08
|
||||||
|
|
||||||
[resource]
|
[resource]
|
||||||
|
mesh_list = Array[Terrain3DMeshAsset]([SubResource("Terrain3DMeshAsset_2g08h")])
|
||||||
texture_list = Array[Terrain3DTextureAsset]([SubResource("Texture_rock"), SubResource("Texture_grass"), SubResource("Texture_soil"), SubResource("Texture_dirt"), SubResource("Texture_river"), SubResource("Texture_compact")])
|
texture_list = Array[Terrain3DTextureAsset]([SubResource("Texture_rock"), SubResource("Texture_grass"), SubResource("Texture_soil"), SubResource("Texture_dirt"), SubResource("Texture_river"), SubResource("Texture_compact")])
|
||||||
|
|||||||
@@ -150,17 +150,21 @@ func _check_economic_event_chain(manager: Node) -> void:
|
|||||||
SimulationIds.EVENT_STORAGE_WITHDRAWN,
|
SimulationIds.EVENT_STORAGE_WITHDRAWN,
|
||||||
SimulationIds.EVENT_ITEM_CONSUMED
|
SimulationIds.EVENT_ITEM_CONSUMED
|
||||||
]
|
]
|
||||||
|
var transfer_events: Array[EconomicEventRecord] = []
|
||||||
|
for event in manager.economic_events:
|
||||||
|
if float(event.data["amount"]) > 0.0:
|
||||||
|
transfer_events.append(event)
|
||||||
_check(
|
_check(
|
||||||
manager.economic_events.size() == expected_types.size(),
|
transfer_events.size() == expected_types.size(),
|
||||||
"Food transfer loop should emit one structured event per transfer"
|
"Food transfer loop should emit one structured event per transfer"
|
||||||
)
|
)
|
||||||
if manager.economic_events.size() != expected_types.size():
|
if transfer_events.size() != expected_types.size():
|
||||||
return
|
return
|
||||||
for index in expected_types.size():
|
for index in expected_types.size():
|
||||||
var event: EconomicEventRecord = manager.economic_events[index]
|
var event: EconomicEventRecord = transfer_events[index]
|
||||||
_check(
|
_check(
|
||||||
(
|
(
|
||||||
int(event.data["event_id"]) == index
|
int(event.data["event_id"]) >= 0
|
||||||
and StringName(event.data["event_type"]) == expected_types[index]
|
and StringName(event.data["event_type"]) == expected_types[index]
|
||||||
and StringName(event.data["item_id"]) == SimulationIds.RESOURCE_FOOD
|
and StringName(event.data["item_id"]) == SimulationIds.RESOURCE_FOOD
|
||||||
),
|
),
|
||||||
@@ -168,10 +172,10 @@ func _check_economic_event_chain(manager: Node) -> void:
|
|||||||
)
|
)
|
||||||
_check(
|
_check(
|
||||||
(
|
(
|
||||||
is_equal_approx(float(manager.economic_events[0].data["amount"]), 2.0)
|
is_equal_approx(float(transfer_events[0].data["amount"]), 2.0)
|
||||||
and is_equal_approx(float(manager.economic_events[1].data["amount"]), 2.0)
|
and is_equal_approx(float(transfer_events[1].data["amount"]), 2.0)
|
||||||
and is_equal_approx(float(manager.economic_events[2].data["amount"]), 1.0)
|
and is_equal_approx(float(transfer_events[2].data["amount"]), 1.0)
|
||||||
and is_equal_approx(float(manager.economic_events[3].data["amount"]), 1.0)
|
and is_equal_approx(float(transfer_events[3].data["amount"]), 1.0)
|
||||||
),
|
),
|
||||||
"Economic events should record exact transferred quantities"
|
"Economic events should record exact transferred quantities"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -106,6 +106,12 @@ func _run() -> void:
|
|||||||
)
|
)
|
||||||
var pantry := main_scene.get_node("JajceWorld/WorldObjects/StorageSites/VillagePantry") as StorageNode
|
var pantry := main_scene.get_node("JajceWorld/WorldObjects/StorageSites/VillagePantry") as StorageNode
|
||||||
destinations.append(pantry.get_interaction_position())
|
destinations.append(pantry.get_interaction_position())
|
||||||
|
_check(
|
||||||
|
main_scene.has_node("JajceWorld/WorldObjects/StorageSites/VillageWoodpile"),
|
||||||
|
"Runtime should expose a typed VillageWoodpile StorageNode"
|
||||||
|
)
|
||||||
|
var woodpile := main_scene.get_node("JajceWorld/WorldObjects/StorageSites/VillageWoodpile") as StorageNode
|
||||||
|
destinations.append(woodpile.get_interaction_position())
|
||||||
var activity_root := main_scene.get_node("JajceWorld/WorldObjects/ActivitySites")
|
var activity_root := main_scene.get_node("JajceWorld/WorldObjects/ActivitySites")
|
||||||
_check(activity_root.get_child_count() == 3, "Runtime should expose three typed activity sites")
|
_check(activity_root.get_child_count() == 3, "Runtime should expose three typed activity sites")
|
||||||
for site in activity_root.get_children():
|
for site in activity_root.get_children():
|
||||||
@@ -149,6 +155,11 @@ func _run() -> void:
|
|||||||
storage_target.get("target_id", "") == String(SimulationIds.STORAGE_VILLAGE_PANTRY),
|
storage_target.get("target_id", "") == String(SimulationIds.STORAGE_VILLAGE_PANTRY),
|
||||||
"Runtime adapter should route storage actions to village_pantry"
|
"Runtime adapter should route storage actions to village_pantry"
|
||||||
)
|
)
|
||||||
|
var wood_target := adapter.get_activity_target(SimulationIds.ACTION_DEPOSIT_WOOD)
|
||||||
|
_check(
|
||||||
|
wood_target.get("target_id", "") == String(SimulationIds.STORAGE_VILLAGE_WOODPILE),
|
||||||
|
"Runtime adapter should route wood deposit to village_woodpile"
|
||||||
|
)
|
||||||
var study_target := adapter.get_activity_target(SimulationIds.ACTION_STUDY)
|
var study_target := adapter.get_activity_target(SimulationIds.ACTION_STUDY)
|
||||||
_check(
|
_check(
|
||||||
study_target.get("target_id", "") == "study_desk",
|
study_target.get("target_id", "") == "study_desk",
|
||||||
|
|||||||
@@ -187,6 +187,12 @@ func _run() -> void:
|
|||||||
pantry.storage_id == SimulationIds.STORAGE_VILLAGE_PANTRY,
|
pantry.storage_id == SimulationIds.STORAGE_VILLAGE_PANTRY,
|
||||||
"VillagePantry should preserve the stable storage ID"
|
"VillagePantry should preserve the stable storage ID"
|
||||||
)
|
)
|
||||||
|
var woodpile := world.get_node("WorldObjects/StorageSites/VillageWoodpile") as StorageNode
|
||||||
|
_check(woodpile != null, "JajceWorld should include a typed VillageWoodpile StorageNode")
|
||||||
|
_check(
|
||||||
|
woodpile.storage_id == SimulationIds.STORAGE_VILLAGE_WOODPILE,
|
||||||
|
"VillageWoodpile should preserve the stable storage ID"
|
||||||
|
)
|
||||||
_check(
|
_check(
|
||||||
not world.has_node("LegacyActivityMarkers/PantryMarker"),
|
not world.has_node("LegacyActivityMarkers/PantryMarker"),
|
||||||
"Pantry should no longer be a legacy activity marker"
|
"Pantry should no longer be a legacy activity marker"
|
||||||
@@ -224,6 +230,7 @@ func _run() -> void:
|
|||||||
for resource in resources:
|
for resource in resources:
|
||||||
destinations.append((resource as ResourceNode).interaction_point.global_position)
|
destinations.append((resource as ResourceNode).interaction_point.global_position)
|
||||||
destinations.append(pantry.get_interaction_position())
|
destinations.append(pantry.get_interaction_position())
|
||||||
|
destinations.append(woodpile.get_interaction_position())
|
||||||
for site in activity_root.get_children():
|
for site in activity_root.get_children():
|
||||||
destinations.append((site as ActivitySite).get_interaction_position())
|
destinations.append((site as ActivitySite).get_interaction_position())
|
||||||
|
|
||||||
@@ -243,6 +250,7 @@ func _run() -> void:
|
|||||||
|
|
||||||
var adapter := ActiveWorldAdapter.new()
|
var adapter := ActiveWorldAdapter.new()
|
||||||
adapter.pantry_storage = pantry
|
adapter.pantry_storage = pantry
|
||||||
|
adapter.woodpile_storage = woodpile
|
||||||
root.add_child(adapter)
|
root.add_child(adapter)
|
||||||
var food_candidates := adapter.get_resource_candidates(SimulationIds.ACTION_GATHER_FOOD)
|
var food_candidates := adapter.get_resource_candidates(SimulationIds.ACTION_GATHER_FOOD)
|
||||||
_check(food_candidates.size() >= 9, "Adapter should expose all authored food candidates")
|
_check(food_candidates.size() >= 9, "Adapter should expose all authored food candidates")
|
||||||
@@ -296,6 +304,11 @@ func _run() -> void:
|
|||||||
storage_target_position.distance_to(pantry.get_interaction_position()) < 0.01,
|
storage_target_position.distance_to(pantry.get_interaction_position()) < 0.01,
|
||||||
"Storage actions should use the pantry StorageNode interaction point"
|
"Storage actions should use the pantry StorageNode interaction point"
|
||||||
)
|
)
|
||||||
|
var wood_target := adapter.get_activity_target(SimulationIds.ACTION_DEPOSIT_WOOD)
|
||||||
|
_check(
|
||||||
|
wood_target.get("target_id", "") == String(SimulationIds.STORAGE_VILLAGE_WOODPILE),
|
||||||
|
"Wood deposit should target the typed woodpile's stable ID"
|
||||||
|
)
|
||||||
var rest_target := adapter.get_activity_target(SimulationIds.ACTION_REST)
|
var rest_target := adapter.get_activity_target(SimulationIds.ACTION_REST)
|
||||||
_check(
|
_check(
|
||||||
rest_target.get("target_id", "") == "rest_bench",
|
rest_target.get("target_id", "") == "rest_bench",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ func _run() -> void:
|
|||||||
_test_sleep_restores_energy()
|
_test_sleep_restores_energy()
|
||||||
_test_schedule_serialization()
|
_test_schedule_serialization()
|
||||||
_test_sleep_targets_home()
|
_test_sleep_targets_home()
|
||||||
|
_test_household_familiarity()
|
||||||
|
|
||||||
if failures.is_empty():
|
if failures.is_empty():
|
||||||
print("[TEST] NPC schedule passed")
|
print("[TEST] NPC schedule passed")
|
||||||
@@ -68,6 +69,7 @@ func _test_work_period_during_day() -> void:
|
|||||||
"100 ticks should land in the work period"
|
"100 ticks should land in the work period"
|
||||||
)
|
)
|
||||||
var npc: SimNPC = manager.npcs[0]
|
var npc: SimNPC = manager.npcs[0]
|
||||||
|
npc.profession = SimulationIds.PROFESSION_WANDERER
|
||||||
npc.hunger = 20.0
|
npc.hunger = 20.0
|
||||||
npc.energy = 80.0
|
npc.energy = 80.0
|
||||||
npc.home_position = npc.position
|
npc.home_position = npc.position
|
||||||
@@ -93,6 +95,7 @@ func _test_meal_period_hunger() -> void:
|
|||||||
"50 ticks should land in the breakfast meal period"
|
"50 ticks should land in the breakfast meal period"
|
||||||
)
|
)
|
||||||
var npc: SimNPC = manager.npcs[0]
|
var npc: SimNPC = manager.npcs[0]
|
||||||
|
npc.profession = SimulationIds.PROFESSION_WANDERER
|
||||||
npc.hunger = 65.0
|
npc.hunger = 65.0
|
||||||
npc.energy = 80.0
|
npc.energy = 80.0
|
||||||
manager.village.food = 5.0
|
manager.village.food = 5.0
|
||||||
@@ -181,6 +184,63 @@ func _test_sleep_targets_home() -> void:
|
|||||||
manager.free()
|
manager.free()
|
||||||
|
|
||||||
|
|
||||||
|
func _test_household_familiarity() -> void:
|
||||||
|
var manager: Node = load("res://simulation/SimulationManager.gd").new()
|
||||||
|
manager.simulation_seed = 700
|
||||||
|
manager.debug_logs = false
|
||||||
|
manager.set_process(false)
|
||||||
|
manager.home_positions = [
|
||||||
|
Vector3(-15.0, 2.0, 7.0),
|
||||||
|
Vector3(-15.0, 2.0, 8.0),
|
||||||
|
Vector3(-8.5, 2.0, 3.0),
|
||||||
|
Vector3(-6.5, 2.0, 1.5),
|
||||||
|
Vector3(20.0, 0.0, 20.0),
|
||||||
|
Vector3(22.0, 0.0, 22.0)
|
||||||
|
]
|
||||||
|
root.add_child(manager)
|
||||||
|
|
||||||
|
var a: SimNPC = manager.npcs[0]
|
||||||
|
var b: SimNPC = manager.npcs[1]
|
||||||
|
var c: SimNPC = manager.npcs[2]
|
||||||
|
var d: SimNPC = manager.npcs[3]
|
||||||
|
var e: SimNPC = manager.npcs[4]
|
||||||
|
|
||||||
|
_check(
|
||||||
|
a.familiarity.has(b.id) and b.familiarity.has(a.id),
|
||||||
|
"NPCs with nearby homes should be mutual household members"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
is_equal_approx(float(a.familiarity[b.id]), 0.5),
|
||||||
|
"Household familiarity should start at baseline 0.5"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
c.familiarity.has(d.id) and d.familiarity.has(c.id),
|
||||||
|
"Second household pair should also be familiar"
|
||||||
|
)
|
||||||
|
_check(
|
||||||
|
not a.familiarity.has(e.id),
|
||||||
|
"Distant NPCs should not have household familiarity"
|
||||||
|
)
|
||||||
|
|
||||||
|
var saved_json: 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(saved_json),
|
||||||
|
"Familiarity should survive save/load round-trip"
|
||||||
|
)
|
||||||
|
var ra: SimNPC = restored.npcs[0]
|
||||||
|
_check(
|
||||||
|
ra.familiarity.size() > 0,
|
||||||
|
"Restored NPC should retain household familiarity"
|
||||||
|
)
|
||||||
|
|
||||||
|
manager.free()
|
||||||
|
restored.free()
|
||||||
|
|
||||||
|
|
||||||
func _check(condition: bool, message: String) -> void:
|
func _check(condition: bool, message: String) -> void:
|
||||||
if not condition:
|
if not condition:
|
||||||
failures.append(message)
|
failures.append(message)
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bqv1xjvihivns
|
||||||
@@ -34,7 +34,7 @@ func _test_registry_integrity() -> void:
|
|||||||
SimulationDefinitions.get_action(definition.action_id) == definition,
|
SimulationDefinitions.get_action(definition.action_id) == definition,
|
||||||
"Action lookup should return '%s'" % definition.action_id
|
"Action lookup should return '%s'" % definition.action_id
|
||||||
)
|
)
|
||||||
_check(action_ids.size() == 10, "Registry should contain all ten executable prototype actions")
|
_check(action_ids.size() == 11, "Registry should contain all eleven executable prototype actions")
|
||||||
|
|
||||||
var profession_ids := SimulationDefinitions.get_profession_ids()
|
var profession_ids := SimulationDefinitions.get_profession_ids()
|
||||||
_check(profession_ids.size() == 5, "Registry should contain all five prototype professions")
|
_check(profession_ids.size() == 5, "Registry should contain all five prototype professions")
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ class_name ActiveWorldAdapter
|
|||||||
extends Node
|
extends Node
|
||||||
|
|
||||||
@export var pantry_storage: StorageNode
|
@export var pantry_storage: StorageNode
|
||||||
|
@export var woodpile_storage: StorageNode
|
||||||
|
|
||||||
|
|
||||||
func get_resource_candidates(action_id: StringName) -> Array[Dictionary]:
|
func get_resource_candidates(action_id: StringName) -> Array[Dictionary]:
|
||||||
@@ -38,6 +39,8 @@ func get_activity_target(action_id: StringName, origin: Vector3 = Vector3.ZERO)
|
|||||||
return _get_storage_target(pantry_storage)
|
return _get_storage_target(pantry_storage)
|
||||||
SimulationIds.ACTION_DEPOSIT_FOOD, SimulationIds.ACTION_WITHDRAW_FOOD:
|
SimulationIds.ACTION_DEPOSIT_FOOD, SimulationIds.ACTION_WITHDRAW_FOOD:
|
||||||
return _get_storage_target(pantry_storage)
|
return _get_storage_target(pantry_storage)
|
||||||
|
SimulationIds.ACTION_DEPOSIT_WOOD:
|
||||||
|
return _get_storage_target(woodpile_storage)
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,6 @@
|
|||||||
|
|
||||||
[node name="JajceWorld" parent="." instance=ExtResource("1_world")]
|
[node name="JajceWorld" parent="." instance=ExtResource("1_world")]
|
||||||
|
|
||||||
[node name="DayNightCycle" parent="JajceWorld" index="9"]
|
|
||||||
initial_cycle = 0.38
|
|
||||||
|
|
||||||
[node name="BeautyCamera" type="Camera3D" parent="."]
|
[node name="BeautyCamera" type="Camera3D" parent="."]
|
||||||
position = Vector3(44, 34, 44)
|
position = Vector3(44, 34, 44)
|
||||||
current = true
|
current = true
|
||||||
|
|||||||
+39
-25
@@ -1,4 +1,4 @@
|
|||||||
[gd_scene load_steps=39 format=3]
|
[gd_scene load_steps=41 format=3]
|
||||||
|
|
||||||
[ext_resource type="Terrain3DAssets" path="res://terrain/jajce/assets.tres" id="1_assets"]
|
[ext_resource type="Terrain3DAssets" path="res://terrain/jajce/assets.tres" id="1_assets"]
|
||||||
[ext_resource type="PackedScene" path="res://world/resource_nodes/ResourceNode.tscn" id="2_resource"]
|
[ext_resource type="PackedScene" path="res://world/resource_nodes/ResourceNode.tscn" id="2_resource"]
|
||||||
@@ -16,17 +16,19 @@
|
|||||||
[ext_resource type="PackedScene" path="res://world/storage/StorageNode.tscn" id="14_storage"]
|
[ext_resource type="PackedScene" path="res://world/storage/StorageNode.tscn" id="14_storage"]
|
||||||
[ext_resource type="Script" path="res://world/activity/ActivitySite.gd" id="15_activity"]
|
[ext_resource type="Script" path="res://world/activity/ActivitySite.gd" id="15_activity"]
|
||||||
[ext_resource type="NavigationMesh" path="res://world/jajce/JajceNavigationMesh.tres" id="16_nav"]
|
[ext_resource type="NavigationMesh" path="res://world/jajce/JajceNavigationMesh.tres" id="16_nav"]
|
||||||
|
[ext_resource type="PackedScene" path="res://world/jajce/WindStrokes.tscn" id="19_windstrokes"]
|
||||||
|
[ext_resource type="PackedScene" path="res://world/jajce/WindSpecks.tscn" id="20_windspecks"]
|
||||||
|
|
||||||
[sub_resource type="Terrain3DMaterial" id="Terrain3DMaterial_jajce"]
|
[sub_resource type="Terrain3DMaterial" id="Terrain3DMaterial_jajce"]
|
||||||
_shader_parameters = {
|
_shader_parameters = {
|
||||||
"auto_base_texture": 1,
|
"auto_base_texture": 0,
|
||||||
"auto_overlay_texture": 0,
|
"auto_overlay_texture": 1,
|
||||||
"auto_slope": 0.72,
|
"auto_slope": 0.62,
|
||||||
"blend_sharpness": 0.82,
|
"blend_sharpness": 0.88,
|
||||||
"enable_macro_variation": true,
|
"enable_macro_variation": true,
|
||||||
"macro_variation1": Color(0.9, 0.94, 0.82, 1),
|
"macro_variation1": Color(0.78, 0.9, 0.42, 1),
|
||||||
"macro_variation2": Color(0.76, 0.81, 0.7, 1),
|
"macro_variation2": Color(0.55, 0.75, 0.38, 1),
|
||||||
"macro_variation_slope": 0.38
|
"macro_variation_slope": 0.48
|
||||||
}
|
}
|
||||||
world_background = 0
|
world_background = 0
|
||||||
auto_shader = true
|
auto_shader = true
|
||||||
@@ -96,10 +98,10 @@ material = SubResource("Material_site_canvas")
|
|||||||
size = Vector3(2.9, 0.08, 1.4)
|
size = Vector3(2.9, 0.08, 1.4)
|
||||||
|
|
||||||
[sub_resource type="ProceduralSkyMaterial" id="SkyMaterial_jajce"]
|
[sub_resource type="ProceduralSkyMaterial" id="SkyMaterial_jajce"]
|
||||||
sky_top_color = Color(0.23, 0.43, 0.62, 1)
|
sky_top_color = Color(0.28, 0.52, 0.72, 1)
|
||||||
sky_horizon_color = Color(0.82, 0.68, 0.5, 1)
|
sky_horizon_color = Color(0.88, 0.74, 0.52, 1)
|
||||||
ground_bottom_color = Color(0.12, 0.17, 0.12, 1)
|
ground_bottom_color = Color(0.18, 0.24, 0.14, 1)
|
||||||
ground_horizon_color = Color(0.62, 0.55, 0.42, 1)
|
ground_horizon_color = Color(0.68, 0.6, 0.44, 1)
|
||||||
sun_angle_max = 18.0
|
sun_angle_max = 18.0
|
||||||
|
|
||||||
[sub_resource type="Sky" id="Sky_jajce"]
|
[sub_resource type="Sky" id="Sky_jajce"]
|
||||||
@@ -110,18 +112,18 @@ background_mode = 2
|
|||||||
sky = SubResource("Sky_jajce")
|
sky = SubResource("Sky_jajce")
|
||||||
background_energy_multiplier = 0.9
|
background_energy_multiplier = 0.9
|
||||||
ambient_light_source = 3
|
ambient_light_source = 3
|
||||||
ambient_light_color = Color(0.91, 0.85, 0.72, 1)
|
ambient_light_color = Color(0.95, 0.9, 0.76, 1)
|
||||||
ambient_light_energy = 0.7
|
ambient_light_energy = 0.75
|
||||||
reflected_light_source = 2
|
reflected_light_source = 2
|
||||||
tonemap_mode = 2
|
tonemap_mode = 2
|
||||||
glow_enabled = true
|
glow_enabled = true
|
||||||
glow_normalized = true
|
glow_normalized = true
|
||||||
glow_intensity = 0.34
|
glow_intensity = 0.5
|
||||||
glow_strength = 0.75
|
glow_strength = 0.9
|
||||||
glow_bloom = 0.08
|
glow_bloom = 0.12
|
||||||
fog_enabled = true
|
fog_enabled = true
|
||||||
fog_light_color = Color(0.82, 0.78, 0.66, 1)
|
fog_light_color = Color(0.88, 0.84, 0.7, 1)
|
||||||
fog_light_energy = 0.72
|
fog_light_energy = 0.78
|
||||||
fog_density = 0.002
|
fog_density = 0.002
|
||||||
fog_height = -2.0
|
fog_height = -2.0
|
||||||
fog_height_density = 0.08
|
fog_height_density = 0.08
|
||||||
@@ -129,15 +131,15 @@ fog_aerial_perspective = 0.35
|
|||||||
fog_sky_affect = 0.28
|
fog_sky_affect = 0.28
|
||||||
volumetric_fog_enabled = true
|
volumetric_fog_enabled = true
|
||||||
volumetric_fog_density = 0.006
|
volumetric_fog_density = 0.006
|
||||||
volumetric_fog_albedo = Color(0.9, 0.84, 0.72, 1)
|
volumetric_fog_albedo = Color(0.94, 0.88, 0.75, 1)
|
||||||
volumetric_fog_emission = Color(0.08, 0.065, 0.04, 1)
|
volumetric_fog_emission = Color(0.1, 0.07, 0.04, 1)
|
||||||
volumetric_fog_emission_energy = 0.18
|
volumetric_fog_emission_energy = 0.22
|
||||||
volumetric_fog_length = 96.0
|
volumetric_fog_length = 96.0
|
||||||
volumetric_fog_detail_spread = 1.6
|
volumetric_fog_detail_spread = 1.6
|
||||||
adjustment_enabled = true
|
adjustment_enabled = true
|
||||||
adjustment_brightness = 1.04
|
adjustment_brightness = 1.08
|
||||||
adjustment_contrast = 1.03
|
adjustment_contrast = 1.04
|
||||||
adjustment_saturation = 1.1
|
adjustment_saturation = 1.25
|
||||||
|
|
||||||
[sub_resource type="StandardMaterial3D" id="Material_guard"]
|
[sub_resource type="StandardMaterial3D" id="Material_guard"]
|
||||||
albedo_color = Color(0.55, 0.45, 0.35, 1)
|
albedo_color = Color(0.55, 0.45, 0.35, 1)
|
||||||
@@ -471,6 +473,10 @@ discovery_priority = 0.45
|
|||||||
[node name="VillagePantry" parent="WorldObjects/StorageSites" instance=ExtResource("14_storage")]
|
[node name="VillagePantry" parent="WorldObjects/StorageSites" instance=ExtResource("14_storage")]
|
||||||
position = Vector3(0, 0, -8)
|
position = Vector3(0, 0, -8)
|
||||||
|
|
||||||
|
[node name="VillageWoodpile" parent="WorldObjects/StorageSites" instance=ExtResource("14_storage")]
|
||||||
|
position = Vector3(8, 0, -12)
|
||||||
|
storage_id = &"village_woodpile"
|
||||||
|
|
||||||
[node name="PantrySignPole" type="MeshInstance3D" parent="WorldObjects/StorageSites/VillagePantry"]
|
[node name="PantrySignPole" type="MeshInstance3D" parent="WorldObjects/StorageSites/VillagePantry"]
|
||||||
position = Vector3(-1.05, 1.1, 1.25)
|
position = Vector3(-1.05, 1.1, 1.25)
|
||||||
mesh = SubResource("Mesh_site_pole")
|
mesh = SubResource("Mesh_site_pole")
|
||||||
@@ -601,6 +607,14 @@ light_energy = 1.45
|
|||||||
shadow_enabled = true
|
shadow_enabled = true
|
||||||
directional_shadow_max_distance = 180.0
|
directional_shadow_max_distance = 180.0
|
||||||
|
|
||||||
|
[node name="WindStrokes_Valley" parent="." instance=ExtResource("19_windstrokes")]
|
||||||
|
|
||||||
|
[node name="WindStrokes_Ridge" parent="." instance=ExtResource("19_windstrokes")]
|
||||||
|
|
||||||
|
[node name="WindSpecks_Valley" parent="." instance=ExtResource("20_windspecks")]
|
||||||
|
|
||||||
|
[node name="WindSpecks_Ridge" parent="." instance=ExtResource("20_windspecks")]
|
||||||
|
|
||||||
[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
|
[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
|
||||||
environment = SubResource("Environment_greybox")
|
environment = SubResource("Environment_greybox")
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
[gd_scene load_steps=4 format=3]
|
||||||
|
|
||||||
|
[sub_resource type="ParticleProcessMaterial" id="ParticleProcessMaterial_specks"]
|
||||||
|
lifetime_randomness = 0.8
|
||||||
|
spread = 120.0
|
||||||
|
flatness = 0.5
|
||||||
|
gravity = Vector3(-0.35, 0.25, 0.0)
|
||||||
|
initial_velocity_min = 0.2
|
||||||
|
initial_velocity_max = 1.2
|
||||||
|
scale_min = 0.06
|
||||||
|
scale_max = 0.28
|
||||||
|
color = Color(1, 0.98, 0.9, 0.3)
|
||||||
|
color_ramp = 1
|
||||||
|
emission_shape = 1
|
||||||
|
emission_box_extents = Vector3(32.0, 14.0, 32.0)
|
||||||
|
|
||||||
|
[sub_resource type="StandardMaterial3D" id="Material_speck"]
|
||||||
|
albedo_color = Color(1, 0.98, 0.92, 0.3)
|
||||||
|
transparency = 1
|
||||||
|
shading_mode = 0
|
||||||
|
billboard_mode = 1
|
||||||
|
cull_mode = 0
|
||||||
|
|
||||||
|
[sub_resource type="QuadMesh" id="Mesh_speck"]
|
||||||
|
material = SubResource("Material_speck")
|
||||||
|
size = Vector2(0.12, 0.12)
|
||||||
|
|
||||||
|
[node name="WindSpecks" type="GPUParticles3D"]
|
||||||
|
emitting = true
|
||||||
|
amount = 40
|
||||||
|
lifetime = 6.0
|
||||||
|
one_shot = false
|
||||||
|
preprocess = 4.0
|
||||||
|
speed_scale = 1.0
|
||||||
|
local_coords = false
|
||||||
|
process_material = SubResource("ParticleProcessMaterial_specks")
|
||||||
|
draw_pass_1 = SubResource("Mesh_speck")
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
[gd_scene load_steps=5 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Shader" path="res://world/jajce/materials/wind_stroke.gdshader" id="1_shader"]
|
||||||
|
|
||||||
|
[sub_resource type="ShaderMaterial" id="ShaderMaterial_stroke"]
|
||||||
|
shader = ExtResource("1_shader")
|
||||||
|
|
||||||
|
[sub_resource type="QuadMesh" id="Mesh_stroke"]
|
||||||
|
material = SubResource("ShaderMaterial_stroke")
|
||||||
|
size = Vector2(2.4, 0.14)
|
||||||
|
|
||||||
|
[sub_resource type="ParticleProcessMaterial" id="ParticleProcessMaterial_wind"]
|
||||||
|
lifetime_randomness = 0.6
|
||||||
|
spread = 55.0
|
||||||
|
flatness = 0.7
|
||||||
|
gravity = Vector3(-0.6, 0.35, 0.0)
|
||||||
|
initial_velocity_min = 0.8
|
||||||
|
initial_velocity_max = 2.8
|
||||||
|
angular_velocity_min = -0.25
|
||||||
|
angular_velocity_max = 0.25
|
||||||
|
radial_accel_min = 0.0
|
||||||
|
radial_accel_max = 0.0
|
||||||
|
tangential_accel_min = -0.08
|
||||||
|
tangential_accel_max = 0.08
|
||||||
|
scale_min = 0.3
|
||||||
|
scale_max = 1.2
|
||||||
|
color = Color(1, 1, 1, 0.35)
|
||||||
|
emission_shape = 1
|
||||||
|
emission_box_extents = Vector3(28.0, 8.0, 28.0)
|
||||||
|
|
||||||
|
[node name="WindStrokes" type="GPUParticles3D"]
|
||||||
|
emitting = true
|
||||||
|
amount = 24
|
||||||
|
lifetime = 5.0
|
||||||
|
one_shot = false
|
||||||
|
preprocess = 3.0
|
||||||
|
speed_scale = 1.0
|
||||||
|
local_coords = false
|
||||||
|
process_material = SubResource("ParticleProcessMaterial_wind")
|
||||||
|
draw_pass_1 = SubResource("Mesh_stroke")
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
extends Node
|
extends Node
|
||||||
|
|
||||||
@export var cycle_duration_seconds := 240.0
|
@export var cycle_duration_seconds := 240.0
|
||||||
@export_range(0.0, 1.0, 0.01) var initial_cycle := 0.35
|
@export_range(0.0, 1.0, 0.01) var initial_cycle := 0.25
|
||||||
@export var directional_light: DirectionalLight3D
|
@export var directional_light: DirectionalLight3D
|
||||||
@export var world_environment: WorldEnvironment
|
@export var world_environment: WorldEnvironment
|
||||||
|
|
||||||
@@ -36,19 +36,26 @@ const SUNRISE_LIGHT_COLOR := Color(1, 0.6, 0.45, 1)
|
|||||||
const SUNRISE_SUNSET_ENERGY := 0.8
|
const SUNRISE_SUNSET_ENERGY := 0.8
|
||||||
|
|
||||||
var elapsed := 0.0
|
var elapsed := 0.0
|
||||||
|
var _simulation_node: Node
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
_simulation_node = get_tree().get_first_node_in_group("simulation_manager")
|
||||||
|
if _simulation_node == null:
|
||||||
elapsed = cycle_duration_seconds * initial_cycle
|
elapsed = cycle_duration_seconds * initial_cycle
|
||||||
_apply_light_rotation(initial_cycle)
|
_apply_light_rotation(initial_cycle)
|
||||||
_apply_environment(initial_cycle)
|
_apply_environment(initial_cycle)
|
||||||
|
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
func _process(_delta: float) -> void:
|
||||||
elapsed += delta
|
var cycle: float
|
||||||
|
if _simulation_node != null and "clock" in _simulation_node:
|
||||||
|
cycle = _simulation_node.clock.time_of_day()
|
||||||
|
else:
|
||||||
|
elapsed += _delta
|
||||||
if elapsed >= cycle_duration_seconds:
|
if elapsed >= cycle_duration_seconds:
|
||||||
elapsed = fmod(elapsed, cycle_duration_seconds)
|
elapsed = fmod(elapsed, cycle_duration_seconds)
|
||||||
var cycle := elapsed / cycle_duration_seconds
|
cycle = elapsed / cycle_duration_seconds
|
||||||
_apply_light_rotation(cycle)
|
_apply_light_rotation(cycle)
|
||||||
_apply_environment(cycle)
|
_apply_environment(cycle)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
shader_type spatial;
|
||||||
|
render_mode blend_mix, depth_draw_opaque, cull_disabled, unshaded;
|
||||||
|
|
||||||
|
uniform float softness : hint_range(0.0, 1.0) = 0.45;
|
||||||
|
uniform vec3 tint : source_color = vec3(0.95, 0.93, 0.88);
|
||||||
|
|
||||||
|
void fragment() {
|
||||||
|
float edge_dist = abs(UV.y - 0.5) * 2.0;
|
||||||
|
float alpha = 1.0 - smoothstep(0.0, 1.0, edge_dist);
|
||||||
|
alpha *= smoothstep(0.0, 0.2, UV.x) * smoothstep(0.0, 0.2, 1.0 - UV.x);
|
||||||
|
alpha *= softness * 0.35;
|
||||||
|
ALBEDO = tint;
|
||||||
|
ALPHA = alpha;
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://b8dxberetmchf
|
||||||
+21
-19
@@ -1,6 +1,6 @@
|
|||||||
extends Control
|
extends Control
|
||||||
|
|
||||||
@export var day_night_cycle: Node
|
@export var simulation_manager: Node
|
||||||
|
|
||||||
const SIZE_DIAL := 60.0
|
const SIZE_DIAL := 60.0
|
||||||
const RING_RADIUS := 24.0
|
const RING_RADIUS := 24.0
|
||||||
@@ -14,7 +14,6 @@ const COLOR_DUSK := Color(0.9, 0.35, 0.2, 1)
|
|||||||
const COLOR_BG := Color(0.06, 0.06, 0.08, 0.65)
|
const COLOR_BG := Color(0.06, 0.06, 0.08, 0.65)
|
||||||
|
|
||||||
var _last_fraction := -1.0
|
var _last_fraction := -1.0
|
||||||
var _last_color := Color.TRANSPARENT
|
|
||||||
|
|
||||||
|
|
||||||
func _process(_delta: float) -> void:
|
func _process(_delta: float) -> void:
|
||||||
@@ -44,29 +43,37 @@ func _draw() -> void:
|
|||||||
var tip_pos := center + tip_dir * RING_RADIUS
|
var tip_pos := center + tip_dir * RING_RADIUS
|
||||||
draw_circle(tip_pos, GLOW_RADIUS, time_color.lightened(0.3))
|
draw_circle(tip_pos, GLOW_RADIUS, time_color.lightened(0.3))
|
||||||
|
|
||||||
var label := _phase_label(fraction)
|
|
||||||
var font := ThemeDB.fallback_font
|
var font := ThemeDB.fallback_font
|
||||||
if font == null:
|
if font == null:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
var time_label := _time_label(fraction)
|
||||||
var font_size := 11
|
var font_size := 11
|
||||||
var text_size := font.get_string_size(label, HORIZONTAL_ALIGNMENT_CENTER, -1, font_size)
|
var text_size := font.get_string_size(time_label, HORIZONTAL_ALIGNMENT_CENTER, -1, font_size)
|
||||||
var text_x := (SIZE_DIAL - text_size.x) / 2.0
|
var text_x := (SIZE_DIAL - text_size.x) / 2.0
|
||||||
var text_y := SIZE_DIAL - text_size.y + 4.0
|
var text_y := SIZE_DIAL - text_size.y + 4.0
|
||||||
draw_string(font, Vector2(text_x, text_y), label, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, Color.WHITE)
|
draw_string(font, Vector2(text_x, text_y), time_label, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, Color.WHITE)
|
||||||
|
|
||||||
|
|
||||||
func _cycle_fraction() -> float:
|
func _cycle_fraction() -> float:
|
||||||
if day_night_cycle == null:
|
if simulation_manager != null:
|
||||||
return 0.0
|
if "clock" in simulation_manager:
|
||||||
|
return simulation_manager.clock.time_of_day()
|
||||||
|
var day_night_cycle := get_node_or_null(day_night_cycle_fallback())
|
||||||
|
if day_night_cycle != null:
|
||||||
var elapsed: float = day_night_cycle.get("elapsed") if "elapsed" in day_night_cycle else 0.0
|
var elapsed: float = day_night_cycle.get("elapsed") if "elapsed" in day_night_cycle else 0.0
|
||||||
var duration: float = (
|
var duration: float = (
|
||||||
day_night_cycle.get("cycle_duration_seconds")
|
day_night_cycle.get("cycle_duration_seconds")
|
||||||
if "cycle_duration_seconds" in day_night_cycle
|
if "cycle_duration_seconds" in day_night_cycle
|
||||||
else 240.0
|
else 240.0
|
||||||
)
|
)
|
||||||
if duration <= 0.0:
|
if duration > 0.0:
|
||||||
return 0.0
|
|
||||||
return fmod(elapsed / duration, 1.0)
|
return fmod(elapsed / duration, 1.0)
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
|
||||||
|
func day_night_cycle_fallback() -> NodePath:
|
||||||
|
return NodePath("../../JajceWorld/DayNightCycle")
|
||||||
|
|
||||||
|
|
||||||
func _time_color(fraction: float) -> Color:
|
func _time_color(fraction: float) -> Color:
|
||||||
@@ -81,13 +88,8 @@ func _time_color(fraction: float) -> Color:
|
|||||||
return COLOR_DUSK.lerp(COLOR_NIGHT, smoothstep(0.85, 1.0, fraction))
|
return COLOR_DUSK.lerp(COLOR_NIGHT, smoothstep(0.85, 1.0, fraction))
|
||||||
|
|
||||||
|
|
||||||
func _phase_label(fraction: float) -> String:
|
func _time_label(fraction: float) -> String:
|
||||||
if fraction < 0.1:
|
var total_minutes := int(fraction * 24.0 * 60.0)
|
||||||
return "Night"
|
var hour := total_minutes / 60
|
||||||
if fraction < 0.2:
|
var minute := total_minutes % 60
|
||||||
return "Dawn"
|
return "%02d:%02d" % [hour, minute]
|
||||||
if fraction < 0.65:
|
|
||||||
return "Day"
|
|
||||||
if fraction < 0.8:
|
|
||||||
return "Dusk"
|
|
||||||
return "Night"
|
|
||||||
|
|||||||
+82
-6
@@ -27,6 +27,8 @@ func _ready() -> void:
|
|||||||
push_error("VillageUI: SimulationManager has no npc_decision_recorded signal")
|
push_error("VillageUI: SimulationManager has no npc_decision_recorded signal")
|
||||||
if simulation_manager.has_signal("state_restored"):
|
if simulation_manager.has_signal("state_restored"):
|
||||||
simulation_manager.state_restored.connect(_on_state_restored)
|
simulation_manager.state_restored.connect(_on_state_restored)
|
||||||
|
if simulation_manager.has_signal("speed_changed"):
|
||||||
|
simulation_manager.speed_changed.connect(_on_speed_changed)
|
||||||
|
|
||||||
if "village" in simulation_manager:
|
if "village" in simulation_manager:
|
||||||
_on_village_changed(simulation_manager.village)
|
_on_village_changed(simulation_manager.village)
|
||||||
@@ -52,12 +54,33 @@ func _unhandled_key_input(event: InputEvent) -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _on_village_changed(village: SimVillage) -> void:
|
func _on_village_changed(village: SimVillage) -> void:
|
||||||
|
var name_map := {}
|
||||||
|
for npc in simulation_manager.npcs:
|
||||||
|
name_map[npc.id] = npc.npc_name
|
||||||
|
var event_lines: Array[String] = []
|
||||||
|
for event in simulation_manager.get_recent_events(3):
|
||||||
|
event_lines.append(event.description(name_map))
|
||||||
|
var event_text := "\n".join(event_lines)
|
||||||
|
if event_text.is_empty():
|
||||||
|
event_text = ""
|
||||||
|
else:
|
||||||
|
event_text = "\n" + event_text
|
||||||
|
var speed_text := ""
|
||||||
|
if simulation_manager.has_method("get_current_speed"):
|
||||||
|
speed_text = simulation_manager.get_current_speed()
|
||||||
|
var rates: Dictionary = simulation_manager.get_resource_rates()
|
||||||
|
var food_rate := ""
|
||||||
|
if rates["food_per_day"] > 0.01:
|
||||||
|
food_rate = " (−%.0f/d)" % rates["food_per_day"]
|
||||||
|
var wood_rate := ""
|
||||||
|
if rates["wood_per_day"] > 0.01:
|
||||||
|
wood_rate = " (−%.0f/d)" % rates["wood_per_day"]
|
||||||
village_stats_label.text = (
|
village_stats_label.text = (
|
||||||
"""
|
"""
|
||||||
Village
|
Village [%s]
|
||||||
|
|
||||||
Food: %s
|
Food: %s%s
|
||||||
Wood: %s
|
Wood: %s%s
|
||||||
Safety: %s
|
Safety: %s
|
||||||
Knowledge: %s
|
Knowledge: %s
|
||||||
Starving: %s
|
Starving: %s
|
||||||
@@ -65,16 +88,21 @@ func _on_village_changed(village: SimVillage) -> void:
|
|||||||
Food Mod: %.2f
|
Food Mod: %.2f
|
||||||
Safety Mod: %.2f
|
Safety Mod: %.2f
|
||||||
Knowledge Mod: %.2f
|
Knowledge Mod: %.2f
|
||||||
|
%s
|
||||||
"""
|
"""
|
||||||
% [
|
% [
|
||||||
|
speed_text,
|
||||||
round(village.food),
|
round(village.food),
|
||||||
|
food_rate,
|
||||||
round(village.wood),
|
round(village.wood),
|
||||||
|
wood_rate,
|
||||||
round(village.safety),
|
round(village.safety),
|
||||||
round(village.knowledge),
|
round(village.knowledge),
|
||||||
simulation_manager.get_starving_count(),
|
simulation_manager.get_starving_count(),
|
||||||
village.food_modifier,
|
village.food_modifier,
|
||||||
village.safety_modifier,
|
village.safety_modifier,
|
||||||
village.knowledge_modifier
|
village.knowledge_modifier,
|
||||||
|
event_text
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -88,6 +116,11 @@ func _on_state_restored() -> void:
|
|||||||
_refresh_npc_inspector()
|
_refresh_npc_inspector()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_speed_changed(_multiplier: float) -> void:
|
||||||
|
if simulation_manager != null and "village" in simulation_manager:
|
||||||
|
_on_village_changed(simulation_manager.village)
|
||||||
|
|
||||||
|
|
||||||
func _refresh_npc_inspector() -> void:
|
func _refresh_npc_inspector() -> void:
|
||||||
if not debug_overlay_visible:
|
if not debug_overlay_visible:
|
||||||
return
|
return
|
||||||
@@ -118,13 +151,17 @@ func _refresh_npc_inspector() -> void:
|
|||||||
)
|
)
|
||||||
if not score_rows.is_empty():
|
if not score_rows.is_empty():
|
||||||
score_text = "\n\nUtility\n" + "\n".join(score_rows)
|
score_text = "\n\nUtility\n" + "\n".join(score_rows)
|
||||||
|
var event_text := _build_event_history(npc)
|
||||||
|
var housemate_text := _build_housemate_display(npc)
|
||||||
npc_inspector_label.text = (
|
npc_inspector_label.text = (
|
||||||
"%s · %s\n"
|
"%s · %s\n"
|
||||||
+ "Task: %s (%s)\n"
|
+ "Task: %s (%s)\n"
|
||||||
+ "Destination: %s\n"
|
+ "Destination: %s\n"
|
||||||
+ "Hunger: %.0f Energy: %.0f\n"
|
+ "Hunger: %.0f Energy: %.0f\n"
|
||||||
+ "Carrying food: %.0f\n\n"
|
+ "Carrying food: %.0f wood: %.0f\n"
|
||||||
|
+ "%s\n"
|
||||||
+ "Why\n%s%s\n\n"
|
+ "Why\n%s%s\n\n"
|
||||||
|
+ "Recent\n%s\n\n"
|
||||||
+ "[Tab] Next villager [F10] Cinematic/debug [F12] Demo reset"
|
+ "[Tab] Next villager [F10] Cinematic/debug [F12] Demo reset"
|
||||||
) % [
|
) % [
|
||||||
npc.npc_name,
|
npc.npc_name,
|
||||||
@@ -135,8 +172,11 @@ func _refresh_npc_inspector() -> void:
|
|||||||
npc.hunger,
|
npc.hunger,
|
||||||
npc.energy,
|
npc.energy,
|
||||||
npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD),
|
npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD),
|
||||||
|
npc.get_inventory_amount(SimulationIds.RESOURCE_WOOD),
|
||||||
|
housemate_text,
|
||||||
reason_text,
|
reason_text,
|
||||||
score_text
|
score_text,
|
||||||
|
event_text
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -156,3 +196,39 @@ func set_debug_overlay_visible(is_visible: bool) -> void:
|
|||||||
visible = is_visible
|
visible = is_visible
|
||||||
if is_visible:
|
if is_visible:
|
||||||
_refresh_npc_inspector()
|
_refresh_npc_inspector()
|
||||||
|
|
||||||
|
|
||||||
|
func _build_event_history(npc: SimNPC) -> String:
|
||||||
|
if not simulation_manager.has_method("get_npc_events"):
|
||||||
|
return ""
|
||||||
|
var events: Array = simulation_manager.get_npc_events(npc.id, 5)
|
||||||
|
if events.is_empty():
|
||||||
|
return "No recorded events yet"
|
||||||
|
var name_map := {}
|
||||||
|
for other_npc in simulation_manager.npcs:
|
||||||
|
name_map[other_npc.id] = other_npc.npc_name
|
||||||
|
var lines: Array[String] = []
|
||||||
|
for event in events:
|
||||||
|
lines.append(event.description(name_map))
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
func _build_housemate_display(npc: SimNPC) -> String:
|
||||||
|
if npc.familiarity.is_empty():
|
||||||
|
return ""
|
||||||
|
var highest_id: int = -1
|
||||||
|
var highest_score := -1.0
|
||||||
|
for key in npc.familiarity:
|
||||||
|
var other_id: int = int(key)
|
||||||
|
var score: float = float(npc.familiarity[key])
|
||||||
|
if score > highest_score:
|
||||||
|
highest_score = score
|
||||||
|
highest_id = other_id
|
||||||
|
if highest_id < 0:
|
||||||
|
return ""
|
||||||
|
var other_name := "Someone"
|
||||||
|
for other_npc in simulation_manager.npcs:
|
||||||
|
if other_npc.id == highest_id:
|
||||||
|
other_name = other_npc.npc_name
|
||||||
|
break
|
||||||
|
return "Familiar: %s (%.0f%%)" % [other_name, highest_score * 100.0]
|
||||||
|
|||||||
@@ -161,15 +161,15 @@ func _on_npc_died(npc: SimNPC) -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _on_npc_inventory_changed(npc: SimNPC, item_id: StringName, amount: float) -> void:
|
func _on_npc_inventory_changed(npc: SimNPC, item_id: StringName, amount: float) -> void:
|
||||||
if item_id != SimulationIds.RESOURCE_FOOD:
|
|
||||||
return
|
|
||||||
var visual = active_npc_visuals.get(npc.id)
|
var visual = active_npc_visuals.get(npc.id)
|
||||||
if visual == null:
|
if visual == null:
|
||||||
return
|
return
|
||||||
|
if item_id == SimulationIds.RESOURCE_FOOD:
|
||||||
if visual.has_method("set_carried_food_visible"):
|
if visual.has_method("set_carried_food_visible"):
|
||||||
visual.set_carried_food_visible(amount > 0.0)
|
visual.set_carried_food_visible(amount > 0.0)
|
||||||
else:
|
elif item_id == SimulationIds.RESOURCE_WOOD:
|
||||||
push_error("WorldViewManager: NPCVisual has no set_carried_food_visible method")
|
if visual.has_method("set_carried_wood_visible"):
|
||||||
|
visual.set_carried_wood_visible(amount > 0.0)
|
||||||
|
|
||||||
|
|
||||||
func _on_npc_task_changed(npc: SimNPC, _old_task: StringName, _new_task: StringName) -> void:
|
func _on_npc_task_changed(npc: SimNPC, _old_task: StringName, _new_task: StringName) -> void:
|
||||||
|
|||||||
Reference in New Issue
Block a user