feat: add Jajce world scaffold

This commit is contained in:
2026-07-04 21:06:57 +02:00
parent 83fc258afc
commit 556d0fdc26
19 changed files with 545 additions and 0 deletions
+46
View File
@@ -0,0 +1,46 @@
# Flat-map baseline
- **Captured:** 2026-07-04
- **Scene:** `res://main.tscn`
- **Automated check:** `res://tests/flat_map_baseline_test.gd`
- **Companion resource test:** `res://tests/resource_node_player_parity_test.gd`
## Contract
The flat prototype remains the regression reference while `JajceWorld` is
developed separately.
The automated baseline verifies:
- the scene creates three simulated NPCs;
- four stable-ID ResourceNodes register;
- three fixed origins can reach the four remaining activity markers and four
ResourceNode interaction points (24 route checks);
- a two-tick working task completes;
- starvation still reaches death at its configured threshold.
The companion player-parity scenario verifies:
- player food extraction conserves the actual remaining amount;
- player wood extraction uses the configured yield;
- player depletion releases an NPC reservation.
## Commands
```powershell
& "C:\Program Files (x86)\Godot\Godot_v4.5.1-stable_win64_console.exe" `
--headless --path . --script res://tests/flat_map_baseline_test.gd
& "C:\Program Files (x86)\Godot\Godot_v4.5.1-stable_win64_console.exe" `
--headless --path . --script res://tests/resource_node_player_parity_test.gd
```
Godot 4.5.1 is the locally available validation runtime. The project currently
declares Godot 4.7 features, so final editor validation should also be performed
with the project version when available.
## Purpose
This baseline protects behavior, not visual composition. Terrain, lighting,
camera framing, and placeholder geometry may change. A Jajce migration is a
regression if it breaks these contracts without an explicit replacement test.
+3
View File
@@ -0,0 +1,3 @@
[gd_resource type="Terrain3DAssets" format=3]
[resource]
+1
View File
@@ -0,0 +1 @@
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+90
View File
@@ -0,0 +1,90 @@
extends SceneTree
var failures: Array[String] = []
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
var main_scene: Node = load("res://main.tscn").instantiate()
root.add_child(main_scene)
await process_frame
await physics_frame
var simulation_manager: Node = main_scene.get_node("SimulationManager")
simulation_manager.set_process(false)
_check(simulation_manager.npcs.size() == 3, "Baseline should create three NPCs")
_check(main_scene.get_node("ResourceNodes").get_child_count() == 4, "Baseline should contain four ResourceNodes")
var navigation_map: RID = main_scene.get_world_3d().navigation_map
await _wait_for_navigation_map(navigation_map)
NavigationServer3D.map_force_update(navigation_map)
var fixed_origins := [
Vector3(-6.0, 0.0, -4.0),
Vector3(0.0, 0.0, 0.0),
Vector3(6.0, 0.0, 4.0)
]
var destinations: Array[Vector3] = []
for marker_path in [
"TaskZone/FoodZone",
"TaskZone/GuardZone",
"TaskZone/StudyZone",
"TaskZone/RestZone"
]:
destinations.append(main_scene.get_node(marker_path).global_position)
for resource in main_scene.get_node("ResourceNodes").get_children():
destinations.append((resource as ResourceNode).interaction_point.global_position)
for origin in fixed_origins:
for destination in destinations:
var path := NavigationServer3D.map_get_path(
navigation_map,
origin,
destination,
true
)
_check(
not path.is_empty(),
"Navigation path should exist from %s to %s" % [origin, destination]
)
var village := SimVillage.new()
var worker := SimNPC.new(100, "BaselineWorker", "scholar", 5.0, 5.0)
worker.set_task("study", 2.0)
worker.start_working()
worker.simulate_tick(village)
worker.simulate_tick(village)
_check(worker.task_complete, "A two-tick working task should complete")
var starving := SimNPC.new(101, "BaselineStarving", "wanderer", 5.0, 5.0)
starving.hunger = 100.0
starving.starvation_death_threshold = 1
starving.simulate_tick(village)
_check(starving.is_dead, "Starvation threshold should still kill an NPC")
if failures.is_empty():
print("[TEST] Flat-map baseline passed: 3 NPCs, 4 resources, 24 navigation routes")
quit(0)
return
for failure in failures:
push_error("[TEST] " + failure)
quit(1)
func _wait_for_navigation_map(navigation_map: RID) -> void:
for attempt in 30:
if (
NavigationServer3D.map_get_iteration_id(navigation_map) > 0
and not NavigationServer3D.map_get_regions(navigation_map).is_empty()
):
await physics_frame
return
await physics_frame
_check(false, "Navigation map did not synchronize within 30 physics frames")
func _check(condition: bool, message: String) -> void:
if not condition:
failures.append(message)
+1
View File
@@ -0,0 +1 @@
uid://cpach52mwjq67
+118
View File
@@ -0,0 +1,118 @@
extends SceneTree
var failures: Array[String] = []
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
var camera := Camera3D.new()
camera.current = true
root.add_child(camera)
var world: Node3D = load("res://world/jajce/JajceWorld.tscn").instantiate()
root.add_child(world)
await process_frame
for frame in 10:
await physics_frame
var terrain = world.get_node("TerrainRoot/Terrain3D")
_check(terrain is Terrain3D, "JajceWorld should own a Terrain3D node")
_check(
terrain.data_directory == "res://terrain/jajce/data",
"Terrain3D should use dedicated Jajce data"
)
_check(world.has_node("WaterRoot"), "JajceWorld should expose WaterRoot")
_check(world.has_node("VillageRoot"), "JajceWorld should expose VillageRoot")
_check(world.has_node("FoliageRoot"), "JajceWorld should expose FoliageRoot")
_check(
world.has_node("GreyboxLandmarks/FortressBlockout"),
"Greybox should include the fortress landmark"
)
_check(
world.has_node("WaterRoot/WaterfallGreybox"),
"Greybox should include the waterfall drop"
)
_check(
world.has_node("VillageRoot/MillBlockout")
and world.has_node("VillageRoot/BridgeBlockout"),
"Greybox should include the mill and bridge"
)
var lookdev: Node3D = load("res://world/jajce/JajceLookdev.tscn").instantiate()
_check(lookdev.has_node("JajceWorld"), "Lookdev should instance JajceWorld")
_check(lookdev.has_node("BeautyCamera"), "Lookdev should provide a beauty camera")
lookdev.free()
var resources := world.get_node("WorldObjects/ResourceNodes").get_children()
var ids: Array[String] = []
for resource in resources:
ids.append(String((resource as ResourceNode).node_id))
ids.sort()
_check(
ids == ["berry_bush_01", "berry_bush_02", "tree_01", "tree_02"],
"Jajce resources should preserve all stable IDs"
)
var navigation_region := world.get_node("NavigationRegion3D") as NavigationRegion3D
var navigation_map: RID = NavigationServer3D.region_get_map(navigation_region.get_rid())
_check(navigation_map.is_valid(), "Jajce navigation region should be assigned to a map")
_check(
navigation_region.navigation_mesh.get_polygon_count() > 0,
"Jajce navigation mesh should contain baked polygons"
)
await _wait_for_navigation_map(navigation_map)
NavigationServer3D.map_force_update(navigation_map)
var origins := [
Vector3(-6, 0, -4),
Vector3(0, 0, 0),
Vector3(6, 0, 4)
]
var destinations: Array[Vector3] = []
for resource in resources:
destinations.append((resource as ResourceNode).interaction_point.global_position)
for marker in world.get_node("LegacyActivityMarkers").get_children():
destinations.append((marker as Marker3D).global_position)
for origin in origins:
for destination in destinations:
var path := NavigationServer3D.map_get_path(
navigation_map,
origin,
destination,
true
)
_check(
not path.is_empty(),
"Jajce navigation path should exist from %s to %s" % [origin, destination]
)
var selected := ResourceNode.find_available(&"gather_food", 777, Vector3.ZERO)
_check(selected != null, "Target discovery should find Jajce resources without a parent path")
if selected != null:
_check(selected.reserve(777), "Selected Jajce resource should be reservable")
selected.release(777)
if failures.is_empty():
print("[TEST] Jajce scaffold passed: Terrain3D, stable IDs, 24 navigation routes")
quit(0)
return
for failure in failures:
push_error("[TEST] " + failure)
quit(1)
func _wait_for_navigation_map(navigation_map: RID) -> void:
for attempt in 30:
if (
NavigationServer3D.map_get_iteration_id(navigation_map) > 0
and not NavigationServer3D.map_get_regions(navigation_map).is_empty()
):
await physics_frame
return
await physics_frame
_check(false, "Jajce navigation map did not synchronize within 30 physics frames")
func _check(condition: bool, message: String) -> void:
if not condition:
failures.append(message)
+1
View File
@@ -0,0 +1 @@
uid://bhujwfm70p60v
+41
View File
@@ -0,0 +1,41 @@
extends SceneTree
const REGION_SIZE := 256
const TERRAIN_SIZE := 512
const DATA_DIRECTORY := "res://terrain/jajce/data"
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
var camera := Camera3D.new()
camera.current = true
root.add_child(camera)
var terrain := Terrain3D.new()
terrain.region_size = REGION_SIZE
root.add_child(terrain)
terrain.set_camera(camera)
await process_frame
var height_map := Image.create_empty(
TERRAIN_SIZE,
TERRAIN_SIZE,
false,
Image.FORMAT_RF
)
height_map.fill(Color(0.0, 0.0, 0.0, 1.0))
var images: Array[Image]
images.resize(Terrain3DRegion.TYPE_MAX)
images[Terrain3DRegion.TYPE_HEIGHT] = height_map
terrain.data.import_images(
images,
Vector3(-TERRAIN_SIZE / 2.0, 0.0, -TERRAIN_SIZE / 2.0),
0.0,
1.0
)
terrain.data.save_directory(DATA_DIRECTORY)
print("[TOOL] Generated flat Jajce Terrain3D seed data in ", DATA_DIRECTORY)
quit(0)
+1
View File
@@ -0,0 +1 @@
uid://cbpngrlbf6fia
+15
View File
@@ -0,0 +1,15 @@
[gd_scene load_steps=3 format=3]
[ext_resource type="PackedScene" path="res://world/jajce/JajceWorld.tscn" id="1_world"]
[ext_resource type="Script" path="res://world/jajce/beauty_camera.gd" id="2_camera"]
[node name="JajceLookdev" type="Node3D"]
[node name="JajceWorld" parent="." instance=ExtResource("1_world")]
[node name="BeautyCamera" type="Camera3D" parent="."]
position = Vector3(44, 34, 44)
current = true
fov = 55.0
script = ExtResource("2_camera")
focus_point = Vector3(-4, 2, 0)
+198
View File
@@ -0,0 +1,198 @@
[gd_scene load_steps=21 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"]
[ext_resource type="Script" path="res://world/jajce/jajce_world.gd" id="3_world"]
[sub_resource type="Terrain3DMaterial" id="Terrain3DMaterial_jajce"]
world_background = 0
auto_shader = true
[sub_resource type="NavigationMesh" id="NavigationMesh_greybox"]
vertices = PackedVector3Array(-14.5, 0.5, 0, -1, 0.5, 0, -1, 0.5, -0.75, 0, 0.5, -1, 0, 0.5, -14.5, -14.5, 0.5, -14.5, 0.75, 0.5, -1, 0.75, 0.75, -0.5, 14.5, 0.5, -0.5, 14.5, 0.5, -14.5, 0.75, 0.75, 0, 0, 1, 0, 0, 0.75, 0.75, -0.5, 0.75, 0.75, -0.5, 0.5, 14.5, 14.5, 0.5, 14.5, -1, 0.5, 0.75, -14.5, 0.5, 14.5)
polygons = [PackedInt32Array(2, 1, 0), PackedInt32Array(3, 2, 4), PackedInt32Array(4, 2, 5), PackedInt32Array(5, 2, 0), PackedInt32Array(6, 3, 4), PackedInt32Array(8, 7, 6), PackedInt32Array(8, 6, 9), PackedInt32Array(9, 6, 4), PackedInt32Array(12, 11, 10), PackedInt32Array(10, 7, 8), PackedInt32Array(14, 13, 12), PackedInt32Array(10, 8, 12), PackedInt32Array(12, 8, 14), PackedInt32Array(14, 8, 15), PackedInt32Array(0, 1, 16), PackedInt32Array(16, 13, 14), PackedInt32Array(14, 17, 16), PackedInt32Array(16, 17, 0)]
[sub_resource type="StandardMaterial3D" id="Material_ground"]
albedo_color = Color(0.24, 0.48, 0.25, 1)
[sub_resource type="StandardMaterial3D" id="Material_ridge"]
albedo_color = Color(0.34, 0.31, 0.27, 1)
[sub_resource type="StandardMaterial3D" id="Material_terrace"]
albedo_color = Color(0.42, 0.5, 0.29, 1)
[sub_resource type="StandardMaterial3D" id="Material_water"]
albedo_color = Color(0.16, 0.43, 0.62, 0.78)
transparency = 1
[sub_resource type="StandardMaterial3D" id="Material_building"]
albedo_color = Color(0.62, 0.48, 0.32, 1)
[sub_resource type="BoxMesh" id="Mesh_ground"]
material = SubResource("Material_ground")
size = Vector3(64, 0.2, 64)
[sub_resource type="BoxShape3D" id="Shape_ground"]
size = Vector3(64, 0.2, 64)
[sub_resource type="BoxMesh" id="Mesh_ridge"]
material = SubResource("Material_ridge")
size = Vector3(12, 10, 28)
[sub_resource type="BoxMesh" id="Mesh_terrace"]
material = SubResource("Material_terrace")
size = Vector3(15, 1.5, 8)
[sub_resource type="BoxMesh" id="Mesh_river"]
material = SubResource("Material_water")
size = Vector3(5, 0.08, 56)
[sub_resource type="BoxMesh" id="Mesh_house"]
material = SubResource("Material_building")
size = Vector3(4, 4, 5)
[sub_resource type="BoxMesh" id="Mesh_fortress"]
material = SubResource("Material_building")
size = Vector3(8, 5, 10)
[sub_resource type="BoxMesh" id="Mesh_bridge"]
material = SubResource("Material_building")
size = Vector3(8, 0.5, 3)
[sub_resource type="BoxMesh" id="Mesh_waterfall"]
material = SubResource("Material_water")
size = Vector3(5, 10, 0.5)
[sub_resource type="Environment" id="Environment_greybox"]
background_mode = 1
background_color = Color(0.55, 0.72, 0.82, 1)
ambient_light_source = 2
ambient_light_color = Color(1, 0.9, 0.78, 1)
ambient_light_energy = 0.55
[node name="JajceWorld" type="Node3D"]
script = ExtResource("3_world")
[node name="NavigationRegion3D" type="NavigationRegion3D" parent="."]
navigation_mesh = SubResource("NavigationMesh_greybox")
[node name="TerrainRoot" type="Node3D" parent="."]
[node name="Terrain3D" type="Terrain3D" parent="TerrainRoot"]
data_directory = "res://terrain/jajce/data"
material = SubResource("Terrain3DMaterial_jajce")
assets = ExtResource("1_assets")
collision_mask = 3
collision_mode = 0
top_level = true
[node name="GreyboxGround" type="StaticBody3D" parent="NavigationRegion3D"]
[node name="Mesh" type="MeshInstance3D" parent="NavigationRegion3D/GreyboxGround"]
mesh = SubResource("Mesh_ground")
[node name="Collision" type="CollisionShape3D" parent="NavigationRegion3D/GreyboxGround"]
shape = SubResource("Shape_ground")
[node name="GreyboxLandmarks" type="Node3D" parent="."]
[node name="FortressRidge" type="MeshInstance3D" parent="GreyboxLandmarks"]
position = Vector3(-25, 5, 9)
mesh = SubResource("Mesh_ridge")
[node name="FortressBlockout" type="MeshInstance3D" parent="GreyboxLandmarks"]
position = Vector3(-25, 12.5, 9)
mesh = SubResource("Mesh_fortress")
[node name="UpperTerrace" type="MeshInstance3D" parent="GreyboxLandmarks"]
position = Vector3(-14, 0.75, 12)
mesh = SubResource("Mesh_terrace")
[node name="LowerTerrace" type="MeshInstance3D" parent="GreyboxLandmarks"]
position = Vector3(-11, 0.75, 2)
mesh = SubResource("Mesh_terrace")
[node name="WaterRoot" type="Node3D" parent="."]
[node name="RiverGreybox" type="MeshInstance3D" parent="WaterRoot"]
position = Vector3(25, 0.08, 0)
mesh = SubResource("Mesh_river")
[node name="WaterfallGreybox" type="MeshInstance3D" parent="WaterRoot"]
position = Vector3(25, 5, -24)
mesh = SubResource("Mesh_waterfall")
[node name="VillageRoot" type="Node3D" parent="."]
[node name="House_01" type="MeshInstance3D" parent="VillageRoot"]
position = Vector3(-15, 2, 7)
mesh = SubResource("Mesh_house")
[node name="House_02" type="MeshInstance3D" parent="VillageRoot"]
position = Vector3(-8, 2, 2)
mesh = SubResource("Mesh_house")
[node name="House_03" type="MeshInstance3D" parent="VillageRoot"]
position = Vector3(-15, 2, -4)
mesh = SubResource("Mesh_house")
[node name="MillBlockout" type="MeshInstance3D" parent="VillageRoot"]
position = Vector3(19, 2, 7)
mesh = SubResource("Mesh_house")
[node name="BridgeBlockout" type="MeshInstance3D" parent="VillageRoot"]
position = Vector3(25, 0.5, 0)
mesh = SubResource("Mesh_bridge")
[node name="FoliageRoot" type="Node3D" parent="."]
[node name="WorldObjects" type="Node3D" parent="."]
[node name="ResourceNodes" type="Node3D" parent="WorldObjects"]
[node name="BerryBush_01" parent="WorldObjects/ResourceNodes" instance=ExtResource("2_resource")]
position = Vector3(-6, 0, -8)
node_id = &"berry_bush_01"
[node name="BerryBush_02" parent="WorldObjects/ResourceNodes" instance=ExtResource("2_resource")]
position = Vector3(-10, 0, -4)
node_id = &"berry_bush_02"
amount_remaining = 8.0
[node name="Tree_01" parent="WorldObjects/ResourceNodes" instance=ExtResource("2_resource")]
position = Vector3(6, 0, -8)
node_id = &"tree_01"
action_id = &"gather_wood"
resource_id = &"wood"
amount_remaining = 15.0
yield_per_action = 3.0
[node name="Tree_02" parent="WorldObjects/ResourceNodes" instance=ExtResource("2_resource")]
position = Vector3(10, 0, -4)
node_id = &"tree_02"
action_id = &"gather_wood"
resource_id = &"wood"
amount_remaining = 12.0
yield_per_action = 3.0
[node name="LegacyActivityMarkers" type="Node3D" parent="."]
[node name="FoodZone" type="Marker3D" parent="LegacyActivityMarkers"]
position = Vector3(0, 0, -8)
[node name="GuardZone" type="Marker3D" parent="LegacyActivityMarkers"]
position = Vector3(0, 0, 8)
[node name="StudyZone" type="Marker3D" parent="LegacyActivityMarkers"]
position = Vector3(-6, 0, 6)
[node name="RestZone" type="Marker3D" parent="LegacyActivityMarkers"]
position = Vector3(4, 0, 5)
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]
rotation_degrees = Vector3(-55, -35, 0)
light_color = Color(1, 0.87, 0.72, 1)
light_energy = 1.25
[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
environment = SubResource("Environment_greybox")
+17
View File
@@ -0,0 +1,17 @@
extends Camera3D
@export var focus_point := Vector3(0.0, 2.0, 0.0)
@export var orbit_speed := 0.025
@export var animate_orbit := true
func _ready() -> void:
look_at(focus_point, Vector3.UP)
func _process(delta: float) -> void:
if not animate_orbit:
return
var offset := global_position - focus_point
offset = offset.rotated(Vector3.UP, orbit_speed * delta)
global_position = focus_point + offset
look_at(focus_point, Vector3.UP)
+1
View File
@@ -0,0 +1 @@
uid://dqe7gnj5dxho7
+11
View File
@@ -0,0 +1,11 @@
extends Node3D
@onready var terrain: Terrain3D = $TerrainRoot/Terrain3D
func _ready() -> void:
call_deferred("_bind_active_camera")
func _bind_active_camera() -> void:
var active_camera := get_viewport().get_camera_3d()
if active_camera != null:
terrain.set_camera(active_camera)
+1
View File
@@ -0,0 +1 @@
uid://b2kolejde4fxs