feat: stage simulation garden first read

This commit is contained in:
2026-07-08 16:47:25 +02:00
parent cd0309e224
commit 5251644b78
11 changed files with 104 additions and 10 deletions
+5 -2
View File
@@ -637,11 +637,14 @@ Completed:
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.
Next:
1. Improve camera staging plus readable path and landmark composition without
adding new simulation mechanics.
1. Strengthen landmark and work-site silhouettes without adding new simulation
mechanics.
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
+2
View File
@@ -685,6 +685,8 @@ Recently completed:
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.
This order strengthens the simulation while regularly producing visible
progress suitable for public development updates.
+4 -2
View File
@@ -218,8 +218,9 @@ plugin content, not game architecture.
- The player is a `CharacterBody3D` represented by placeholder primitive
geometry.
- WASD movement is camera-relative.
- The elevated third-person camera rotates with the mouse and uses smoothed
follow/focus behavior.
- The elevated third-person camera rotates with the mouse, uses smoothed
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
village through the same `ResourceNode` contract used by NPCs.
- Guard, study, rest, and food interactions now use typed world sites.
@@ -331,6 +332,7 @@ distance with resource `safety_risk`, `comfort_distance`, and
- a centered 512 m Terrain3D landscape split across four regions;
- deterministic shaped terrain data and six game-owned surface layers;
- fortress, houses, mill, bridge, shader river/waterfall, mist, and smoke;
- authored dirt path strips that make the current village task loop readable;
- warm sky, fog, shadows, and wind-reactive foliage;
- eight ResourceNodes preserving stable food/wood discovery IDs;
- typed village pantry storage and typed guard, study, and rest activity sites;
+6 -3
View File
@@ -29,15 +29,18 @@ 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.
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 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 reads better than the old flat map, but paths,
landmarks, water, and foreground silhouettes need stronger first-read staging;
- 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 help, but the cinematic view still needs clearer work-site props
and route composition so intent is readable before the viewer learns the icon
language.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 506 KiB

After

Width:  |  Height:  |  Size: 707 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 530 KiB

After

Width:  |  Height:  |  Size: 694 KiB

+1
View File
@@ -53,6 +53,7 @@ target = NodePath("../Player")
follow_speed = 8.0
deadzone_right = 2.0
deadzone_forward = 2.0
max_zoom_offset = Vector3(0, 20, 15)
[node name="Camera3D" type="Camera3D" parent="CameraRig" unique_id=1992528776]
current = true
+19 -2
View File
@@ -6,6 +6,7 @@ extends Node3D
@export var follow_speed := 12.0
@export var rotation_smooth_speed := 18.0
@export var focus_smooth_speed := 14.0
@export var pitch_degrees := -55.0
@export var mouse_sensitivity := 0.002
@@ -32,7 +33,7 @@ func _ready() -> void:
if target:
focus_position = target.global_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:
@@ -59,7 +60,7 @@ func _physics_process(delta: float) -> void:
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 diff := target_pos - desired_focus_position
@@ -89,3 +90,19 @@ func _physics_process(delta: float) -> void:
var desired_position := focus_position + rotated_offset
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)
+17
View File
@@ -27,6 +27,17 @@ func _run() -> void:
terrain.get_camera() == main_scene.get_node("CameraRig/Camera3D"),
"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"),
@@ -47,6 +58,12 @@ func _run() -> void:
simulation_manager.resource_states.size() == 12,
"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")
var navigation_map: RID = main_scene.get_world_3d().navigation_map
var navigation_region := main_scene.get_node("JajceWorld/NavigationRegion3D") as NavigationRegion3D
+13
View File
@@ -25,8 +25,10 @@ func _run() -> void:
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")
@@ -104,3 +106,14 @@ func _analyze_image(image: Image) -> Dictionary:
"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
+37 -1
View File
@@ -1,4 +1,4 @@
[gd_scene load_steps=27 format=3]
[gd_scene load_steps=29 format=3]
[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"]
@@ -45,6 +45,14 @@ size = Vector3(64, 0.2, 64)
[sub_resource type="BoxShape3D" id="Shape_ground"]
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="ProceduralSkyMaterial" id="SkyMaterial_jajce"]
sky_top_color = Color(0.23, 0.43, 0.62, 1)
sky_horizon_color = Color(0.82, 0.68, 0.5, 1)
@@ -170,6 +178,34 @@ position = Vector3(19, 2, 7)
[node name="Bridge" parent="VillageRoot" instance=ExtResource("8_bridge")]
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="Tree_West_01" parent="FoliageRoot" instance=ExtResource("4_tree")]