Compare commits
7 Commits
d2eca15ecd
...
c6e2d64162
| Author | SHA1 | Date | |
|---|---|---|---|
| c6e2d64162 | |||
| 5251644b78 | |||
| cd0309e224 | |||
| e813a8b2d5 | |||
| 0fa8d80119 | |||
| 324084f233 | |||
| cc0d6ef548 |
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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:
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
```
|
||||||
@@ -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 |
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ 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
|
||||||
await physics_frame
|
for _frame in 10:
|
||||||
|
await physics_frame
|
||||||
|
|
||||||
var simulation_manager: Node = main_scene.get_node("SimulationManager")
|
var simulation_manager: Node = main_scene.get_node("SimulationManager")
|
||||||
simulation_manager.set_process(false)
|
simulation_manager.set_process(false)
|
||||||
@@ -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]
|
||||||
|
)
|
||||||
|
|||||||
@@ -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]
|
||||||
|
)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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
@@ -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"]
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user