W0: terrain from worldlet building blocks (Valley in data/worldlets, identical heights), WorldInfo API, worldlet in saves and servers; the cliff tunnel plan
This commit is contained in:
17
data/worldlets/valley.json
Normal file
17
data/worldlets/valley.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"_doc": "A worldlet (docs/WORLDLETS_DESIGN.md): its size, and its terrain as building blocks applied in order (scripts/world/terrain.gd: hills, rim, sea_cliff, lake, ocean_edge). The numbers here are the Valley exactly as it was hand-coded: tests/test_worldlets.gd checks it still makes the same height grid.",
|
||||
"id": "valley",
|
||||
"name": "Valley",
|
||||
"description": "A sheltered valley ringed by mountains, with a lake in the middle and sea cliffs on one side.",
|
||||
"size": 320,
|
||||
"resolution": 256,
|
||||
"terrain": [
|
||||
{"type": "hills", "height_scale": 9.0, "detail": 1.4},
|
||||
{"type": "rim", "play_radius": 100.0, "rim_width": 30.0, "mountain_height": 58.0, "rim_warp": 16.0, "wall_frac": 0.45},
|
||||
{"type": "sea_cliff", "angle_deg": -75.0, "half_width_deg": 38.0, "fade_deg": 22.0, "sea_level": -24.0, "cliff_top": 0.5, "cliff_offset": 4.0, "drop_width": 6.0},
|
||||
{"type": "lake", "center": [14.0, -46.0], "radius": 24.0, "water_level": -2.6},
|
||||
{"type": "ocean_edge"}
|
||||
],
|
||||
"climate": {"snowline": 40.0},
|
||||
"next": [{"to": "cove", "by": "tunnel", "where": "sea_cliff"}]
|
||||
}
|
||||
@@ -1,7 +1,23 @@
|
||||
# Worldlets design
|
||||
|
||||
Status: **proposal**. Nothing here is built yet, apart from the first piece
|
||||
of groundwork (`world.water_at()` and `data/fish.json`, see §6).
|
||||
Status: **W0 built**; the rest is planned. The journey between worldlets is
|
||||
§10.
|
||||
|
||||
*As built in W0:*
|
||||
- `data/worldlets/valley.json`: the Valley as data, its terrain as five
|
||||
building blocks in order (`hills`, `rim`, `sea_cliff`, `lake`,
|
||||
`ocean_edge`) with every number that used to be hard-coded.
|
||||
- `terrain.gd` runs a worldlet's blocks (`use_worldlet()`); each block is
|
||||
the old code's arithmetic, unchanged. `tests/test_worldlets.gd` checks
|
||||
four seeds give exactly the same height grid as before (SHA-256 of the
|
||||
grid, taken from the old code).
|
||||
- The worldlet is chosen like the seed: a server's (it sends its real one),
|
||||
else the save's `meta.json` "worldlet", else the Valley (worlds from
|
||||
before W0). New worlds and servers record it.
|
||||
- The WorldInfo API on the world: `worldlet_id()`, `water_bodies()`,
|
||||
`climate()`, `crossings()` (the planned ways on), beside `water_at()`.
|
||||
Systems still read the terrain's numbers directly; they move over to
|
||||
these as each new worldlet needs them to differ (W1-W3).
|
||||
|
||||
## 1. What a worldlet is
|
||||
A **worldlet** is a small, complete world type: a ~320 m playable area with
|
||||
@@ -224,6 +240,41 @@ you've learned and gathered there:
|
||||
- **Through:** the far end opens onto the next worldlet. Walking out loads
|
||||
it (a short fade, "The Cove"), with your bag, cloak and stats as they are.
|
||||
|
||||
### The first tunnel: down the sea cliff to the Cove
|
||||
The Valley's sea side ends at a sheer cliff, its top about level with the
|
||||
valley and the sea some 25 m below. The Cove is down there, so the first
|
||||
tunnel goes **down**, inside the cliff, from the clifftop to the shore.
|
||||
- **The seam:** a cracked outcrop on the clifftop strip near the edge (the
|
||||
sea_cliff block marks where), with old pick marks and loose stones at its
|
||||
foot. It can't be dug until you have the antler pickaxe (tier 3); a lesser
|
||||
pick just chips it ("Too hard: you need a better pick").
|
||||
- **Digging down:** the tunnel is about six stretches, each a few metres on
|
||||
and a few down: a sloping passage that turns back on itself like a
|
||||
stair, so the 25 m drop fits inside the cliff. A stretch takes many pick
|
||||
blows (stamina, and wear on the pick) to open.
|
||||
- **Props:** each new stretch needs a **timber prop** (2 wood) set at its
|
||||
mouth before you can dig the next. Leave one out and the face won't take
|
||||
another blow ("It's loose: prop it first"); nothing caves in on you, so
|
||||
it's a cost, not a trap. That's about 12 wood all the way down.
|
||||
- **Light:** the tunnel has no daylight at all. Every stretch is pitch dark
|
||||
until you bring light: a torch in your hand works, but your hands are full
|
||||
with the pick, so the way to do it is to set **candles and torch stands**
|
||||
as you go (tallow candles from bear fat, torch stands from wood). Placed
|
||||
lights stay, so the way is lit for the trip back and for friends.
|
||||
- **The bottom:** the last stretch breaks out into a sea cave, its mouth
|
||||
opening on the Cove's shingle beach. Walking out of the mouth is the
|
||||
crossing: a fade, then the Cove, arriving at the cave's Cove end. The way
|
||||
back starts there and climbs the same tunnel.
|
||||
- **What's saved:** the tunnel belongs to the Valley's save: how far it's
|
||||
dug, the props, the lights. On a server it's shared: anyone can dig on
|
||||
from where someone stopped, and the lights are everyone's.
|
||||
- **How it's built:** the tunnel is its own small space (a "between" scene
|
||||
next to the worldlet), made from a kit of rock pieces (straight, turn,
|
||||
slope down, the cave mouth) laid out from the seed, with box-shaped
|
||||
colliders. The heightmap can't have holes or overhangs, so the passage is
|
||||
never cut into the terrain itself: you walk into the seam and you're in
|
||||
the tunnel scene.
|
||||
|
||||
### Why go back
|
||||
- **Each worldlet has things the others lack.** Clay and salt only in the
|
||||
Valley and Cove; little wood in the Highlands; no bears outside the
|
||||
|
||||
@@ -6,7 +6,7 @@ runnable=true
|
||||
dedicated_server=false
|
||||
custom_features=""
|
||||
export_filter="all_resources"
|
||||
include_filter="data/*.json"
|
||||
include_filter="data/*.json, data/worldlets/*.json"
|
||||
exclude_filter="tests/*, docs/*, tools/*, server/*"
|
||||
export_path="build/macos/Emberwild.zip"
|
||||
patches=PackedStringArray()
|
||||
@@ -64,7 +64,7 @@ runnable=true
|
||||
dedicated_server=false
|
||||
custom_features=""
|
||||
export_filter="all_resources"
|
||||
include_filter="data/*.json"
|
||||
include_filter="data/*.json, data/worldlets/*.json"
|
||||
exclude_filter="tests/*, docs/*, tools/*, server/*"
|
||||
export_path="build/windows/Emberwild/Emberwild.exe"
|
||||
patches=PackedStringArray()
|
||||
|
||||
@@ -56,7 +56,7 @@ func create_world(world_name: String, seed: int, options: Dictionary = {}) -> St
|
||||
var now := Time.get_unix_time_from_system()
|
||||
var meta := {
|
||||
"name": world_name, "seed": seed, "created": now, "last_played": now,
|
||||
"play_time": 0.0, "version": VERSION,
|
||||
"play_time": 0.0, "version": VERSION, "worldlet": "valley",
|
||||
}
|
||||
for k in options:
|
||||
meta[k] = options[k]
|
||||
@@ -65,7 +65,7 @@ func create_world(world_name: String, seed: int, options: Dictionary = {}) -> St
|
||||
|
||||
## The world slot called exactly `id` (a server's world), made with `seed` if
|
||||
## it doesn't exist yet. An existing world keeps its own seed.
|
||||
func open_or_create(id: String, seed: int) -> String:
|
||||
func open_or_create(id: String, seed: int, worldlet: String = "valley") -> String:
|
||||
if not read_meta(id).is_empty():
|
||||
var had := int(read_meta(id).get("seed", seed))
|
||||
if had != seed:
|
||||
@@ -75,7 +75,7 @@ func open_or_create(id: String, seed: int) -> String:
|
||||
var now := Time.get_unix_time_from_system()
|
||||
_write_json(_dir(id).path_join("meta.json"), {
|
||||
"name": id, "seed": seed, "created": now, "last_played": now,
|
||||
"play_time": 0.0, "version": VERSION,
|
||||
"play_time": 0.0, "version": VERSION, "worldlet": worldlet,
|
||||
})
|
||||
return id
|
||||
|
||||
|
||||
@@ -638,7 +638,7 @@ func _c2s_proof(sig: PackedByteArray) -> void:
|
||||
print("[server] %s joined (id %s%s, %d/%d online)" % [name, id, ", admin" if p.admin else "",
|
||||
players.size(), int(config.v("max_players"))])
|
||||
# (The world's own seed: an existing world keeps the seed it was made with.)
|
||||
var world := {"seed": int(_terrain.noise_seed), "worldlet": String(config.v("worldlet")), "spawn": start, "yaw": yaw,
|
||||
var world := {"seed": int(_terrain.noise_seed), "worldlet": String(_terrain.worldlet_id), "spawn": start, "yaw": yaw,
|
||||
"island_spawn": _spawn, "day": _world.day_night.day, "time": _world.day_night.time, "server_time": server_time,
|
||||
"weather": _world.weather.override}
|
||||
_s2c_welcome.rpc_id(peer, {"you": peer, "server": config.public_info(), "players": players, "world": world})
|
||||
|
||||
@@ -49,7 +49,7 @@ func open_server_world(config: RefCounted) -> Node3D:
|
||||
var id := String(config.v("world")).validate_filename()
|
||||
if id.is_empty():
|
||||
id = "server-world"
|
||||
Saves.current_id = Saves.open_or_create(id, int(config.v("seed")))
|
||||
Saves.current_id = Saves.open_or_create(id, int(config.v("seed")), String(config.v("worldlet")))
|
||||
backup("start-up") # (the state it's about to load, before anyone changes it)
|
||||
_backup_left = maxf(float(config.v("backup_hours")), 0.25) * 3600.0
|
||||
var w: Node3D = load("res://scenes/world.tscn").instantiate()
|
||||
|
||||
@@ -75,8 +75,15 @@ func _ready() -> void:
|
||||
var saves := get_node_or_null("/root/Saves")
|
||||
if net != null and net.state == "connected" and net.world_info.has("seed"):
|
||||
noise_seed = int(net.world_info.seed)
|
||||
worldlet_id = String(net.world_info.get("worldlet", worldlet_id))
|
||||
elif saves != null and saves.has_world():
|
||||
noise_seed = saves.world_seed()
|
||||
# (worlds from before worldlets have none: they're the Valley)
|
||||
worldlet_id = String(saves.read_meta(saves.current_id).get("worldlet", "valley"))
|
||||
var why := use_worldlet(worldlet_id)
|
||||
if not why.is_empty():
|
||||
push_error("[terrain] %s: using the Valley" % why)
|
||||
use_worldlet("valley")
|
||||
_build()
|
||||
|
||||
# --- height function --------------------------------------------------------
|
||||
@@ -135,14 +142,114 @@ func _sea_w(x: float, z: float) -> float:
|
||||
func _wall_threshold(x: float, z: float) -> float:
|
||||
return lerpf(_wall_d(), _cliff_d() - 1.0, _sea_w(x, z))
|
||||
|
||||
# --- worldlets (docs/WORLDLETS_DESIGN.md, W0) ---------------------------------------
|
||||
# A worldlet's terrain is a list of building blocks applied in order
|
||||
# (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.
|
||||
|
||||
const BLOCKS := ["hills", "rim", "sea_cliff", "lake", "ocean_edge"]
|
||||
var worldlet_id := "valley"
|
||||
var worldlet: Dictionary = {} ## the definition, as loaded
|
||||
var _blocks: Array = [] ## its terrain blocks, in order
|
||||
var _valley := 0.0 ## the hills before anything else (the sea cliff eases from it)
|
||||
var _detail_k := 1.4
|
||||
|
||||
## The definition of worldlet `id` (data/worldlets/<id>.json), or {} if none.
|
||||
static func load_worldlet(id: String) -> Dictionary:
|
||||
var p := "res://data/worldlets/%s.json" % id
|
||||
if not FileAccess.file_exists(p):
|
||||
return {}
|
||||
var d: Variant = JSON.parse_string(FileAccess.get_file_as_string(p))
|
||||
return d if d is Dictionary else {}
|
||||
|
||||
## Every worldlet there is: [{id, name, description}].
|
||||
static func worldlets() -> Array:
|
||||
var out: Array = []
|
||||
for f in DirAccess.get_files_at("res://data/worldlets"):
|
||||
if f.ends_with(".json"):
|
||||
var d := load_worldlet(f.get_basename())
|
||||
if not d.is_empty():
|
||||
out.append({"id": String(d.id), "name": String(d.get("name", d.id)), "description": String(d.get("description", ""))})
|
||||
return out
|
||||
|
||||
## Takes on worldlet `id`: its size and every block's numbers. Returns ""
|
||||
## on success, else what's wrong (an unknown worldlet or block keeps the old
|
||||
## settings, so the game still has an island).
|
||||
func use_worldlet(id: String) -> String:
|
||||
var d := load_worldlet(id)
|
||||
if d.is_empty():
|
||||
return "no worldlet '%s'" % id
|
||||
for b in d.get("terrain", []):
|
||||
if not (b is Dictionary) or not String(b.get("type", "")) in BLOCKS:
|
||||
return "worldlet '%s': unknown terrain block %s" % [id, str(b)]
|
||||
worldlet_id = id
|
||||
worldlet = d
|
||||
size = int(d.get("size", size))
|
||||
resolution = int(d.get("resolution", resolution))
|
||||
_blocks = d.get("terrain", [])
|
||||
for b in _blocks:
|
||||
match String(b.type):
|
||||
"hills":
|
||||
height_scale = float(b.get("height_scale", height_scale))
|
||||
_detail_k = float(b.get("detail", _detail_k))
|
||||
"rim":
|
||||
play_radius = float(b.get("play_radius", play_radius))
|
||||
rim_width = float(b.get("rim_width", rim_width))
|
||||
mountain_height = float(b.get("mountain_height", mountain_height))
|
||||
rim_warp = float(b.get("rim_warp", rim_warp))
|
||||
wall_frac = float(b.get("wall_frac", wall_frac))
|
||||
"sea_cliff":
|
||||
sea_angle_deg = float(b.get("angle_deg", sea_angle_deg))
|
||||
sea_half_width_deg = float(b.get("half_width_deg", sea_half_width_deg))
|
||||
sea_fade_deg = float(b.get("fade_deg", sea_fade_deg))
|
||||
sea_level = float(b.get("sea_level", sea_level))
|
||||
cliff_top = float(b.get("cliff_top", cliff_top))
|
||||
cliff_offset = float(b.get("cliff_offset", cliff_offset))
|
||||
cliff_drop_width = float(b.get("drop_width", cliff_drop_width))
|
||||
"lake":
|
||||
var c: Array = b.get("center", [lake_center.x, lake_center.y])
|
||||
lake_center = Vector2(float(c[0]), float(c[1]))
|
||||
lake_radius = float(b.get("radius", lake_radius))
|
||||
water_level = float(b.get("water_level", water_level))
|
||||
snow_height = float(d.get("climate", {}).get("snowline", snow_height))
|
||||
return ""
|
||||
|
||||
func _has_block(t: String) -> bool:
|
||||
for b in _blocks:
|
||||
if String(b.type) == t:
|
||||
return true
|
||||
return false
|
||||
|
||||
func _height_fn(x: float, z: float) -> float:
|
||||
_ensure_noise()
|
||||
var valley := _noise.get_noise_2d(x, z) * height_scale
|
||||
valley += _detail.get_noise_2d(x, z) * 1.4
|
||||
var d := _rim_d(x, z)
|
||||
if _blocks.is_empty():
|
||||
use_worldlet(worldlet_id)
|
||||
var land := 0.0
|
||||
for b in _blocks:
|
||||
match String(b.type):
|
||||
"hills":
|
||||
land = _blk_hills(x, z)
|
||||
"rim":
|
||||
land = _blk_rim(x, z, land)
|
||||
"sea_cliff":
|
||||
land = _blk_sea_cliff(x, z, land)
|
||||
"lake":
|
||||
land = _blk_lake(x, z, land)
|
||||
"ocean_edge":
|
||||
land = _blk_ocean_edge(x, z, land)
|
||||
return land
|
||||
|
||||
# Mountain rim.
|
||||
var land := valley
|
||||
## Rolling hills: the ground everything else is built on.
|
||||
func _blk_hills(x: float, z: float) -> float:
|
||||
var valley := _noise.get_noise_2d(x, z) * height_scale
|
||||
valley += _detail.get_noise_2d(x, z) * _detail_k
|
||||
_valley = valley
|
||||
return valley
|
||||
|
||||
## The mountain rim: rises from play_radius, stepped into ledges.
|
||||
func _blk_rim(x: float, z: float, land: float) -> float:
|
||||
var valley := _valley
|
||||
var d := _rim_d(x, z)
|
||||
var t := smoothstep(play_radius, play_radius + rim_width, d)
|
||||
if t > 0.0:
|
||||
var ridge := 1.0 - absf(_ridge.get_noise_2d(x, z))
|
||||
@@ -153,7 +260,12 @@ func _height_fn(x: float, z: float) -> float:
|
||||
var stepped := floorf(m / band) * band + smoothstep(0.35, 0.65, fposmod(m, band) / band) * band
|
||||
m = lerpf(m, stepped, 0.45 * t)
|
||||
land = valley * (1.0 - t * 0.6) + m
|
||||
return land
|
||||
|
||||
## The sea side of the rim: the valley eases to a cliff top, then drops sheer.
|
||||
func _blk_sea_cliff(x: float, z: float, land: float) -> float:
|
||||
var valley := _valley
|
||||
var d := _rim_d(x, z)
|
||||
# Sea side: the valley eases to a cliff top, then drops sheer to the seabed.
|
||||
# The edge sits 1 m past the invisible wall at every angle, including the
|
||||
# headlands where the wall moves outward. (With the edge fixed at _cliff_d(),
|
||||
@@ -167,14 +279,17 @@ func _height_fn(x: float, z: float) -> float:
|
||||
var seabed := sea_level - 7.0 - minf(maxf(d - edge, 0.0), 60.0) * 0.08 + _detail.get_noise_2d(x, z) * 2.0
|
||||
coast = lerpf(coast, seabed, smoothstep(edge, edge + cliff_drop_width, d))
|
||||
land = lerpf(land, coast, sw)
|
||||
return land
|
||||
|
||||
# Lake basin: blend the ground down to a bed below the waterline.
|
||||
## A lake basin: the ground blends down to a bed below the waterline.
|
||||
func _blk_lake(x: float, z: float, land: float) -> float:
|
||||
var ld := Vector2(x - lake_center.x, z - lake_center.y).length()
|
||||
ld += _detail.get_noise_2d(x * 0.5, z * 0.5) * 5.0
|
||||
land = lerpf(land, water_level - 3.2, smoothstep(lake_radius, lake_radius * 0.35, ld))
|
||||
return lerpf(land, water_level - 3.2, smoothstep(lake_radius, lake_radius * 0.35, ld))
|
||||
|
||||
# Ocean all round the chunk edge, so the world is an island and the edge of
|
||||
# the mesh is always hidden under deep water.
|
||||
## Ocean all round the chunk edge, so the world is an island and the edge of
|
||||
## the mesh is always hidden under deep water.
|
||||
func _blk_ocean_edge(x: float, z: float, land: float) -> float:
|
||||
var e := smoothstep(size * 0.5 - 28.0, size * 0.5 - 4.0, sqrt(x * x + z * z))
|
||||
return lerpf(land, sea_level - 10.0 + _detail.get_noise_2d(x, z) * 2.0, e)
|
||||
|
||||
|
||||
@@ -895,6 +895,44 @@ func water_at(x: float, z: float) -> Dictionary:
|
||||
return {"type": "sea", "surface": terrain.sea_level, "depth": terrain.sea_level - ground}
|
||||
return {}
|
||||
|
||||
# --- the worldlet (docs/WORLDLETS_DESIGN.md: the WorldInfo API) --------------------
|
||||
# What a system outside the terrain should ask about this place, rather than
|
||||
# reading the terrain's numbers: which worldlet it is, its water, its climate,
|
||||
# and its ways out. (W0 adds these; systems move over to them as each worldlet
|
||||
# needs them to be different.)
|
||||
|
||||
## This worldlet's id ("valley") and its whole definition.
|
||||
func worldlet_id() -> String:
|
||||
return terrain.worldlet_id
|
||||
|
||||
func worldlet() -> Dictionary:
|
||||
return terrain.worldlet
|
||||
|
||||
## Its bodies of water: [{"type", "surface", "center" (x, z; the sea's is a
|
||||
## point on its coast), "radius" (a lake's; the sea's is 0)}].
|
||||
func water_bodies() -> Array:
|
||||
var out: Array = []
|
||||
for b in terrain._blocks:
|
||||
match String(b.type):
|
||||
"lake":
|
||||
out.append({"type": "lake", "surface": terrain.water_level,
|
||||
"center": terrain.lake_center, "radius": terrain.lake_radius})
|
||||
"sea_cliff":
|
||||
out.append({"type": "sea", "surface": terrain.sea_level,
|
||||
"center": terrain.coast_point(terrain.sea_angle_deg, 20.0), "radius": 0.0})
|
||||
return out
|
||||
|
||||
## Its climate: {"snowline": m, ...} (more as worldlets differ).
|
||||
func climate() -> Dictionary:
|
||||
var c: Dictionary = terrain.worldlet.get("climate", {}).duplicate()
|
||||
c["snowline"] = terrain.snow_height
|
||||
return c
|
||||
|
||||
## Its ways on to other worldlets (the journey): [{"to", "by", "where"}].
|
||||
## Planned in the definition; travel comes with J1.
|
||||
func crossings() -> Array:
|
||||
return terrain.worldlet.get("next", [])
|
||||
|
||||
# --- multiplayer -----------------------------------------------------------------
|
||||
|
||||
## Joined a server: start where it says, follow its clock, show the others.
|
||||
|
||||
@@ -9,7 +9,7 @@ if [ ! -f .godot/global_script_class_cache.cfg ]; then
|
||||
echo "Importing the project first (fresh copy)..."
|
||||
godot --headless --path . --import >/dev/null 2>&1
|
||||
fi
|
||||
for t in test_survival_core test_survival_stats test_save_load test_weather_building test_creatures_combat test_wolves test_alpha test_bear test_thieves test_barrow test_boss test_den_sites test_fishing_diet test_food test_hazards test_iron test_stone test_net_lobby test_net_world test_net_shared test_net_life test_net_boss test_net_rules test_host test_tutorial test_tutorial_full test_hints test_updates test_audio test_items_more test_terrain_edges test_boundary; do
|
||||
for t in test_survival_core test_survival_stats test_save_load test_weather_building test_creatures_combat test_wolves test_alpha test_bear test_thieves test_barrow test_boss test_den_sites test_fishing_diet test_food test_hazards test_iron test_stone test_net_lobby test_net_world test_net_shared test_net_life test_net_boss test_net_rules test_host test_tutorial test_tutorial_full test_hints test_updates test_audio test_items_more test_worldlets test_terrain_edges test_boundary; do
|
||||
extra=""
|
||||
[ "$t" = "test_boundary" ] && extra="--fixed-fps 30"
|
||||
# A suite that waits on a world that failed to load would hang forever,
|
||||
|
||||
89
tests/test_worldlets.gd
Normal file
89
tests/test_worldlets.gd
Normal file
@@ -0,0 +1,89 @@
|
||||
extends SceneTree
|
||||
## W0 (docs/WORLDLETS_DESIGN.md): the terrain is built from a worldlet's
|
||||
## blocks (data/worldlets/<id>.json), and the Valley made that way is the same
|
||||
## island, height for height, as the hand-coded one it replaced (the hashes
|
||||
## below were taken from the old code). Also: the world answers the WorldInfo
|
||||
## questions, a new save remembers its worldlet, an old save without one is
|
||||
## the Valley, and a bad worldlet falls back to the Valley.
|
||||
## Run: godot --headless --path . --script res://tests/test_worldlets.gd
|
||||
|
||||
## SHA-256 of the height grid, from the terrain before W0 (commit f4df2bc).
|
||||
const GRID_HASHES := {
|
||||
1337: "c87679a4ed30bff0c4be1caacda735454ad23f690ada28e7179667a9a7d62aa7",
|
||||
777: "2edef348df09bf1f84ed3f5b0921f28fe54b3c827a698fac0a7d50fcdb95a24e",
|
||||
42: "801430858909739fc8cf46694eee35c0cffa86ff1a9ad1ed379f1dfdea4b133c",
|
||||
9001: "7d509d2508395a0e27e93a3384ac1e63323d588ef3cd1262f1e3f0c1c3e3254b",
|
||||
}
|
||||
|
||||
var _fails := 0
|
||||
var _finished := false
|
||||
|
||||
func check(cond: bool, what: String) -> void:
|
||||
print((" ok " if cond else " FAIL ") + what)
|
||||
if not cond:
|
||||
_fails += 1
|
||||
|
||||
func _initialize() -> void:
|
||||
_run.call_deferred()
|
||||
|
||||
func _run() -> void:
|
||||
var S: Node = root.get_node("Saves")
|
||||
S.root_dir = "user://test_worlds"
|
||||
for w in S.list_worlds():
|
||||
S.delete_world(w.id)
|
||||
await _body(S)
|
||||
check(_finished, "test ran to the end (no script errors)")
|
||||
for w in S.list_worlds():
|
||||
S.delete_world(w.id)
|
||||
DirAccess.remove_absolute(S.root_dir)
|
||||
print("RESULT: %s (%d failures)" % ["PASS" if _fails == 0 else "FAIL", _fails])
|
||||
quit(1 if _fails > 0 else 0)
|
||||
|
||||
func _grid_hash(t: Node) -> String:
|
||||
var h := HashingContext.new()
|
||||
h.start(HashingContext.HASH_SHA256)
|
||||
h.update(t._heights.to_byte_array())
|
||||
return h.finish().hex_encode()
|
||||
|
||||
func _body(S: Node) -> void:
|
||||
var T: Script = load("res://scripts/world/terrain.gd")
|
||||
print("the Valley, from data")
|
||||
var list: Array = T.worldlets()
|
||||
check(list.any(func(w): return w.id == "valley"), "the Valley is a worldlet (%s)" % [list.map(func(w): return w.id)])
|
||||
var def: Dictionary = T.load_worldlet("valley")
|
||||
check(def.get("terrain", []).size() == 5, "built from 5 blocks: hills, rim, sea cliff, lake, ocean edge")
|
||||
for s in GRID_HASHES:
|
||||
var t: Node3D = T.new()
|
||||
t.noise_seed = s
|
||||
root.add_child(t)
|
||||
await process_frame
|
||||
check(_grid_hash(t) == GRID_HASHES[s], "seed %d: the same island, height for height" % s)
|
||||
t.queue_free()
|
||||
await process_frame
|
||||
var bad: Node3D = T.new()
|
||||
check(not bad.use_worldlet("nowhere").is_empty() and bad.worldlet_id == "valley", "an unknown worldlet is refused (the Valley stays)")
|
||||
bad.free()
|
||||
|
||||
print("in a world")
|
||||
var id: String = S.create_world("W0", 1337, {})
|
||||
check(String(S.read_meta(id).get("worldlet", "")) == "valley", "a new world remembers its worldlet")
|
||||
var meta: Dictionary = S.read_meta(id)
|
||||
meta.erase("worldlet")
|
||||
S._write_json(S._dir(id).path_join("meta.json"), meta)
|
||||
S.current_id = id
|
||||
var world: Node3D = load("res://scenes/world.tscn").instantiate()
|
||||
root.add_child(world)
|
||||
for _i in range(5):
|
||||
await physics_frame
|
||||
check(world.worldlet_id() == "valley", "an old world with no worldlet is the Valley")
|
||||
var water: Array = world.water_bodies()
|
||||
check(water.size() == 2 and water.any(func(w): return w.type == "lake") and water.any(func(w): return w.type == "sea"),
|
||||
"its water: a lake and the sea")
|
||||
var lake: Dictionary = water.filter(func(w): return w.type == "lake")[0]
|
||||
var lp: Vector2 = lake.center
|
||||
check(world.water_at(lp.x, lp.y).get("type", "") == "lake", "(and the lake is where it says)")
|
||||
check(absf(float(world.climate().snowline) - 40.0) < 0.01, "its snowline: 40 m")
|
||||
check(world.crossings().any(func(c): return c.to == "cove" and c.by == "tunnel"), "its way on: a tunnel to the Cove (planned)")
|
||||
world.queue_free()
|
||||
await process_frame
|
||||
_finished = true
|
||||
Reference in New Issue
Block a user