diff --git a/docs/BUILD_IN_PUBLIC_PLAN.md b/docs/BUILD_IN_PUBLIC_PLAN.md index b6fa848..d3ba456 100644 --- a/docs/BUILD_IN_PUBLIC_PLAN.md +++ b/docs/BUILD_IN_PUBLIC_PLAN.md @@ -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 diff --git a/docs/LEARNING_ROADMAP.md b/docs/LEARNING_ROADMAP.md index e06273d..9baba4d 100644 --- a/docs/LEARNING_ROADMAP.md +++ b/docs/LEARNING_ROADMAP.md @@ -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. diff --git a/docs/PROJECT_CONTEXT.md b/docs/PROJECT_CONTEXT.md index cb2ead3..88865f9 100644 --- a/docs/PROJECT_CONTEXT.md +++ b/docs/PROJECT_CONTEXT.md @@ -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; diff --git a/docs/baselines/SIMULATION_GARDEN_01.md b/docs/baselines/SIMULATION_GARDEN_01.md index 83b04ed..88718f0 100644 --- a/docs/baselines/SIMULATION_GARDEN_01.md +++ b/docs/baselines/SIMULATION_GARDEN_01.md @@ -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. diff --git a/docs/baselines/simulation_garden_01_cinematic.png b/docs/baselines/simulation_garden_01_cinematic.png index a1ed490..c605fda 100644 Binary files a/docs/baselines/simulation_garden_01_cinematic.png and b/docs/baselines/simulation_garden_01_cinematic.png differ diff --git a/docs/baselines/simulation_garden_01_debug.png b/docs/baselines/simulation_garden_01_debug.png index 1801106..2ff02f8 100644 Binary files a/docs/baselines/simulation_garden_01_debug.png and b/docs/baselines/simulation_garden_01_debug.png differ diff --git a/main.tscn b/main.tscn index c358892..a28b527 100644 --- a/main.tscn +++ b/main.tscn @@ -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 diff --git a/player/camera_rig.gd b/player/camera_rig.gd index 5d97fe6..2b82ace 100644 --- a/player/camera_rig.gd +++ b/player/camera_rig.gd @@ -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) diff --git a/tests/jajce_runtime_integration_test.gd b/tests/jajce_runtime_integration_test.gd index bcb521d..5202b0c 100644 --- a/tests/jajce_runtime_integration_test.gd +++ b/tests/jajce_runtime_integration_test.gd @@ -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 diff --git a/tools/capture_simulation_garden.gd b/tools/capture_simulation_garden.gd index 0256f65..4909312 100644 --- a/tools/capture_simulation_garden.gd +++ b/tools/capture_simulation_garden.gd @@ -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 diff --git a/world/jajce/JajceWorld.tscn b/world/jajce/JajceWorld.tscn index 50ed59c..6c760ca 100644 --- a/world/jajce/JajceWorld.tscn +++ b/world/jajce/JajceWorld.tscn @@ -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")]