Compare commits

...

7 Commits

24 changed files with 882 additions and 29 deletions
+22 -3
View File
@@ -623,12 +623,31 @@ Completed:
6. `JajceWorld` runtime integration with bindings and regression coverage. 6. `JajceWorld` runtime integration with bindings and regression coverage.
7. Bounded Jajce beauty baseline with six terrain layers, water, atmosphere, 7. Bounded Jajce beauty baseline with six terrain layers, water, atmosphere,
foliage motion, building blockouts, and smoke. foliage motion, building blockouts, and smoke.
8. Base Terrain3D collision/navigation guardrails: runtime tests now keep
Terrain3D collision enabled, keep the old greybox ground out of physics, and
verify required navigation paths track the authored height field.
9. Terrain3D-derived playable-loop navigation: `JajceWorld` now uses an
external baked navigation resource generated from Terrain3D source geometry,
and runtime tests reject partial paths that do not reach their targets.
10. `Jajce Lookdev 01` capture and review: a reproducible capture tool writes
the daytime lookdev baseline to `docs/baselines/jajce_lookdev_01.png`, and
the review notes the current strengths and visual follow-ups.
11. `Simulation Garden 01` capture and review: a reproducible runtime capture
tool writes debug and cinematic `main.tscn` baselines to `docs/baselines/`
and verifies live NPC visuals in the terrain-backed village.
12. Runtime task glyphs: active NPCs now keep compact task-intent markers in
cinematic mode while debug names, labels, and inspector UI remain optional.
13. Runtime first-read staging: the capture tool uses an explicit presentation
camera preset, the gameplay camera supports the same staging API, and the
village has authored dirt path strips for the current task loop.
14. Landmark and work-site silhouettes: the ridge landmark, pantry, guard post,
study desk, and rest bench now have compact presentation props that remain
readable in the runtime cinematic frame.
Next: Next:
1. Capture and visually review “Jajce Lookdev 01.” 1. Strengthen water and foreground silhouettes without adding new simulation
2. Add the cinematic/debug toggle and repeatable demo reset. mechanics.
3. Capture “Simulation Garden 01.”
Do not start with GIS data, a full city, a large asset pack, or more NPC Do not start with GIS data, a full city, a large asset pack, or more NPC
mechanics. The next proof is a beautiful stage for the systems that already mechanics. The next proof is a beautiful stage for the systems that already
+19
View File
@@ -670,6 +670,25 @@ Recently completed:
- Activity-site capacity enforcement for rest, study, and patrol target - Activity-site capacity enforcement for rest, study, and patrol target
resolution, derived from NPC target claims rather than presentation-only resolution, derived from NPC target claims rather than presentation-only
counters. counters.
- Terrain3D collision/navigation hardening began: scaffold and runtime tests
now assert Terrain3D collision remains enabled, the hidden greybox ground no
longer provides physics collision, and tested navigation routes stay close to
the authored Terrain3D height field.
- The temporary greybox navigation source was replaced by a project-owned
Terrain3D-derived navigation mesh resource generated by
`tools/bake_jajce_navigation.gd`; route tests now also reject partial paths
that do not reach their requested targets.
- `Jajce Lookdev 01` is captured and reviewed with a project-owned capture
script and a daytime baseline image under `docs/baselines/`.
- `Simulation Garden 01` is captured and reviewed from `main.tscn` with paired
debug/cinematic baselines that verify live NPC visuals in the Terrain3D-backed
village.
- Runtime task glyphs preserve active NPC task intent in cinematic mode while
leaving the simulation task state authoritative.
- Runtime first-read staging now includes a presentation camera preset and
authored path strips that reveal the active village task loop.
- Landmark and work-site silhouette props make the ridge landmark, pantry,
guard, study, and rest sites easier to identify without debug labels.
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.
+34 -9
View File
@@ -193,7 +193,14 @@ study, and patrol target typed activity sites. The migration is documented in
- **Main scene:** `res://main.tscn` - **Main scene:** `res://main.tscn`
- **Terrain:** Terrain3D 1.0.2 is installed and enabled - **Terrain:** Terrain3D 1.0.2 is installed and enabled
- **Jajce runtime:** reusable 512 m Terrain3D seed, greybox landmarks, stable - **Jajce runtime:** reusable 512 m Terrain3D seed, greybox landmarks, stable
ResourceNode placement, lookdev camera, and tested runtime navigation ResourceNode placement, lookdev camera, and Terrain3D-derived runtime
navigation with collision guardrails
- **Lookdev baseline:** `Jajce Lookdev 01` is captured at
`docs/baselines/jajce_lookdev_01.png` with
`tools/capture_jajce_lookdev.gd`
- **Runtime baseline:** `Simulation Garden 01` is captured as paired debug and
cinematic `main.tscn` images under `docs/baselines/` with
`tools/capture_simulation_garden.gd`
- **Terrain compatibility floor:** Godot 4.4 according to the bundled extension - **Terrain compatibility floor:** Godot 4.4 according to the bundled extension
- **Version control:** Git - **Version control:** Git
- **Primary branch:** `main` - **Primary branch:** `main`
@@ -211,8 +218,9 @@ plugin content, not game architecture.
- The player is a `CharacterBody3D` represented by placeholder primitive - The player is a `CharacterBody3D` represented by placeholder primitive
geometry. geometry.
- WASD movement is camera-relative. - WASD movement is camera-relative.
- The elevated third-person camera rotates with the mouse and uses smoothed - The elevated third-person camera rotates with the mouse, uses smoothed
follow/focus behavior. follow/focus behavior, and exposes an opt-in presentation preset for
reproducible runtime captures.
- Pressing `E` near a berry bush or tree extracts its configured yield into the - Pressing `E` near a berry bush or tree extracts its configured yield into the
village through the same `ResourceNode` contract used by NPCs. village through the same `ResourceNode` contract used by NPCs.
- Guard, study, rest, and food interactions now use typed world sites. - Guard, study, rest, and food interactions now use typed world sites.
@@ -324,11 +332,20 @@ distance with resource `safety_risk`, `comfort_distance`, and
- a centered 512 m Terrain3D landscape split across four regions; - a centered 512 m Terrain3D landscape split across four regions;
- deterministic shaped terrain data and six game-owned surface layers; - deterministic shaped terrain data and six game-owned surface layers;
- fortress, houses, mill, bridge, shader river/waterfall, mist, and smoke; - fortress, houses, mill, bridge, shader river/waterfall, mist, and smoke;
- authored dirt path strips that make the current village task loop readable;
- compact silhouette props for the ridge landmark, pantry, guard, study, and
rest sites;
- warm sky, fog, shadows, and wind-reactive foliage; - warm sky, fog, shadows, and wind-reactive foliage;
- eight ResourceNodes preserving stable food/wood discovery IDs; - eight ResourceNodes preserving stable food/wood discovery IDs;
- typed village pantry storage and typed guard, study, and rest activity sites; - typed village pantry storage and typed guard, study, and rest activity sites;
- a temporary baked navigation loop with 24 tested routes; - a Terrain3D-derived baked navigation mesh covering the current playable loop;
- a look-development scene and camera. - base collision/navigation guardrails that keep Terrain3D collision enabled,
keep the old greybox ground out of runtime physics, require paths to reach
their targets, and compare required navigation paths against the Terrain3D
height field;
- a look-development scene and camera;
- reproducible lookdev and runtime capture scripts plus review notes under
`docs/baselines/`.
This is a runtime integration proof, not the final terrain composition. This is a runtime integration proof, not the final terrain composition.
Terrain3D startup requires several physics frames before reliable Terrain3D startup requires several physics frames before reliable
@@ -348,8 +365,10 @@ A small village panel displays:
- a Tab-cycled NPC inspector with profession, needs, task state, destination, - a Tab-cycled NPC inspector with profession, needs, task state, destination,
decision reason, and utility scores. decision reason, and utility scores.
NPC name/profession labels, definition-driven colors and props, and the NPC name/profession labels, definition-driven colors and props, carried-food
carried-food visual make active simulation state readable in the world. visuals, and compact task glyphs make active simulation state readable in the
world. The F10 cinematic mode hides debug labels while preserving the task
glyphs.
## Runtime architecture ## Runtime architecture
@@ -534,8 +553,13 @@ These are expected prototype constraints, not necessarily isolated bugs:
mutation APIs, but selection, execution, target resolution, and active-world mutation APIs, but selection, execution, target resolution, and active-world
queries now have focused collaborators. queries now have focused collaborators.
- Path failure and interruption emit a `navigation_failed` signal and send the NPC to wander; this is functional but not yet polished. - Path failure and interruption emit a `navigation_failed` signal and send the NPC to wander; this is functional but not yet polished.
- The old greybox navigation source has been replaced by a project-owned
Terrain3D-derived navigation resource. The current bake is still a first
playable-loop pass, so future terrain sculpting should rerun the bake tool and
recheck reachability rather than hand-editing navigation polygons.
- Terrain3D and the bounded Jajce beauty baseline now run in the main game - Terrain3D and the bounded Jajce beauty baseline now run in the main game
scene; final visual review and character/profession readability remain. scene; `Jajce Lookdev 01` and the runtime `Simulation Garden 01` are captured
as reproducible presentation baselines.
- Combat, companions, factions, politics, trade, rumours, quests, persistence, - Combat, companions, factions, politics, trade, rumours, quests, persistence,
and regional travel do not yet exist. and regional travel do not yet exist.
- Placeholder geometry is sufficient for debugging but not for build-in-public - Placeholder geometry is sufficient for debugging but not for build-in-public
@@ -753,7 +777,8 @@ eating consumes it. Each successful transfer creates a persisted structured
event, and active NPCs visibly carry food through presentation derived from event, and active NPCs visibly carry food through presentation derived from
their inventory. The bounded save-slot slice is also complete, including their inventory. The bounded save-slot slice is also complete, including
validation, atomic replacement recovery, and active-visual rebuilding. validation, atomic replacement recovery, and active-visual rebuilding.
Terrain3D runtime integration and the simulation-garden beauty pass are next. Terrain3D runtime integration, the bounded Jajce beauty pass, and the first
simulation-garden runtime capture are complete.
The coherent visual slice can then aim for: The coherent visual slice can then aim for:
+46
View File
@@ -0,0 +1,46 @@
# Jajce Lookdev 01
- **Captured:** 2026-07-08
- **Scene:** `res://world/jajce/JajceLookdev.tscn`
- **Image:** [`jajce_lookdev_01.png`](jajce_lookdev_01.png)
- **Capture tool:** `res://tools/capture_jajce_lookdev.gd`
## Contract
This checkpoint captures the first readable Jajce visual stage after the
Terrain3D-derived navigation pass. It is a visual composition baseline, not a
gameplay regression test.
The image should show:
- the broad shaped valley terrain;
- the village and fortress blockouts;
- water, waterfall/foam, and bridge silhouettes;
- visible foliage clusters;
- warm daylight/fog treatment;
- stable enough framing to compare future terrain, lighting, and asset passes.
## Review
The scene now reads as a coherent daytime valley rather than the previous dark
startup view. The terrain, homes, bridge, water, waterfall, smoke/mist, and
first foliage clusters are all visible in one frame.
Known follow-up work:
- reduce the heavy haze if it hides terrain color and landmark silhouettes;
- improve fortress/waterfall readability from the beauty-camera angle;
- replace blockout architecture and placeholder resource markers gradually;
- capture a runtime `Simulation Garden 01` view with active NPC behavior.
## Command
Run with a real display driver, not `--headless`; headless Godot uses the dummy
renderer on this machine and cannot read back a viewport image.
```powershell
$env:APPDATA = "$PWD\logs\quality\godot_profile"
$env:LOCALAPPDATA = "$PWD\logs\quality\godot_profile"
& "C:\Users\Rijad\Downloads\tools\Godot_v4.7-stable_win64.exe\Godot_v4.7-stable_win64_console.exe" `
--path "$PWD" --script res://tools/capture_jajce_lookdev.gd
```
+52
View File
@@ -0,0 +1,52 @@
# Simulation Garden 01
Captured: 2026-07-08
## Files
- `docs/baselines/simulation_garden_01_debug.png`
- `docs/baselines/simulation_garden_01_cinematic.png`
- `tools/capture_simulation_garden.gd`
## Capture Command
Run from the project root with the normal renderer:
```powershell
$env:APPDATA = 'C:\Users\Rijad\Documents\Simulation Game\logs\quality\godot_profile'
$env:LOCALAPPDATA = 'C:\Users\Rijad\Documents\Simulation Game\logs\quality\godot_profile'
& 'C:\Users\Rijad\Downloads\tools\Godot_v4.7-stable_win64.exe\Godot_v4.7-stable_win64_console.exe' --path 'C:\Users\Rijad\Documents\Simulation Game' --script res://tools/capture_simulation_garden.gd
```
The script warms up `main.tscn`, verifies that the simulation and active NPC
visuals exist, captures the F10 debug presentation, then captures the cinematic
presentation with development labels and UI hidden.
## Review
The milestone is valid as a runtime integration checkpoint: the same Jajce
Terrain3D world is running in `main.tscn`, NPCs are alive in the terrain-backed
village, the selected-NPC reason inspector is fed by real task scoring, and the
cinematic/debug toggle produces a shareable view without changing simulation
state. A follow-up pass adds small task glyphs above active NPCs, so the
cinematic view now preserves task intent after the debug labels are hidden. A
camera-staging pass then gives the runtime capture a wider village view and adds
authored dirt path strips that make the task loop readable from the first frame.
The next presentation pass adds compact silhouette props to the ridge landmark,
pantry, guard, study, and rest sites so work locations remain identifiable in
cinematic mode.
The frame also makes the next presentation risks plain:
- the camera still favors verification over composition;
- placeholder characters, houses, and work props remain useful but visibly
prototype-grade;
- terrain texture variation and path strips read better than the old flat map,
but landmarks, water, and foreground silhouettes need stronger first-read
staging;
- task glyphs and work-site props help, but the cinematic view still needs
richer water/foreground shapes and stronger authored silhouettes before it
becomes a strong public-facing shot.
Use this baseline as the first runtime comparison image before adding more
systems or expanding the terrain.
Binary file not shown.

After

Width:  |  Height:  |  Size: 732 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 710 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 KiB

+1
View File
@@ -53,6 +53,7 @@ target = NodePath("../Player")
follow_speed = 8.0 follow_speed = 8.0
deadzone_right = 2.0 deadzone_right = 2.0
deadzone_forward = 2.0 deadzone_forward = 2.0
max_zoom_offset = Vector3(0, 20, 15)
[node name="Camera3D" type="Camera3D" parent="CameraRig" unique_id=1992528776] [node name="Camera3D" type="Camera3D" parent="CameraRig" unique_id=1992528776]
current = true current = true
+19 -2
View File
@@ -6,6 +6,7 @@ extends Node3D
@export var follow_speed := 12.0 @export var follow_speed := 12.0
@export var rotation_smooth_speed := 18.0 @export var rotation_smooth_speed := 18.0
@export var focus_smooth_speed := 14.0 @export var focus_smooth_speed := 14.0
@export var pitch_degrees := -55.0
@export var mouse_sensitivity := 0.002 @export var mouse_sensitivity := 0.002
@@ -32,7 +33,7 @@ func _ready() -> void:
if target: if target:
focus_position = target.global_position focus_position = target.global_position
desired_focus_position = focus_position desired_focus_position = focus_position
global_position = focus_position + min_zoom_offset.lerp(max_zoom_offset, zoom_smooth) _apply_camera_transform()
func _unhandled_input(event: InputEvent) -> void: func _unhandled_input(event: InputEvent) -> void:
@@ -59,7 +60,7 @@ func _physics_process(delta: float) -> void:
smoothed_yaw = lerp_angle(smoothed_yaw, yaw, rot_t) smoothed_yaw = lerp_angle(smoothed_yaw, yaw, rot_t)
rotation = Vector3(deg_to_rad(-55), smoothed_yaw, 0) rotation = Vector3(deg_to_rad(pitch_degrees), smoothed_yaw, 0)
var target_pos := target.global_position var target_pos := target.global_position
var diff := target_pos - desired_focus_position var diff := target_pos - desired_focus_position
@@ -89,3 +90,19 @@ func _physics_process(delta: float) -> void:
var desired_position := focus_position + rotated_offset var desired_position := focus_position + rotated_offset
global_position = global_position.lerp(desired_position, follow_t) global_position = global_position.lerp(desired_position, follow_t)
func apply_presentation_preset(focus: Vector3, yaw_degrees: float, zoom_value: float) -> void:
yaw = deg_to_rad(yaw_degrees)
smoothed_yaw = yaw
zoom = clampf(zoom_value, 0.0, 1.0)
zoom_smooth = zoom
focus_position = focus
desired_focus_position = focus
_apply_camera_transform()
func _apply_camera_transform() -> void:
rotation = Vector3(deg_to_rad(pitch_degrees), smoothed_yaw, 0)
var active_offset := min_zoom_offset.lerp(max_zoom_offset, zoom_smooth)
global_position = focus_position + active_offset.rotated(Vector3.UP, smoothed_yaw)
+90
View File
@@ -16,6 +16,8 @@ const DEBUG_LOGS := true
@onready var profession_prop: MeshInstance3D = $ProfessionProp @onready var profession_prop: MeshInstance3D = $ProfessionProp
@onready var profession_label: Label3D = $ProfessionLabel @onready var profession_label: Label3D = $ProfessionLabel
@onready var carried_food_visual: MeshInstance3D = $CarriedFood @onready var carried_food_visual: MeshInstance3D = $CarriedFood
@onready var task_glyph_root: Node3D = $TaskGlyphRoot
@onready var task_glyph: MeshInstance3D = $TaskGlyphRoot/TaskGlyph
var has_reported_arrival := false var has_reported_arrival := false
var sim_id: int = -1 var sim_id: int = -1
@@ -51,6 +53,7 @@ func setup_from_sim(npc: SimNPC) -> void:
name = "NPC_%s_%s" % [sim_id, npc_name] name = "NPC_%s_%s" % [sim_id, npc_name]
_apply_profession_presentation() _apply_profession_presentation()
set_carried_food_visible(npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD) > 0.0) set_carried_food_visible(npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD) > 0.0)
set_task_presentation(npc.current_task, npc.task_state)
func _apply_profession_presentation() -> void: func _apply_profession_presentation() -> void:
@@ -114,6 +117,18 @@ func set_carried_food_visible(is_visible: bool) -> void:
carried_food_visual.visible = is_visible and not is_dead_visual carried_food_visual.visible = is_visible and not is_dead_visual
func set_task_presentation(action_id: StringName, task_state: StringName) -> void:
if task_glyph_root == null or task_glyph == null:
return
if is_dead_visual or not _should_show_task_glyph(action_id, task_state):
task_glyph_root.visible = false
return
task_glyph.mesh = _create_task_glyph_mesh(action_id)
task_glyph.material_override = _create_task_glyph_material(action_id)
task_glyph_root.visible = task_glyph.mesh != null
func set_debug_overlay_visible(is_visible: bool) -> void: func set_debug_overlay_visible(is_visible: bool) -> void:
debug_overlay_visible = is_visible debug_overlay_visible = is_visible
if profession_label != null: if profession_label != null:
@@ -251,6 +266,7 @@ func apply_dead_visual_state() -> void:
carried_food_visual.visible = false carried_food_visual.visible = false
profession_prop.visible = false profession_prop.visible = false
profession_label.visible = false profession_label.visible = false
task_glyph_root.visible = false
path_request_id += 1 path_request_id += 1
velocity = Vector3.ZERO velocity = Vector3.ZERO
current_path = PackedVector3Array() current_path = PackedVector3Array()
@@ -269,3 +285,77 @@ func apply_dead_visual_state() -> void:
collision_layer = 0 collision_layer = 0
collision_mask = 0 collision_mask = 0
debug_log("Applied dead visual state") debug_log("Applied dead visual state")
func _should_show_task_glyph(action_id: StringName, task_state: StringName) -> bool:
if task_state not in [SimNPC.TASK_STATE_TRAVELING, SimNPC.TASK_STATE_WORKING]:
return false
return action_id not in [
SimulationIds.ACTION_IDLE,
SimulationIds.ACTION_DEAD,
SimulationIds.ACTION_WANDER,
]
func _create_task_glyph_mesh(action_id: StringName) -> PrimitiveMesh:
task_glyph.rotation_degrees = Vector3.ZERO
match action_id:
SimulationIds.ACTION_GATHER_FOOD, SimulationIds.ACTION_EAT, SimulationIds.ACTION_WITHDRAW_FOOD:
var food := SphereMesh.new()
food.radius = 0.18
food.height = 0.26
return food
SimulationIds.ACTION_GATHER_WOOD:
var wood := BoxMesh.new()
wood.size = Vector3(0.18, 0.42, 0.18)
task_glyph.rotation_degrees = Vector3(0, 0, -18)
return wood
SimulationIds.ACTION_DEPOSIT_FOOD:
var crate := BoxMesh.new()
crate.size = Vector3(0.34, 0.22, 0.34)
return crate
SimulationIds.ACTION_PATROL:
var shield := CylinderMesh.new()
shield.top_radius = 0.2
shield.bottom_radius = 0.2
shield.height = 0.1
task_glyph.rotation_degrees = Vector3(90, 0, 0)
return shield
SimulationIds.ACTION_STUDY:
var book := BoxMesh.new()
book.size = Vector3(0.36, 0.08, 0.24)
return book
SimulationIds.ACTION_REST:
var rest := SphereMesh.new()
rest.radius = 0.2
rest.height = 0.12
return rest
return null
func _create_task_glyph_material(action_id: StringName) -> StandardMaterial3D:
var material := StandardMaterial3D.new()
material.roughness = 0.65
material.emission_enabled = true
material.emission_energy_multiplier = 0.35
var color := _get_task_glyph_color(action_id)
material.albedo_color = color
material.emission = color
return material
func _get_task_glyph_color(action_id: StringName) -> Color:
match action_id:
SimulationIds.ACTION_GATHER_FOOD, SimulationIds.ACTION_EAT, SimulationIds.ACTION_WITHDRAW_FOOD:
return Color(0.95, 0.42, 0.25, 1.0)
SimulationIds.ACTION_GATHER_WOOD:
return Color(0.58, 0.34, 0.16, 1.0)
SimulationIds.ACTION_DEPOSIT_FOOD:
return Color(0.95, 0.72, 0.32, 1.0)
SimulationIds.ACTION_PATROL:
return Color(0.36, 0.58, 0.95, 1.0)
SimulationIds.ACTION_STUDY:
return Color(0.62, 0.46, 0.9, 1.0)
SimulationIds.ACTION_REST:
return Color(0.4, 0.78, 0.72, 1.0)
return Color(0.9, 0.9, 0.85, 1.0)
+6
View File
@@ -55,6 +55,12 @@ visible = false
transform = Transform3D(0.75, 0, 0, 0, 0.75, 0, 0, 0, 0.75, 0.48, 0.7, 0) transform = Transform3D(0.75, 0, 0, 0, 0.75, 0, 0, 0, 0.75, 0.48, 0.7, 0)
mesh = SubResource("SphereMesh_food") mesh = SubResource("SphereMesh_food")
[node name="TaskGlyphRoot" type="Node3D" parent="."]
visible = false
position = Vector3(0, 2.25, 0)
[node name="TaskGlyph" type="MeshInstance3D" parent="TaskGlyphRoot"]
[node name="NavigationAgent3D" type="NavigationAgent3D" parent="."] [node name="NavigationAgent3D" type="NavigationAgent3D" parent="."]
path_desired_distance = 0.3 path_desired_distance = 0.3
target_desired_distance = 0.5 target_desired_distance = 0.5
+1
View File
@@ -217,6 +217,7 @@ func simulate_tick() -> void:
npc.current_task = SimulationIds.ACTION_IDLE npc.current_task = SimulationIds.ACTION_IDLE
npc.has_travel_target = false npc.has_travel_target = false
npc.target_id = &"" npc.target_id = &""
npc_task_changed.emit(npc, completed_task, npc.current_task)
if debug_logs: if debug_logs:
print( print(
+85
View File
@@ -11,6 +11,7 @@ func _run() -> void:
var main_scene: Node = load("res://main.tscn").instantiate() var main_scene: Node = load("res://main.tscn").instantiate()
root.add_child(main_scene) root.add_child(main_scene)
await process_frame await process_frame
for _frame in 10:
await physics_frame await physics_frame
var simulation_manager: Node = main_scene.get_node("SimulationManager") var simulation_manager: Node = main_scene.get_node("SimulationManager")
@@ -26,6 +27,22 @@ func _run() -> void:
terrain.get_camera() == main_scene.get_node("CameraRig/Camera3D"), terrain.get_camera() == main_scene.get_node("CameraRig/Camera3D"),
"Runtime Terrain3D should bind to the gameplay camera" "Runtime Terrain3D should bind to the gameplay camera"
) )
var camera_rig := main_scene.get_node("CameraRig")
_check(
camera_rig.has_method("apply_presentation_preset"),
"Runtime camera should expose a presentation staging preset"
)
var presentation_focus := Vector3(-2.0, 1.2, -0.5)
camera_rig.apply_presentation_preset(presentation_focus, 128.0, 0.95)
_check(
camera_rig.global_position.distance_to(presentation_focus) > 10.0,
"Presentation preset should pull the camera back from the village focus"
)
_check(terrain.collision_mode != 0, "Runtime Terrain3D collision should be enabled")
_check(
not main_scene.has_node("JajceWorld/NavigationRegion3D/GreyboxGround"),
"Runtime should not keep the temporary greybox navigation ground"
)
_check( _check(
( (
not main_scene.has_node("World") not main_scene.has_node("World")
@@ -41,8 +58,39 @@ func _run() -> void:
simulation_manager.resource_states.size() == 12, simulation_manager.resource_states.size() == 12,
"Simulation authority should bind all twelve Jajce resources" "Simulation authority should bind all twelve Jajce resources"
) )
var village_root := main_scene.get_node("JajceWorld/VillageRoot")
var path_strips := 0
for child in village_root.get_children():
if child.name.begins_with("Path_"):
path_strips += 1
_check(path_strips >= 4, "Runtime should include authored path strips for first-read composition")
_check(
main_scene.has_node("JajceWorld/FortressBlockout/Keep/RidgeBanner"),
"Runtime should include a readable ridge landmark banner"
)
_check(
main_scene.has_node("JajceWorld/WorldObjects/StorageSites/VillagePantry/PantrySign"),
"Pantry storage should have a readable work-site silhouette"
)
_check(
main_scene.has_node("JajceWorld/WorldObjects/ActivitySites/GuardPost/GuardPennant"),
"Guard site should have a readable work-site silhouette"
)
_check(
main_scene.has_node("JajceWorld/WorldObjects/ActivitySites/StudyDesk/OpenBook_A"),
"Study site should have readable study props"
)
_check(
main_scene.has_node("JajceWorld/WorldObjects/ActivitySites/RestBench/RestCanopy"),
"Rest site should have a readable rest silhouette"
)
var navigation_map: RID = main_scene.get_world_3d().navigation_map var navigation_map: RID = main_scene.get_world_3d().navigation_map
var navigation_region := main_scene.get_node("JajceWorld/NavigationRegion3D") as NavigationRegion3D
_check(
navigation_region.navigation_mesh.resource_path == "res://world/jajce/JajceNavigationMesh.tres",
"Runtime navigation should use the Terrain3D-derived baked mesh resource"
)
await _wait_for_navigation_map(navigation_map) await _wait_for_navigation_map(navigation_map)
NavigationServer3D.map_force_update(navigation_map) NavigationServer3D.map_force_update(navigation_map)
@@ -73,6 +121,12 @@ func _run() -> void:
not path.is_empty(), not path.is_empty(),
"Navigation path should exist from %s to %s" % [origin, destination] "Navigation path should exist from %s to %s" % [origin, destination]
) )
_check_path_tracks_terrain(
terrain, path, "Runtime navigation path should track Terrain3D height"
)
_check_path_reaches_destination(
path, destination, "Runtime navigation path should reach its requested target"
)
var village := SimVillage.new() var village := SimVillage.new()
var worker := SimNPC.new(100, "BaselineWorker", SimulationIds.PROFESSION_SCHOLAR, 5.0, 5.0) var worker := SimNPC.new(100, "BaselineWorker", SimulationIds.PROFESSION_SCHOLAR, 5.0, 5.0)
@@ -126,3 +180,34 @@ func _wait_for_navigation_map(navigation_map: RID) -> void:
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)
func _check_path_tracks_terrain(terrain: Terrain3D, path: PackedVector3Array, message: String) -> void:
if path.is_empty():
return
for point in path:
var terrain_height: float = terrain.data.get_height(point)
if is_nan(terrain_height):
failures.append("%s: missing terrain height at %s" % [message, point])
return
if absf(point.y - terrain_height) > 0.85:
failures.append(
"%s: path point %s is too far from terrain height %.2f"
% [message, point, terrain_height]
)
return
func _check_path_reaches_destination(path: PackedVector3Array, destination: Vector3, message: String) -> void:
if path.is_empty():
return
var final_point := path[path.size() - 1]
var final_xz := Vector2(final_point.x, final_point.z)
var destination_xz := Vector2(destination.x, destination.z)
if final_xz.distance_to(destination_xz) > 1.5:
failures.append(
"%s: final path point %s is too far from %s"
% [message, final_point, destination]
)
+47 -1
View File
@@ -17,7 +17,7 @@ func _run() -> void:
var world: Node3D = load("res://world/jajce/JajceWorld.tscn").instantiate() var world: Node3D = load("res://world/jajce/JajceWorld.tscn").instantiate()
root.add_child(world) root.add_child(world)
await process_frame await process_frame
for frame in 10: for _frame in 10:
await physics_frame await physics_frame
var terrain = world.get_node("TerrainRoot/Terrain3D") var terrain = world.get_node("TerrainRoot/Terrain3D")
@@ -26,6 +26,11 @@ func _run() -> void:
terrain.data_directory == "res://terrain/jajce/data", terrain.data_directory == "res://terrain/jajce/data",
"Terrain3D should use dedicated Jajce data" "Terrain3D should use dedicated Jajce data"
) )
_check(terrain.collision_mode != 0, "Terrain3D collision should be enabled for gameplay")
_check(
not world.has_node("NavigationRegion3D/GreyboxGround"),
"JajceWorld should not keep the temporary greybox navigation ground"
)
_check( _check(
configured_texture_count == 6, configured_texture_count == 6,
( (
@@ -197,6 +202,10 @@ func _run() -> void:
navigation_region.navigation_mesh.get_polygon_count() > 0, navigation_region.navigation_mesh.get_polygon_count() > 0,
"Jajce navigation mesh should contain baked polygons" "Jajce navigation mesh should contain baked polygons"
) )
_check(
navigation_region.navigation_mesh.resource_path == "res://world/jajce/JajceNavigationMesh.tres",
"Jajce navigation should use the Terrain3D-derived baked mesh resource"
)
await _wait_for_navigation_map(navigation_map) await _wait_for_navigation_map(navigation_map)
NavigationServer3D.map_force_update(navigation_map) NavigationServer3D.map_force_update(navigation_map)
@@ -215,6 +224,12 @@ func _run() -> void:
not path.is_empty(), not path.is_empty(),
"Jajce navigation path should exist from %s to %s" % [origin, destination] "Jajce navigation path should exist from %s to %s" % [origin, destination]
) )
_check_path_tracks_terrain(
terrain, path, "Jajce navigation path should track Terrain3D height"
)
_check_path_reaches_destination(
path, destination, "Jajce navigation path should reach its requested target"
)
var adapter := ActiveWorldAdapter.new() var adapter := ActiveWorldAdapter.new()
adapter.pantry_storage = pantry adapter.pantry_storage = pantry
@@ -304,3 +319,34 @@ func _wait_for_navigation_map(navigation_map: RID) -> void:
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)
func _check_path_tracks_terrain(terrain: Terrain3D, path: PackedVector3Array, message: String) -> void:
if path.is_empty():
return
for point in path:
var terrain_height: float = terrain.data.get_height(point)
if is_nan(terrain_height):
failures.append("%s: missing terrain height at %s" % [message, point])
return
if absf(point.y - terrain_height) > 0.85:
failures.append(
"%s: path point %s is too far from terrain height %.2f"
% [message, point, terrain_height]
)
return
func _check_path_reaches_destination(path: PackedVector3Array, destination: Vector3, message: String) -> void:
if path.is_empty():
return
var final_point := path[path.size() - 1]
var final_xz := Vector2(final_point.x, final_point.z)
var destination_xz := Vector2(destination.x, destination.z)
if final_xz.distance_to(destination_xz) > 1.5:
failures.append(
"%s: final path point %s is too far from %s"
% [message, final_point, destination]
)
+18
View File
@@ -32,6 +32,24 @@ func _run() -> void:
and profession_definition.display_name in visual.get_node("ProfessionLabel").text, and profession_definition.display_name in visual.get_node("ProfessionLabel").text,
"NPC visual should expose a profession prop and readable label" "NPC visual should expose a profession prop and readable label"
) )
visual.set_task_presentation(SimulationIds.ACTION_GATHER_FOOD, SimNPC.TASK_STATE_TRAVELING)
_check(
visual.get_node("TaskGlyphRoot").visible
and visual.get_node("TaskGlyphRoot/TaskGlyph").mesh is SphereMesh,
"NPC visual should expose a compact task glyph for cinematic readability"
)
visual.set_debug_overlay_visible(false)
_check(
not visual.get_node("ProfessionLabel").visible
and visual.get_node("TaskGlyphRoot").visible,
"Cinematic mode should hide debug labels while preserving task glyphs"
)
visual.set_debug_overlay_visible(true)
visual.set_task_presentation(SimulationIds.ACTION_WANDER, SimNPC.TASK_STATE_TRAVELING)
_check(
not visual.get_node("TaskGlyphRoot").visible,
"Ambient wander should not display a task glyph"
)
var test_decision := ActionSelectionResult.new( var test_decision := ActionSelectionResult.new(
SimulationIds.ACTION_GATHER_FOOD, SimulationIds.ACTION_GATHER_FOOD,
-1.0, -1.0,
+74
View File
@@ -0,0 +1,74 @@
extends SceneTree
const OUTPUT_PATH := "res://world/jajce/JajceNavigationMesh.tres"
const WORLD_SCENE_PATH := "res://world/jajce/JajceWorld.tscn"
const TERRAIN_PATH := "TerrainRoot/Terrain3D"
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
var world_scene := load(WORLD_SCENE_PATH) as PackedScene
if world_scene == null:
push_error("Could not load %s" % WORLD_SCENE_PATH)
quit(1)
return
var world := world_scene.instantiate() as Node3D
root.add_child(world)
await process_frame
for _frame in 10:
await physics_frame
var terrain := world.get_node(TERRAIN_PATH) as Terrain3D
if terrain == null or terrain.data == null:
push_error("Jajce Terrain3D data is not available")
quit(1)
return
var nav_mesh := _create_navigation_mesh_template()
var bake_aabb: AABB = nav_mesh.filter_baking_aabb
bake_aabb.position += nav_mesh.filter_baking_aabb_offset
var terrain_faces: PackedVector3Array = terrain.generate_nav_mesh_source_geometry(bake_aabb, false)
if terrain_faces.is_empty():
push_error("Terrain3D produced no navigation source faces")
quit(1)
return
var source_geometry := NavigationMeshSourceGeometryData3D.new()
source_geometry.add_faces(terrain_faces, Transform3D.IDENTITY)
NavigationServer3D.bake_from_source_geometry_data(nav_mesh, source_geometry)
if nav_mesh.get_polygon_count() == 0:
push_error("Baked Jajce navigation mesh contains no polygons")
quit(1)
return
var error := ResourceSaver.save(nav_mesh, OUTPUT_PATH)
if error != OK:
push_error("Could not save %s: %s" % [OUTPUT_PATH, error_string(error)])
quit(1)
return
print(
"[TOOL] Saved %s with %d vertices and %d polygons"
% [OUTPUT_PATH, nav_mesh.get_vertices().size(), nav_mesh.get_polygon_count()]
)
quit(0)
func _create_navigation_mesh_template() -> NavigationMesh:
var nav_mesh := NavigationMesh.new()
nav_mesh.agent_height = 1.7
nav_mesh.agent_radius = 0.4
nav_mesh.agent_max_climb = 0.55
nav_mesh.agent_max_slope = 35.0
nav_mesh.cell_size = 0.25
nav_mesh.cell_height = 0.1
nav_mesh.filter_baking_aabb = AABB(
Vector3(-36.0, -8.0, -36.0), Vector3(72.0, 32.0, 72.0)
)
return nav_mesh
+71
View File
@@ -0,0 +1,71 @@
extends SceneTree
const OUTPUT_PATH := "res://docs/baselines/jajce_lookdev_01.png"
const SCENE_PATH := "res://world/jajce/JajceLookdev.tscn"
const CAPTURE_SIZE := Vector2i(1280, 720)
const WARMUP_FRAMES := 90
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
root.size = CAPTURE_SIZE
var scene := load(SCENE_PATH) as PackedScene
if scene == null:
push_error("Could not load %s" % SCENE_PATH)
quit(1)
return
var lookdev := scene.instantiate() as Node3D
root.add_child(lookdev)
await process_frame
for _frame in WARMUP_FRAMES:
await process_frame
var image := root.get_texture().get_image()
if image == null or image.is_empty():
push_error("Lookdev capture produced no image")
quit(1)
return
var analysis := _analyze_image(image)
if analysis.get("range", 0.0) < 0.05:
push_error("Lookdev capture is too uniform: %s" % analysis)
quit(1)
return
var error := image.save_png(OUTPUT_PATH)
if error != OK:
push_error("Could not save %s: %s" % [OUTPUT_PATH, error_string(error)])
quit(1)
return
print("[TOOL] Saved %s | %s" % [OUTPUT_PATH, analysis])
quit(0)
func _analyze_image(image: Image) -> Dictionary:
var min_luma := INF
var max_luma := -INF
var total_luma := 0.0
var samples := 0
var step_x = maxi(1, image.get_width() / 64)
var step_y = maxi(1, image.get_height() / 36)
for y in range(0, image.get_height(), step_y):
for x in range(0, image.get_width(), step_x):
var color := image.get_pixel(x, y)
var luma := color.r * 0.2126 + color.g * 0.7152 + color.b * 0.0722
min_luma = minf(min_luma, luma)
max_luma = maxf(max_luma, luma)
total_luma += luma
samples += 1
return {
"size": "%sx%s" % [image.get_width(), image.get_height()],
"samples": samples,
"average_luma": total_luma / samples,
"range": max_luma - min_luma,
}
+119
View File
@@ -0,0 +1,119 @@
extends SceneTree
const SCENE_PATH := "res://main.tscn"
const DEBUG_OUTPUT_PATH := "res://docs/baselines/simulation_garden_01_debug.png"
const CINEMATIC_OUTPUT_PATH := "res://docs/baselines/simulation_garden_01_cinematic.png"
const CAPTURE_SIZE := Vector2i(1280, 720)
const WARMUP_FRAMES := 540
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
root.size = CAPTURE_SIZE
var scene := load(SCENE_PATH) as PackedScene
if scene == null:
push_error("Could not load %s" % SCENE_PATH)
quit(1)
return
var main_scene := scene.instantiate()
var simulation_manager := main_scene.get_node("SimulationManager")
simulation_manager.debug_logs = false
root.add_child(main_scene)
await process_frame
_apply_capture_camera(main_scene)
for _frame in WARMUP_FRAMES:
await process_frame
_apply_capture_camera(main_scene)
var active_npcs := main_scene.get_node("ActiveNPCs")
var demo_controller := main_scene.get_node("DemoController")
if simulation_manager.npcs.is_empty() or active_npcs.get_child_count() == 0:
push_error("Simulation Garden capture needs active NPCs")
quit(1)
return
if demo_controller.has_method("toggle_debug_overlay"):
if not demo_controller.debug_overlay_visible:
demo_controller.toggle_debug_overlay()
var debug_analysis := _capture(DEBUG_OUTPUT_PATH)
if debug_analysis.is_empty():
quit(1)
return
if demo_controller.has_method("toggle_debug_overlay") and demo_controller.debug_overlay_visible:
demo_controller.toggle_debug_overlay()
await process_frame
for _frame in 30:
await process_frame
var cinematic_analysis := _capture(CINEMATIC_OUTPUT_PATH)
if cinematic_analysis.is_empty():
quit(1)
return
print(
"[TOOL] Saved Simulation Garden captures | debug %s | cinematic %s"
% [debug_analysis, cinematic_analysis]
)
quit(0)
func _capture(output_path: String) -> Dictionary:
var image := root.get_texture().get_image()
if image == null or image.is_empty():
push_error("Simulation Garden capture produced no image")
return {}
var analysis := _analyze_image(image)
if analysis.get("range", 0.0) < 0.05:
push_error("Simulation Garden capture is too uniform: %s" % analysis)
return {}
var error := image.save_png(output_path)
if error != OK:
push_error("Could not save %s: %s" % [output_path, error_string(error)])
return {}
return analysis
func _analyze_image(image: Image) -> Dictionary:
var min_luma := INF
var max_luma := -INF
var total_luma := 0.0
var samples := 0
var step_x = maxi(1, image.get_width() / 64)
var step_y = maxi(1, image.get_height() / 36)
for y in range(0, image.get_height(), step_y):
for x in range(0, image.get_width(), step_x):
var color := image.get_pixel(x, y)
var luma := color.r * 0.2126 + color.g * 0.7152 + color.b * 0.0722
min_luma = minf(min_luma, luma)
max_luma = maxf(max_luma, luma)
total_luma += luma
samples += 1
return {
"size": "%sx%s" % [image.get_width(), image.get_height()],
"samples": samples,
"average_luma": total_luma / samples,
"range": max_luma - min_luma,
}
func _apply_capture_camera(main_scene: Node) -> void:
var camera_rig := main_scene.get_node("CameraRig")
if camera_rig.has_method("apply_presentation_preset"):
camera_rig.pitch_degrees = -42.0
camera_rig.apply_presentation_preset(Vector3(-2.0, 1.2, -0.5), 128.0, 0.95)
camera_rig.set_physics_process(false)
var camera := main_scene.get_node("CameraRig/Camera3D") as Camera3D
camera.fov = 62.0
+3
View File
@@ -7,6 +7,9 @@
[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
File diff suppressed because one or more lines are too long
+142 -13
View File
@@ -1,4 +1,4 @@
[gd_scene load_steps=26 format=3] [gd_scene load_steps=39 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"]
@@ -15,6 +15,7 @@
[ext_resource type="Script" path="res://world/jajce/day_night_cycle.gd" id="13_daynight"] [ext_resource type="Script" path="res://world/jajce/day_night_cycle.gd" id="13_daynight"]
[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"]
[sub_resource type="Terrain3DMaterial" id="Terrain3DMaterial_jajce"] [sub_resource type="Terrain3DMaterial" id="Terrain3DMaterial_jajce"]
_shader_parameters = { _shader_parameters = {
@@ -44,6 +45,56 @@ size = Vector3(64, 0.2, 64)
[sub_resource type="BoxShape3D" id="Shape_ground"] [sub_resource type="BoxShape3D" id="Shape_ground"]
size = Vector3(64, 0.2, 64) size = Vector3(64, 0.2, 64)
[sub_resource type="StandardMaterial3D" id="Material_path_readability"]
albedo_color = Color(0.52, 0.39, 0.24, 1)
roughness = 0.95
[sub_resource type="BoxMesh" id="Mesh_path_readability"]
material = SubResource("Material_path_readability")
size = Vector3(1, 0.06, 1)
[sub_resource type="StandardMaterial3D" id="Material_landmark_banner"]
albedo_color = Color(0.68, 0.12, 0.1, 1)
roughness = 0.72
[sub_resource type="StandardMaterial3D" id="Material_site_canvas"]
albedo_color = Color(0.82, 0.68, 0.38, 1)
roughness = 0.88
[sub_resource type="StandardMaterial3D" id="Material_site_dark_wood"]
albedo_color = Color(0.24, 0.13, 0.07, 1)
roughness = 0.82
[sub_resource type="StandardMaterial3D" id="Material_site_book"]
albedo_color = Color(0.37, 0.18, 0.62, 1)
roughness = 0.74
[sub_resource type="BoxMesh" id="Mesh_landmark_banner"]
material = SubResource("Material_landmark_banner")
size = Vector3(0.12, 1.7, 1.0)
[sub_resource type="CylinderMesh" id="Mesh_site_pole"]
material = SubResource("Material_site_dark_wood")
top_radius = 0.06
bottom_radius = 0.07
height = 2.2
[sub_resource type="BoxMesh" id="Mesh_site_sign"]
material = SubResource("Material_site_canvas")
size = Vector3(1.0, 0.08, 0.5)
[sub_resource type="BoxMesh" id="Mesh_site_sack"]
material = SubResource("Material_site_canvas")
size = Vector3(0.5, 0.62, 0.42)
[sub_resource type="BoxMesh" id="Mesh_study_book"]
material = SubResource("Material_site_book")
size = Vector3(0.42, 0.08, 0.3)
[sub_resource type="BoxMesh" id="Mesh_rest_canopy"]
material = SubResource("Material_site_canvas")
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.23, 0.43, 0.62, 1)
sky_horizon_color = Color(0.82, 0.68, 0.5, 1) sky_horizon_color = Color(0.82, 0.68, 0.5, 1)
@@ -118,18 +169,7 @@ size = Vector3(2.8, 0.3, 0.9)
script = ExtResource("3_world") script = ExtResource("3_world")
[node name="NavigationRegion3D" type="NavigationRegion3D" parent="."] [node name="NavigationRegion3D" type="NavigationRegion3D" parent="."]
navigation_mesh = SubResource("NavigationMesh_greybox") navigation_mesh = ExtResource("16_nav")
[node name="GreyboxGround" type="StaticBody3D" parent="NavigationRegion3D"]
collision_layer = 0
collision_mask = 0
[node name="Mesh" type="MeshInstance3D" parent="NavigationRegion3D/GreyboxGround"]
visible = false
mesh = SubResource("Mesh_ground")
[node name="Collision" type="CollisionShape3D" parent="NavigationRegion3D/GreyboxGround"]
shape = SubResource("Shape_ground")
[node name="TerrainRoot" type="Node3D" parent="."] [node name="TerrainRoot" type="Node3D" parent="."]
@@ -146,6 +186,14 @@ position = Vector3(-25, 7, 9)
[node name="Keep" parent="FortressBlockout" instance=ExtResource("5_fortress")] [node name="Keep" parent="FortressBlockout" instance=ExtResource("5_fortress")]
[node name="RidgeBannerPole" type="MeshInstance3D" parent="FortressBlockout/Keep"]
position = Vector3(2.2, 8.6, 2.8)
mesh = SubResource("Mesh_site_pole")
[node name="RidgeBanner" type="MeshInstance3D" parent="FortressBlockout/Keep"]
position = Vector3(2.2, 8.95, 3.35)
mesh = SubResource("Mesh_landmark_banner")
[node name="WaterRoot" type="Node3D" parent="."] [node name="WaterRoot" type="Node3D" parent="."]
[node name="RiverSurface" parent="WaterRoot" instance=ExtResource("9_water")] [node name="RiverSurface" parent="WaterRoot" instance=ExtResource("9_water")]
@@ -180,6 +228,34 @@ position = Vector3(19, 2, 7)
[node name="Bridge" parent="VillageRoot" instance=ExtResource("8_bridge")] [node name="Bridge" parent="VillageRoot" instance=ExtResource("8_bridge")]
position = Vector3(25, 0.5, 0) position = Vector3(25, 0.5, 0)
[node name="Path_Village_Spine" type="MeshInstance3D" parent="VillageRoot"]
position = Vector3(-3, 0.04, -1)
rotation_degrees = Vector3(0, 20, 0)
scale = Vector3(1.25, 1, 13)
cast_shadow = 0
mesh = SubResource("Mesh_path_readability")
[node name="Path_Pantry_Worksites" type="MeshInstance3D" parent="VillageRoot"]
position = Vector3(-2, 0.04, -7.5)
rotation_degrees = Vector3(0, 82, 0)
scale = Vector3(1.15, 1, 8)
cast_shadow = 0
mesh = SubResource("Mesh_path_readability")
[node name="Path_River_Bridge" type="MeshInstance3D" parent="VillageRoot"]
position = Vector3(12, 0.04, 3.5)
rotation_degrees = Vector3(0, 72, 0)
scale = Vector3(1.35, 1, 18)
cast_shadow = 0
mesh = SubResource("Mesh_path_readability")
[node name="Path_Forest_Edge" type="MeshInstance3D" parent="VillageRoot"]
position = Vector3(4, 0.04, -9)
rotation_degrees = Vector3(0, -42, 0)
scale = Vector3(1.1, 1, 9)
cast_shadow = 0
mesh = SubResource("Mesh_path_readability")
[node name="FoliageRoot" type="Node3D" parent="."] [node name="FoliageRoot" type="Node3D" parent="."]
[node name="Tree_West_01" parent="FoliageRoot" instance=ExtResource("4_tree")] [node name="Tree_West_01" parent="FoliageRoot" instance=ExtResource("4_tree")]
@@ -343,6 +419,25 @@ discovery_priority = 0.75
[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="PantrySignPole" type="MeshInstance3D" parent="WorldObjects/StorageSites/VillagePantry"]
position = Vector3(-1.05, 1.1, 1.25)
mesh = SubResource("Mesh_site_pole")
[node name="PantrySign" type="MeshInstance3D" parent="WorldObjects/StorageSites/VillagePantry"]
position = Vector3(-1.05, 1.85, 1.25)
rotation_degrees = Vector3(0, 90, 0)
mesh = SubResource("Mesh_site_sign")
[node name="PantrySack_A" type="MeshInstance3D" parent="WorldObjects/StorageSites/VillagePantry"]
position = Vector3(-0.95, 0.3, -0.65)
scale = Vector3(0.65, 1.0, 0.65)
mesh = SubResource("Mesh_site_sack")
[node name="PantrySack_B" type="MeshInstance3D" parent="WorldObjects/StorageSites/VillagePantry"]
position = Vector3(-1.35, 0.22, -0.25)
scale = Vector3(0.5, 0.75, 0.5)
mesh = SubResource("Mesh_site_sack")
[node name="ActivitySites" type="Node3D" parent="WorldObjects"] [node name="ActivitySites" type="Node3D" parent="WorldObjects"]
[node name="GuardPost" type="Node3D" parent="WorldObjects/ActivitySites"] [node name="GuardPost" type="Node3D" parent="WorldObjects/ActivitySites"]
@@ -357,6 +452,15 @@ capacity = 2
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.25, 0) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.25, 0)
mesh = SubResource("Mesh_guard_post") mesh = SubResource("Mesh_guard_post")
[node name="GuardPennantPole" type="MeshInstance3D" parent="WorldObjects/ActivitySites/GuardPost"]
position = Vector3(0.45, 2.35, 0)
mesh = SubResource("Mesh_site_pole")
[node name="GuardPennant" type="MeshInstance3D" parent="WorldObjects/ActivitySites/GuardPost"]
position = Vector3(0.45, 2.85, 0.48)
scale = Vector3(0.8, 0.55, 0.65)
mesh = SubResource("Mesh_landmark_banner")
[node name="InteractionPoint" type="Marker3D" parent="WorldObjects/ActivitySites/GuardPost"] [node name="InteractionPoint" type="Marker3D" parent="WorldObjects/ActivitySites/GuardPost"]
[node name="DebugLabel" type="Label3D" parent="WorldObjects/ActivitySites/GuardPost"] [node name="DebugLabel" type="Label3D" parent="WorldObjects/ActivitySites/GuardPost"]
@@ -379,6 +483,16 @@ display_name = "Study Desk"
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.125, 0) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.125, 0)
mesh = SubResource("Mesh_study_desk") mesh = SubResource("Mesh_study_desk")
[node name="OpenBook_A" type="MeshInstance3D" parent="WorldObjects/ActivitySites/StudyDesk"]
position = Vector3(-0.25, 0.32, 0)
rotation_degrees = Vector3(0, 14, 0)
mesh = SubResource("Mesh_study_book")
[node name="OpenBook_B" type="MeshInstance3D" parent="WorldObjects/ActivitySites/StudyDesk"]
position = Vector3(0.22, 0.32, 0.04)
rotation_degrees = Vector3(0, -14, 0)
mesh = SubResource("Mesh_study_book")
[node name="InteractionPoint" type="Marker3D" parent="WorldObjects/ActivitySites/StudyDesk"] [node name="InteractionPoint" type="Marker3D" parent="WorldObjects/ActivitySites/StudyDesk"]
[node name="DebugLabel" type="Label3D" parent="WorldObjects/ActivitySites/StudyDesk"] [node name="DebugLabel" type="Label3D" parent="WorldObjects/ActivitySites/StudyDesk"]
@@ -402,6 +516,21 @@ capacity = 2
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.15, 0) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.15, 0)
mesh = SubResource("Mesh_rest_bench") mesh = SubResource("Mesh_rest_bench")
[node name="RestCanopy" type="MeshInstance3D" parent="WorldObjects/ActivitySites/RestBench"]
position = Vector3(0, 1.35, 0)
rotation_degrees = Vector3(0, 0, -6)
mesh = SubResource("Mesh_rest_canopy")
[node name="RestCanopyPost_A" type="MeshInstance3D" parent="WorldObjects/ActivitySites/RestBench"]
position = Vector3(-1.25, 0.72, -0.52)
scale = Vector3(0.8, 0.8, 0.8)
mesh = SubResource("Mesh_site_pole")
[node name="RestCanopyPost_B" type="MeshInstance3D" parent="WorldObjects/ActivitySites/RestBench"]
position = Vector3(1.25, 0.72, 0.52)
scale = Vector3(0.8, 0.8, 0.8)
mesh = SubResource("Mesh_site_pole")
[node name="InteractionPoint" type="Marker3D" parent="WorldObjects/ActivitySites/RestBench"] [node name="InteractionPoint" type="Marker3D" parent="WorldObjects/ActivitySites/RestBench"]
[node name="DebugLabel" type="Label3D" parent="WorldObjects/ActivitySites/RestBench"] [node name="DebugLabel" type="Label3D" parent="WorldObjects/ActivitySites/RestBench"]
+7
View File
@@ -1,6 +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 var directional_light: DirectionalLight3D @export var directional_light: DirectionalLight3D
@export var world_environment: WorldEnvironment @export var world_environment: WorldEnvironment
@@ -37,6 +38,12 @@ const SUNRISE_SUNSET_ENERGY := 0.8
var elapsed := 0.0 var elapsed := 0.0
func _ready() -> void:
elapsed = cycle_duration_seconds * initial_cycle
_apply_light_rotation(initial_cycle)
_apply_environment(initial_cycle)
func _process(delta: float) -> void: func _process(delta: float) -> void:
elapsed += delta elapsed += delta
if elapsed >= cycle_duration_seconds: if elapsed >= cycle_duration_seconds:
+14
View File
@@ -36,6 +36,10 @@ func initialize_world_view() -> void:
simulation_manager.npc_inventory_changed.connect(_on_npc_inventory_changed) simulation_manager.npc_inventory_changed.connect(_on_npc_inventory_changed)
else: else:
push_error("WorldViewManager: SimulationManager has no npc_inventory_changed signal") push_error("WorldViewManager: SimulationManager has no npc_inventory_changed signal")
if simulation_manager.has_signal("npc_task_changed"):
simulation_manager.npc_task_changed.connect(_on_npc_task_changed)
else:
push_error("WorldViewManager: SimulationManager has no npc_task_changed signal")
if simulation_manager.has_signal("state_restored"): if simulation_manager.has_signal("state_restored"):
simulation_manager.state_restored.connect(_on_simulation_state_restored) simulation_manager.state_restored.connect(_on_simulation_state_restored)
else: else:
@@ -168,6 +172,16 @@ func _on_npc_inventory_changed(npc: SimNPC, item_id: StringName, amount: float)
push_error("WorldViewManager: NPCVisual has no set_carried_food_visible method") push_error("WorldViewManager: NPCVisual has no set_carried_food_visible method")
func _on_npc_task_changed(npc: SimNPC, _old_task: StringName, _new_task: StringName) -> void:
var visual = active_npc_visuals.get(npc.id)
if visual == null:
return
if visual.has_method("set_task_presentation"):
visual.set_task_presentation(npc.current_task, npc.task_state)
else:
push_error("WorldViewManager: NPCVisual has no set_task_presentation method")
func _on_simulation_state_restored() -> void: func _on_simulation_state_restored() -> void:
for visual in active_npc_visuals.values(): for visual in active_npc_visuals.values():
visual.queue_free() visual.queue_free()