M0.5: detailed scenery, mountain/sea-cliff boundary, Godot 4.7

Scenery (all procedural, seeded, no imported assets):
- Terrain shader: grass/dry grass/dirt/sand/rock strata/snow + detail normals
- 5 tree types, faceted mossy rocks, cliff outcrops, sea stacks, bushes,
  logs and stumps, batched as MultiMesh per 48 m cell
- Wind-animated grass and flowers streamed in tiles around the player
- Lake (depth-shaded water with shore foam), ocean to the horizon,
  cloud sky shader, volumetric fog, glow, softer shadows
- Swimming: float at the surface, swim-jump to climb out

World bounds:
- Steep mountain rim with an invisible wall partway up; one side opens to
  a sheer sea cliff with the wall 1 m inside the edge; whole chunk is an
  island so the mesh edge is never visible
- Respawn safety net if the player leaves the walkable area or the map
- Fix: the cliff edge now tracks the wall in the headlands (it used to
  drop away inside the wall, leaving a pit with lake water hanging over it)

Tests: tests/test_boundary.gd (24-direction escape attempt) and
tests/test_terrain_edges.gd (no ground below waterline near the wall).

Upgrades the project to Godot 4.7 (project.godot rewritten by the 4.7
importer, .uid files added) and documents it in the setup guide.
This commit is contained in:
Paul Bailey
2026-11-03 23:49:39 +00:00
parent 1ebfce08e4
commit 12f7fd677d
32 changed files with 1922 additions and 233 deletions

57
tests/test_boundary.gd Normal file
View File

@@ -0,0 +1,57 @@
extends SceneTree
## Boundary test: tries to escape the valley in several directions by walking
## and spamming jump at the mountain rim, and fails if the player ever gets past
## the invisible wall, falls off the map, or ends up stuck below the waterline
## (in a pit near the edge).
##
## Run: godot --headless --path . --fixed-fps 30 --script res://tests/test_boundary.gd
const DIRECTIONS := 24
const SECONDS_PER_TRY := 25.0
func _initialize() -> void:
var world: Node3D = load("res://scenes/world.tscn").instantiate()
root.add_child(world)
_run.call_deferred(world)
func _run(world: Node3D) -> void:
for _i in range(10):
await physics_frame
var terrain: Node3D = world.get_node("Terrain")
var player: CharacterBody3D = world.get_node("Player")
var failures := 0
var tick := 1.0 / Engine.physics_ticks_per_second
for i in range(DIRECTIONS):
var ang := TAU * i / DIRECTIONS
var out := Vector3(cos(ang), 0.0, sin(ang))
var start: Vector3 = out * (terrain.play_radius - 12.0)
start.y = terrain.sample_height(start.x, start.z) + 1.0
player.global_position = start
player.velocity = Vector3.ZERO
player.rotation = Vector3(0.0, atan2(-out.x, -out.z), 0.0)
Input.action_press("move_forward")
var escaped := false
var max_d := 0.0
var frames := int(SECONDS_PER_TRY / tick)
for f in range(frames):
if f % 12 == 0:
Input.action_press("jump")
elif f % 12 == 1:
Input.action_release("jump")
await physics_frame
var p := player.global_position
max_d = maxf(max_d, Vector2(p.x, p.z).length())
if not terrain.inside_boundary(p.x, p.z, -1.5) or p.y < terrain.min_height - 5.0:
escaped = true
break
Input.action_release("move_forward")
Input.action_release("jump")
var p := player.global_position
if not escaped and p.y < terrain.water_level - 0.5:
escaped = true # stuck in a pit below the waterline
print("dir %2d (%3d deg): %s max radius %.1f final (%.1f, %.1f, %.1f)" % [
i, int(rad_to_deg(ang)), "FAILED" if escaped else "held", max_d, p.x, p.y, p.z])
if escaped:
failures += 1
print("RESULT: %s (%d/%d directions held)" % ["PASS" if failures == 0 else "FAIL", DIRECTIONS - failures, DIRECTIONS])
quit(1 if failures > 0 else 0)

View File

@@ -0,0 +1 @@
uid://qlm8an6ka4dw

View File

@@ -0,0 +1,39 @@
extends SceneTree
## Terrain edge check: there must be no ground below the inland waterline in the
## band just inside the boundary wall. Ground like that shows up as inland water
## hanging over a drop (the lake water surface stops at the wall) and makes pits
## the player can fall into and not climb out of.
##
## Run: godot --headless --path . --script res://tests/test_terrain_edges.gd
const BAND := 10.0 # metres inside the wall to check
func _initialize() -> void:
var terrain: Node3D = load("res://scripts/world/terrain.gd").new()
root.add_child(terrain) # builds in _ready
_run.call_deferred(terrain)
func _run(terrain: Node3D) -> void:
var bad := 0
var worst := INF
var worst_at := Vector2.ZERO
var half: float = terrain.size * 0.5
var x := -half
while x < half:
var z := -half
while z < half:
if terrain.inside_boundary(x, z, 0.0) and not terrain.inside_boundary(x, z, BAND):
var h: float = terrain.sample_height(x, z)
if h < terrain.water_level + 0.2:
bad += 1
if h < worst:
worst = h
worst_at = Vector2(x, z)
z += 1.0
x += 1.0
if bad > 0:
print("FAIL: %d samples near the wall are below the waterline; lowest %.1f m at (%.0f, %.0f), sea weight %.2f" % [
bad, worst, worst_at.x, worst_at.y, terrain.sea_weight(worst_at.x, worst_at.y)])
else:
print("PASS: ground near the wall stays above the waterline")
quit(1 if bad > 0 else 0)