The Cove's stream: a stream block whose water only runs downhill (spring to the bay), its own water ribbon, trout, on the map; scenery and landmarks keep clear
This commit is contained in:
@@ -44,6 +44,20 @@
|
|||||||
"min_depth": 2.2
|
"min_depth": 2.2
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"stream": {
|
||||||
|
"min_depth": 0.3,
|
||||||
|
"bite_wait": [
|
||||||
|
8.0,
|
||||||
|
18.0
|
||||||
|
],
|
||||||
|
"fish": [
|
||||||
|
{
|
||||||
|
"id": "trout",
|
||||||
|
"weight": 100,
|
||||||
|
"dawn_dusk": 100
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"_doc": "The Cove (docs/WORLDLETS_DESIGN.md §5, W3): the second worldlet, reached from the Valley down the cliff tunnel (J1). Low hills run down to a wide sandy bay with a pier (scripts/world/pier.gd) for sea fishing; a small pond inland; mountains behind. Not a start: you get here on the journey.",
|
"_doc": "The Cove (docs/WORLDLETS_DESIGN.md §5, W3): the second worldlet, reached from the Valley down the cliff tunnel (J1). Low hills run down to a wide sandy bay with a pier (scripts/world/pier.gd) for sea fishing; a small pond inland; a stream from a spring under the mountains down to the bay (a 'stream' block: its points, spring first, in world metres); mountains behind. Not a start: you get here on the journey.",
|
||||||
"id": "cove",
|
"id": "cove",
|
||||||
"name": "Cove",
|
"name": "Cove",
|
||||||
"description": "Low hills running down to a sheltered sandy bay, with a pier out over the water for sea fishing.",
|
"description": "Low hills running down to a sheltered sandy bay, with a pier out over the water for sea fishing.",
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
{"type": "rim", "play_radius": 100.0, "rim_width": 30.0, "mountain_height": 52.0, "rim_warp": 14.0, "wall_frac": 0.45},
|
{"type": "rim", "play_radius": 100.0, "rim_width": 30.0, "mountain_height": 52.0, "rim_warp": 14.0, "wall_frac": 0.45},
|
||||||
{"type": "beach", "angle_deg": 90.0, "half_width_deg": 55.0, "fade_deg": 22.0, "shore_offset": -16.0, "slope": 0.08, "current_out": 22.0, "wall_out": 36.0},
|
{"type": "beach", "angle_deg": 90.0, "half_width_deg": 55.0, "fade_deg": 22.0, "shore_offset": -16.0, "slope": 0.08, "current_out": 22.0, "wall_out": 36.0},
|
||||||
{"type": "lake", "center": [-30.0, -40.0], "radius": 11.0, "water_level": -2.6},
|
{"type": "lake", "center": [-30.0, -40.0], "radius": 11.0, "water_level": -2.6},
|
||||||
|
{"type": "stream", "points": [[46.0, -80.0], [34.0, -48.0], [40.0, -14.0], [30.0, 22.0], [36.0, 52.0], [33.0, 78.0], [33.0, 104.0]], "width": 2.4, "depth": 0.6},
|
||||||
{"type": "ocean_edge"}
|
{"type": "ocean_edge"}
|
||||||
],
|
],
|
||||||
"climate": {"snowline": 42.0},
|
"climate": {"snowline": 42.0},
|
||||||
|
|||||||
@@ -52,9 +52,14 @@ its decisions.
|
|||||||
- Tests: `test_cove` (walk onto the pier, catch a mackerel off its end, cod
|
- Tests: `test_cove` (walk onto the pier, catch a mackerel off its end, cod
|
||||||
only in deep water), and the Cove in `test_terrain_edges` and
|
only in deep water), and the Cove in `test_terrain_edges` and
|
||||||
`test_boundary`.
|
`test_boundary`.
|
||||||
- Still to do: the stream (it needs water that runs downhill, which the
|
- The stream (after 0.4.0): a `stream` block, a line of points from a
|
||||||
one flat water surface can't do: its own ribbon of water, planned with
|
spring under the mountains to the bay. Its water surface is the running
|
||||||
the Cove's sea cave when the tunnel arrives).
|
minimum of the ground along it (so it only ever runs downhill, down to
|
||||||
|
sea level), the bed is cut 0.6 m under that and the banks rise a little
|
||||||
|
above it; its water is its own ribbon mesh (`terrain._stream_mesh()`),
|
||||||
|
murkier than the lake. `water_at()` answers "stream" (trout bite there,
|
||||||
|
`data/fish.json`), the map shows it, and scenery, the den, the barrow and
|
||||||
|
the stones keep clear of it (`terrain.near_stream()`).
|
||||||
|
|
||||||
*As built in W2:*
|
*As built in W2:*
|
||||||
- `lake` blocks can repeat: every lake and pond is its own body
|
- `lake` blocks can repeat: every lake and pond is its own body
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ func _check_site(world: Node3D, p: Vector2) -> Vector3:
|
|||||||
var hs: Array[float] = []
|
var hs: Array[float] = []
|
||||||
for d in [Vector2(-1, -1), Vector2(1, -1), Vector2(-1, 1), Vector2(1, 1), Vector2.ZERO]:
|
for d in [Vector2(-1, -1), Vector2(1, -1), Vector2(-1, 1), Vector2(1, 1), Vector2.ZERO]:
|
||||||
hs.append(terrain.sample_height(p.x + d.x * 4.0, p.y + d.y * 4.0))
|
hs.append(terrain.sample_height(p.x + d.x * 4.0, p.y + d.y * 4.0))
|
||||||
if hs.max() - hs.min() > 2.2 or hs.min() < terrain.water_level + 1.0:
|
if hs.max() - hs.min() > 2.2 or hs.min() < terrain.water_level + 1.0 or terrain.near_stream(p.x, p.y, 7.0):
|
||||||
return Vector3.INF
|
return Vector3.INF
|
||||||
# Trees and boulders in the way are cleared (_clear_area); just avoid
|
# Trees and boulders in the way are cleared (_clear_area); just avoid
|
||||||
# the thickest woods. Counted by position, because their colliders
|
# the thickest woods. Counted by position, because their colliders
|
||||||
|
|||||||
@@ -163,6 +163,8 @@ static func render(w: Node3D) -> Image:
|
|||||||
|
|
||||||
static func ground_colour(t: Node3D, x: float, z: float) -> Color:
|
static func ground_colour(t: Node3D, x: float, z: float) -> Color:
|
||||||
var h: float = t.sample_height(x, z)
|
var h: float = t.sample_height(x, z)
|
||||||
|
if t.near_stream(x, z, 0.3):
|
||||||
|
return Color(0.32, 0.56, 0.66) # (the stream, a little wider than it is so it shows)
|
||||||
var surface: float = t.water_level if t.inside_boundary(x, z, -2.0) else t.sea_level
|
var surface: float = t.water_level if t.inside_boundary(x, z, -2.0) else t.sea_level
|
||||||
if h < surface:
|
if h < surface:
|
||||||
var deep := clampf((surface - h) / 6.0, 0.0, 1.0)
|
var deep := clampf((surface - h) / 6.0, 0.0, 1.0)
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ func _search(max_spread: float, from_spawn: float, from_den: float) -> Vector3:
|
|||||||
hs.append(terrain.sample_height(c.x + cos(b) * rad, c.y + sin(b) * rad))
|
hs.append(terrain.sample_height(c.x + cos(b) * rad, c.y + sin(b) * rad))
|
||||||
var h0: float = terrain.sample_height(c.x, c.y)
|
var h0: float = terrain.sample_height(c.x, c.y)
|
||||||
hs.append(h0)
|
hs.append(h0)
|
||||||
if hs.min() < terrain.water_level + 1.0:
|
if hs.min() < terrain.water_level + 1.0 or terrain.near_stream(c.x, c.y, MOUND_R + 4.0):
|
||||||
continue
|
continue
|
||||||
var spread: float = hs.max() - hs.min()
|
var spread: float = hs.max() - hs.min()
|
||||||
if spread > max_spread:
|
if spread > max_spread:
|
||||||
|
|||||||
@@ -423,6 +423,8 @@ func _scatter_trees() -> void:
|
|||||||
func _on_clifftop(x: float, z: float) -> bool:
|
func _on_clifftop(x: float, z: float) -> bool:
|
||||||
if pier != null and pier.covers(x, z, 2.0):
|
if pier != null and pier.covers(x, z, 2.0):
|
||||||
return true
|
return true
|
||||||
|
if terrain.near_stream(x, z, 1.2):
|
||||||
|
return true # (the stream's bed and banks)
|
||||||
for c in keep_clear:
|
for c in keep_clear:
|
||||||
if Vector2(x - c[0].x, z - c[0].z).length() < float(c[1]):
|
if Vector2(x - c[0].x, z - c[0].z).length() < float(c[1]):
|
||||||
return true
|
return true
|
||||||
@@ -615,7 +617,7 @@ func _place_logs() -> void:
|
|||||||
continue
|
continue
|
||||||
var h: float = terrain.sample_height(x, z)
|
var h: float = terrain.sample_height(x, z)
|
||||||
var n: Vector3 = terrain.sample_normal(x, z)
|
var n: Vector3 = terrain.sample_normal(x, z)
|
||||||
if h < terrain.water_level + 0.6 or n.y < 0.97 or Vector2(x - _spawn.x, z - _spawn.z).length() < 8.0:
|
if h < terrain.water_level + 0.6 or n.y < 0.97 or Vector2(x - _spawn.x, z - _spawn.z).length() < 8.0 or terrain.near_stream(x, z, 1.5):
|
||||||
continue
|
continue
|
||||||
var mi := MeshInstance3D.new()
|
var mi := MeshInstance3D.new()
|
||||||
var shape: Shape3D
|
var shape: Shape3D
|
||||||
@@ -680,7 +682,7 @@ func _scatter_pickups() -> void:
|
|||||||
continue
|
continue
|
||||||
var h: float = terrain.sample_height(x, z)
|
var h: float = terrain.sample_height(x, z)
|
||||||
var n: Vector3 = terrain.sample_normal(x, z)
|
var n: Vector3 = terrain.sample_normal(x, z)
|
||||||
if h < terrain.water_level + 0.3 or n.y < 0.8 or (pier != null and pier.covers(x, z, 1.0)):
|
if h < terrain.water_level + 0.3 or n.y < 0.8 or (pier != null and pier.covers(x, z, 1.0)) or terrain.near_stream(x, z, 0.3):
|
||||||
continue
|
continue
|
||||||
var chance := 1.0
|
var chance := 1.0
|
||||||
match id:
|
match id:
|
||||||
@@ -742,7 +744,7 @@ func _scatter_berry_bushes(root: Node3D) -> void:
|
|||||||
continue
|
continue
|
||||||
var h: float = terrain.sample_height(x, z)
|
var h: float = terrain.sample_height(x, z)
|
||||||
var n: Vector3 = terrain.sample_normal(x, z)
|
var n: Vector3 = terrain.sample_normal(x, z)
|
||||||
if h < terrain.water_level + 0.6 or n.y < 0.85:
|
if h < terrain.water_level + 0.6 or n.y < 0.85 or terrain.near_stream(x, z, 1.0):
|
||||||
continue
|
continue
|
||||||
var f := _forest(x, z)
|
var f := _forest(x, z)
|
||||||
var chance := 0.12 + (1.0 - absf(f - 0.45) * 2.0) * 0.5
|
var chance := 0.12 + (1.0 - absf(f - 0.45) * 2.0) * 0.5
|
||||||
@@ -824,7 +826,7 @@ func _make_grass_tile(k: Vector2i) -> Node3D:
|
|||||||
if not terrain.in_bounds(x, z):
|
if not terrain.in_bounds(x, z):
|
||||||
continue
|
continue
|
||||||
var h: float = terrain.sample_height(x, z)
|
var h: float = terrain.sample_height(x, z)
|
||||||
if h < terrain.water_level + 0.25 or h > terrain.snow_height - 6.0:
|
if h < terrain.water_level + 0.25 or h > terrain.snow_height - 6.0 or terrain.near_stream(x, z, -0.2):
|
||||||
continue
|
continue
|
||||||
var n: Vector3 = terrain.sample_normal(x, z)
|
var n: Vector3 = terrain.sample_normal(x, z)
|
||||||
if n.y < 0.72:
|
if n.y < 0.72:
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ func _find_site() -> Vector3:
|
|||||||
hs.append(terrain.sample_height(c.x + cos(b) * RING_R, c.y + sin(b) * RING_R))
|
hs.append(terrain.sample_height(c.x + cos(b) * RING_R, c.y + sin(b) * RING_R))
|
||||||
var h0: float = terrain.sample_height(c.x, c.y)
|
var h0: float = terrain.sample_height(c.x, c.y)
|
||||||
hs.append(h0)
|
hs.append(h0)
|
||||||
if hs.max() - hs.min() > float(pass_[0]) or hs.min() < terrain.water_level + 1.0:
|
if hs.max() - hs.min() > float(pass_[0]) or hs.min() < terrain.water_level + 1.0 or terrain.near_stream(c.x, c.y, RING_R + 3.0):
|
||||||
continue
|
continue
|
||||||
return Vector3(c.x, h0, c.y)
|
return Vector3(c.x, h0, c.y)
|
||||||
return Vector3.INF
|
return Vector3.INF
|
||||||
|
|||||||
@@ -174,11 +174,18 @@ func _wall_threshold(x: float, z: float) -> float:
|
|||||||
# (data/worldlets/<id>.json "terrain"). Each block takes the ground so far and
|
# (data/worldlets/<id>.json "terrain"). Each block takes the ground so far and
|
||||||
# returns it changed; "hills" starts it. A worldlet can only combine these.
|
# returns it changed; "hills" starts it. A worldlet can only combine these.
|
||||||
|
|
||||||
const BLOCKS := ["hills", "rim", "sea_cliff", "beach", "lake", "ocean_edge"]
|
const BLOCKS := ["hills", "rim", "sea_cliff", "beach", "lake", "stream", "ocean_edge"]
|
||||||
var worldlet_id := "valley"
|
var worldlet_id := "valley"
|
||||||
var worldlet: Dictionary = {} ## the definition, as loaded
|
var worldlet: Dictionary = {} ## the definition, as loaded
|
||||||
var _blocks: Array = [] ## its terrain blocks, in order
|
var _blocks: Array = [] ## its terrain blocks, in order
|
||||||
var lakes: Array = [] ## every "lake" block's {center: Vector2, radius} (W2: several)
|
var lakes: Array = [] ## every "lake" block's {center: Vector2, radius} (W2: several)
|
||||||
|
var stream_path := PackedVector2Array() ## a "stream" block's points, spring first (W3: the Cove's)
|
||||||
|
var stream_width := 2.4
|
||||||
|
var stream_depth := 0.6
|
||||||
|
var _stream_profile := PackedFloat32Array() ## its water surface, every STREAM_STEP m along it
|
||||||
|
var _stream_len := 0.0 ## its length
|
||||||
|
var _stream_end := 0.0 ## where it meets the sea (its water ends there)
|
||||||
|
const STREAM_STEP := 1.0
|
||||||
var _has_sea := true ## a sea cliff (the Valley's)
|
var _has_sea := true ## a sea cliff (the Valley's)
|
||||||
var _has_beach := false ## a beach (W2)
|
var _has_beach := false ## a beach (W2)
|
||||||
var _valley := 0.0 ## the hills before anything else (the sea cliff eases from it)
|
var _valley := 0.0 ## the hills before anything else (the sea cliff eases from it)
|
||||||
@@ -231,6 +238,8 @@ func use_worldlet(id: String) -> String:
|
|||||||
resolution = int(d.get("resolution", resolution))
|
resolution = int(d.get("resolution", resolution))
|
||||||
_blocks = d.get("terrain", [])
|
_blocks = d.get("terrain", [])
|
||||||
lakes = []
|
lakes = []
|
||||||
|
stream_path = PackedVector2Array()
|
||||||
|
_stream_profile = PackedFloat32Array()
|
||||||
_has_sea = _has_block("sea_cliff")
|
_has_sea = _has_block("sea_cliff")
|
||||||
_has_beach = _has_block("beach")
|
_has_beach = _has_block("beach")
|
||||||
for b in _blocks:
|
for b in _blocks:
|
||||||
@@ -252,6 +261,12 @@ func use_worldlet(id: String) -> String:
|
|||||||
cliff_top = float(b.get("cliff_top", cliff_top))
|
cliff_top = float(b.get("cliff_top", cliff_top))
|
||||||
cliff_offset = float(b.get("cliff_offset", cliff_offset))
|
cliff_offset = float(b.get("cliff_offset", cliff_offset))
|
||||||
cliff_drop_width = float(b.get("drop_width", cliff_drop_width))
|
cliff_drop_width = float(b.get("drop_width", cliff_drop_width))
|
||||||
|
"stream":
|
||||||
|
stream_path = PackedVector2Array()
|
||||||
|
for q in b.get("points", []):
|
||||||
|
stream_path.append(Vector2(float(q[0]), float(q[1])))
|
||||||
|
stream_width = float(b.get("width", stream_width))
|
||||||
|
stream_depth = float(b.get("depth", stream_depth))
|
||||||
"beach":
|
"beach":
|
||||||
beach_angle_deg = float(b.get("angle_deg", beach_angle_deg))
|
beach_angle_deg = float(b.get("angle_deg", beach_angle_deg))
|
||||||
beach_half_width_deg = float(b.get("half_width_deg", beach_half_width_deg))
|
beach_half_width_deg = float(b.get("half_width_deg", beach_half_width_deg))
|
||||||
@@ -285,10 +300,14 @@ func _has_block(t: String) -> bool:
|
|||||||
return true
|
return true
|
||||||
return false
|
return false
|
||||||
|
|
||||||
func _height_fn(x: float, z: float) -> float:
|
## The ground at (x, z). `stop_at_stream`: as it is before the stream cuts
|
||||||
|
## its bed (what the stream's water level is worked out from).
|
||||||
|
func _height_fn(x: float, z: float, stop_at_stream: bool = false) -> float:
|
||||||
_ensure_noise()
|
_ensure_noise()
|
||||||
if _blocks.is_empty():
|
if _blocks.is_empty():
|
||||||
use_worldlet(worldlet_id)
|
use_worldlet(worldlet_id)
|
||||||
|
if not stop_at_stream and stream_path.size() >= 2 and _stream_profile.is_empty():
|
||||||
|
_make_stream_profile()
|
||||||
var land := 0.0
|
var land := 0.0
|
||||||
for b in _blocks:
|
for b in _blocks:
|
||||||
match String(b.type):
|
match String(b.type):
|
||||||
@@ -302,6 +321,10 @@ func _height_fn(x: float, z: float) -> float:
|
|||||||
land = _blk_beach(x, z, land)
|
land = _blk_beach(x, z, land)
|
||||||
"lake":
|
"lake":
|
||||||
land = _blk_lake(x, z, land, b._c, b._r)
|
land = _blk_lake(x, z, land, b._c, b._r)
|
||||||
|
"stream":
|
||||||
|
if stop_at_stream:
|
||||||
|
return land
|
||||||
|
land = _blk_stream(x, z, land)
|
||||||
"ocean_edge":
|
"ocean_edge":
|
||||||
land = _blk_ocean_edge(x, z, land)
|
land = _blk_ocean_edge(x, z, land)
|
||||||
return land
|
return land
|
||||||
@@ -354,6 +377,101 @@ func _blk_lake(x: float, z: float, land: float, at: Vector2, r: float) -> float:
|
|||||||
ld += _detail.get_noise_2d(x * 0.5, z * 0.5) * 5.0
|
ld += _detail.get_noise_2d(x * 0.5, z * 0.5) * 5.0
|
||||||
return lerpf(land, water_level - 3.2, smoothstep(r, r * 0.35, ld))
|
return lerpf(land, water_level - 3.2, smoothstep(r, r * 0.35, ld))
|
||||||
|
|
||||||
|
## A stream (W3): runs along its points from a spring to the sea. Its water
|
||||||
|
## surface comes down step by step from the ground at the spring, never
|
||||||
|
## rising (the running minimum of the land along it), so it always runs
|
||||||
|
## downhill. The bed is cut stream_depth below the surface, the banks rise a
|
||||||
|
## little above it (so the water never hangs over lower ground) and blend
|
||||||
|
## into the land, and where the land is high it cuts a little valley.
|
||||||
|
func _make_stream_profile() -> void:
|
||||||
|
_stream_profile = PackedFloat32Array()
|
||||||
|
_stream_len = 0.0
|
||||||
|
for i in range(1, stream_path.size()):
|
||||||
|
_stream_len += stream_path[i - 1].distance_to(stream_path[i])
|
||||||
|
_stream_end = _stream_len
|
||||||
|
var surf := INF
|
||||||
|
var n := int(_stream_len / STREAM_STEP) + 1
|
||||||
|
for i in range(n + 1):
|
||||||
|
var p := _stream_point(i * STREAM_STEP)
|
||||||
|
var g := _height_fn(p.x, p.y, true)
|
||||||
|
surf = minf(surf - 0.01, g - 0.25) if surf < INF else g - 0.25
|
||||||
|
surf = maxf(surf, water_level)
|
||||||
|
_stream_profile.append(surf)
|
||||||
|
if g < water_level - 0.2 and _stream_end >= _stream_len:
|
||||||
|
_stream_end = i * STREAM_STEP # (into the sea: the stream's done)
|
||||||
|
|
||||||
|
## The point `along` metres down the stream.
|
||||||
|
func _stream_point(along: float) -> Vector2:
|
||||||
|
var left := along
|
||||||
|
for i in range(1, stream_path.size()):
|
||||||
|
var seg := stream_path[i - 1].distance_to(stream_path[i])
|
||||||
|
if left <= seg or i == stream_path.size() - 1:
|
||||||
|
return stream_path[i - 1].lerp(stream_path[i], clampf(left / maxf(seg, 0.001), 0.0, 1.0))
|
||||||
|
left -= seg
|
||||||
|
return stream_path[stream_path.size() - 1]
|
||||||
|
|
||||||
|
## How far (x, z) is from the stream's line, and how far along it that is.
|
||||||
|
func _stream_near(x: float, z: float) -> Vector2:
|
||||||
|
var p := Vector2(x, z)
|
||||||
|
var best := INF
|
||||||
|
var best_along := 0.0
|
||||||
|
var run := 0.0
|
||||||
|
for i in range(1, stream_path.size()):
|
||||||
|
var a := stream_path[i - 1]
|
||||||
|
var b := stream_path[i]
|
||||||
|
var ab := b - a
|
||||||
|
var t := clampf((p - a).dot(ab) / maxf(ab.length_squared(), 0.001), 0.0, 1.0)
|
||||||
|
var d := p.distance_to(a + ab * t)
|
||||||
|
if d < best:
|
||||||
|
best = d
|
||||||
|
best_along = run + t * ab.length()
|
||||||
|
run += ab.length()
|
||||||
|
return Vector2(best, best_along)
|
||||||
|
|
||||||
|
func _stream_surface_at(along: float) -> float:
|
||||||
|
var f := clampf(along / STREAM_STEP, 0.0, _stream_profile.size() - 1.001)
|
||||||
|
var i := int(f)
|
||||||
|
return lerpf(_stream_profile[i], _stream_profile[i + 1], f - i)
|
||||||
|
|
||||||
|
func _blk_stream(x: float, z: float, land: float) -> float:
|
||||||
|
if _stream_profile.size() < 2:
|
||||||
|
return land
|
||||||
|
var near := _stream_near(x, z)
|
||||||
|
var hw := stream_width * 0.5
|
||||||
|
var k := near.x / hw
|
||||||
|
if k > 6.0:
|
||||||
|
return land
|
||||||
|
var along := near.y
|
||||||
|
var surf := _stream_surface_at(along)
|
||||||
|
var target: float
|
||||||
|
if k < 1.0:
|
||||||
|
target = surf - stream_depth * (1.0 - k * k)
|
||||||
|
else:
|
||||||
|
target = surf + 0.12 + (k - 1.0) * 0.3
|
||||||
|
# Down to the bed, up to the banks; where the land's high, a gentle
|
||||||
|
# valley rather than a slot.
|
||||||
|
var w := smoothstep(6.0, 1.6, k) if land > target else smoothstep(2.6, 1.6, k)
|
||||||
|
w *= smoothstep(0.0, 5.0, along) # (the spring)
|
||||||
|
w *= 1.0 - smoothstep(_stream_end - 8.0, _stream_end, along) # (into the sea)
|
||||||
|
return lerpf(land, target, w)
|
||||||
|
|
||||||
|
## The stream's water surface at (x, z), or -INF where there's no stream.
|
||||||
|
func stream_surface(x: float, z: float) -> float:
|
||||||
|
if _stream_profile.size() < 2:
|
||||||
|
return -INF
|
||||||
|
var near := _stream_near(x, z)
|
||||||
|
if near.x > stream_width * 0.5 or near.y > _stream_end - 1.0 or near.y < 0.5:
|
||||||
|
return -INF
|
||||||
|
return _stream_surface_at(near.y)
|
||||||
|
|
||||||
|
## Whether (x, z) is in or by the stream (within `margin` m of its banks):
|
||||||
|
## scenery and landmarks keep clear.
|
||||||
|
func near_stream(x: float, z: float, margin: float = 1.0) -> bool:
|
||||||
|
if _stream_profile.size() < 2:
|
||||||
|
return false
|
||||||
|
var near := _stream_near(x, z)
|
||||||
|
return near.x < stream_width * 0.5 + margin and near.y < _stream_end
|
||||||
|
|
||||||
## A beach (W2): on its side of the rim the land eases down to a sandy
|
## A beach (W2): on its side of the rim the land eases down to a sandy
|
||||||
## waterline, then shelves gently into the sea (beach_slope deeper per
|
## waterline, then shelves gently into the sea (beach_slope deeper per
|
||||||
## metre), so you can wade, then swim, until the current turns you back.
|
## metre), so you can wade, then swim, until the current turns you back.
|
||||||
@@ -572,6 +690,11 @@ func _build_water() -> void:
|
|||||||
if absf(sea_level - water_level) > 0.05: # (by a beach the sea is the same surface: no second sheet)
|
if absf(sea_level - water_level) > 0.05: # (by a beach the sea is the same surface: no second sheet)
|
||||||
_add_water("Water", disc, water_level, {})
|
_add_water("Water", disc, water_level, {})
|
||||||
|
|
||||||
|
if _stream_profile.size() >= 2:
|
||||||
|
# (Murkier than the lake, so half a metre of water reads as water, not a path.)
|
||||||
|
_add_water("Stream", _stream_mesh(), 0.0, {"clarity": 0.45,
|
||||||
|
"shallow_col": Color(0.20, 0.42, 0.46), "deep_col": Color(0.05, 0.16, 0.22)})
|
||||||
|
|
||||||
var ocean := PlaneMesh.new()
|
var ocean := PlaneMesh.new()
|
||||||
ocean.size = Vector2(8000.0, 8000.0)
|
ocean.size = Vector2(8000.0, 8000.0)
|
||||||
_add_water("Ocean", ocean, sea_level, {
|
_add_water("Ocean", ocean, sea_level, {
|
||||||
@@ -580,6 +703,40 @@ func _build_water() -> void:
|
|||||||
"clarity": 6.0,
|
"clarity": 6.0,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
## The stream's water: a ribbon down its bed, just under the tops of the
|
||||||
|
## banks, each point at the surface there. It stops where it meets the sea
|
||||||
|
## (whose own water carries on from there).
|
||||||
|
func _stream_mesh() -> ArrayMesh:
|
||||||
|
var st := SurfaceTool.new()
|
||||||
|
st.begin(Mesh.PRIMITIVE_TRIANGLES)
|
||||||
|
var hw := stream_width * 0.5 * 1.05
|
||||||
|
var prev_l := Vector3.ZERO
|
||||||
|
var prev_r := Vector3.ZERO
|
||||||
|
var have := false
|
||||||
|
var along := 0.6
|
||||||
|
var stop := _stream_end - 1.0
|
||||||
|
while along < stop:
|
||||||
|
var p := _stream_point(along)
|
||||||
|
var q := _stream_point(minf(along + 0.5, _stream_len))
|
||||||
|
var dir := (q - p).normalized() if q.distance_to(p) > 0.001 else Vector2(0, 1)
|
||||||
|
var side := Vector2(-dir.y, dir.x) * hw
|
||||||
|
var y := _stream_surface_at(along) + 0.02
|
||||||
|
if y <= water_level + 0.06:
|
||||||
|
break # (level with the sea: its water takes over)
|
||||||
|
var l := Vector3(p.x + side.x, y, p.y + side.y)
|
||||||
|
var r := Vector3(p.x - side.x, y, p.y - side.y)
|
||||||
|
if have:
|
||||||
|
for v in [prev_l, l, prev_r, prev_r, l, r]:
|
||||||
|
st.set_normal(Vector3.UP)
|
||||||
|
st.set_tangent(Plane(1, 0, 0, 1))
|
||||||
|
st.set_uv(Vector2(v.x, v.z) / size + Vector2(0.5, 0.5))
|
||||||
|
st.add_vertex(v)
|
||||||
|
prev_l = l
|
||||||
|
prev_r = r
|
||||||
|
have = true
|
||||||
|
along += 0.75
|
||||||
|
return st.commit()
|
||||||
|
|
||||||
func _add_water(label: String, mesh: Mesh, y: float, params: Dictionary) -> void:
|
func _add_water(label: String, mesh: Mesh, y: float, params: Dictionary) -> void:
|
||||||
var mat := ShaderMaterial.new()
|
var mat := ShaderMaterial.new()
|
||||||
mat.shader = preload("res://shaders/water.gdshader")
|
mat.shader = preload("res://shaders/water.gdshader")
|
||||||
|
|||||||
@@ -1122,6 +1122,9 @@ func _update_grass_cuts() -> void:
|
|||||||
## fishing (and later swimming, boats) work on any world that answers it.
|
## fishing (and later swimming, boats) work on any world that answers it.
|
||||||
func water_at(x: float, z: float) -> Dictionary:
|
func water_at(x: float, z: float) -> Dictionary:
|
||||||
var ground: float = terrain.sample_height(x, z)
|
var ground: float = terrain.sample_height(x, z)
|
||||||
|
var ss: float = terrain.stream_surface(x, z)
|
||||||
|
if ground < ss:
|
||||||
|
return {"type": "stream", "surface": ss, "depth": ss - ground}
|
||||||
var beach: bool = terrain.beach_weight(x, z) > 0.5
|
var beach: bool = terrain.beach_weight(x, z) > 0.5
|
||||||
if terrain.inside_boundary(x, z, -2.0) and ground < terrain.water_level:
|
if terrain.inside_boundary(x, z, -2.0) and ground < terrain.water_level:
|
||||||
if beach and not _in_lake(x, z):
|
if beach and not _in_lake(x, z):
|
||||||
@@ -1161,6 +1164,11 @@ func water_bodies() -> Array:
|
|||||||
"beach":
|
"beach":
|
||||||
out.append({"type": "sea", "surface": terrain.sea_level,
|
out.append({"type": "sea", "surface": terrain.sea_level,
|
||||||
"center": terrain.shore_point(12.0), "radius": 0.0})
|
"center": terrain.shore_point(12.0), "radius": 0.0})
|
||||||
|
"stream":
|
||||||
|
if terrain.stream_path.size() >= 2:
|
||||||
|
var mid: Vector2 = terrain._stream_point(terrain._stream_end * 0.5)
|
||||||
|
out.append({"type": "stream", "surface": terrain._stream_surface_at(terrain._stream_end * 0.5),
|
||||||
|
"center": mid, "radius": terrain.stream_width * 0.5, "path": terrain.stream_path})
|
||||||
return out
|
return out
|
||||||
|
|
||||||
## Whether (x, z) is in one of the lakes or ponds (not the sea off a beach).
|
## Whether (x, z) is in one of the lakes or ponds (not the sea off a beach).
|
||||||
|
|||||||
@@ -62,6 +62,30 @@ func _body() -> void:
|
|||||||
check(world.den != null and world.barrow != null and world.stones != null, "the den, the barrow and the stones found places")
|
check(world.den != null and world.barrow != null and world.stones != null, "the den, the barrow and the stones found places")
|
||||||
check(t.get_node_or_null("Water") == null, "one water surface (the bay is the sea)")
|
check(t.get_node_or_null("Water") == null, "one water surface (the bay is the sea)")
|
||||||
|
|
||||||
|
print("the stream")
|
||||||
|
check(t.stream_path.size() >= 2 and t.get_node_or_null("Stream") != null, "a stream, with water in it")
|
||||||
|
var downhill := true
|
||||||
|
for i in range(1, t._stream_profile.size()):
|
||||||
|
downhill = downhill and t._stream_profile[i] <= t._stream_profile[i - 1] + 0.0001
|
||||||
|
check(downhill and t._stream_profile[0] > t.water_level + 1.5, "running downhill all the way from its spring (%.1f m) to the sea" % t._stream_profile[0])
|
||||||
|
var mid: Vector2 = t._stream_point(t._stream_end * 0.3)
|
||||||
|
var sw: Dictionary = world.water_at(mid.x, mid.y)
|
||||||
|
check(sw.get("type", "") == "stream" and float(sw.depth) > 0.3 and float(sw.depth) < 1.0,
|
||||||
|
"stream water, wading deep (%.1f m)" % float(sw.get("depth", 0.0)))
|
||||||
|
var side: Vector2 = mid + Vector2(t.stream_width * 1.2, 0.0)
|
||||||
|
check(world.water_at(side.x, side.y).is_empty() and t.sample_height(side.x, side.y) > float(sw.surface),
|
||||||
|
"its banks are dry, and above the water")
|
||||||
|
check(DB.water_fishing("stream").get("fish", []).any(func(f): return f.id == "trout"), "trout in the stream")
|
||||||
|
var away := true
|
||||||
|
for p in [world.den.global_position, world.barrow.global_position, world.stones.global_position, world._spawn]:
|
||||||
|
away = away and not t.near_stream(p.x, p.z, 3.0)
|
||||||
|
check(away, "the den, the barrow, the stones and the start are all clear of it")
|
||||||
|
var in_bed := 0
|
||||||
|
for n in world.props.resource_nodes.values():
|
||||||
|
if is_instance_valid(n) and t.near_stream(n.global_position.x, n.global_position.z, 0.0):
|
||||||
|
in_bed += 1
|
||||||
|
check(in_bed == 0, "no trees or boulders standing in it")
|
||||||
|
|
||||||
print("the pier")
|
print("the pier")
|
||||||
var pier: Node3D = world.pier
|
var pier: Node3D = world.pier
|
||||||
check(pier != null and pier.is_in_group("pier"), "a pier")
|
check(pier != null and pier.is_in_group("pier"), "a pier")
|
||||||
|
|||||||
@@ -345,7 +345,7 @@ The Valley is only the start. Beyond it lie other lands, each reached a differen
|
|||||||
|
|
||||||
### The Cove
|
### The Cove
|
||||||
|
|
||||||
Low hills run down to a sheltered sandy bay, with mountains behind and a small pond inland. Wade out from the beach and it gets deeper slowly; swim too far and a strong current carries you back to the shore.
|
Low hills run down to a sheltered sandy bay, with mountains behind, a small pond inland, and a stream that runs from a spring under the mountains down to the sea. You can wade the stream anywhere, and trout bite in it. Wade out from the beach and it gets deeper slowly; swim too far and a strong current carries you back to the shore.
|
||||||
|
|
||||||
- **The pier** runs out over the bay. Fish off its end for **mackerel**, the usual catch. **Cod** are bigger but only bite where the water's at least 2.2 m deep, so cast well out. Both bite best at dawn and dusk, and grill at a campfire, salt or smoke like lake fish.
|
- **The pier** runs out over the bay. Fish off its end for **mackerel**, the usual catch. **Cod** are bigger but only bite where the water's at least 2.2 m deep, so cast well out. Both bite best at dawn and dusk, and grill at a campfire, salt or smoke like lake fish.
|
||||||
- **The sea cave** at the end of the beach is the way back: `E` goes into it and up the same tunnel, arriving at the bottom.
|
- **The sea cave** at the end of the beach is the way back: `E` goes into it and up the same tunnel, arriving at the bottom.
|
||||||
|
|||||||
Reference in New Issue
Block a user