diff --git a/scenes/world.tscn b/scenes/world.tscn index 94d4134..9b0af96 100644 --- a/scenes/world.tscn +++ b/scenes/world.tscn @@ -1,18 +1,10 @@ [gd_scene load_steps=6 format=3 uid="uid://c0world0valheim00"] +[ext_resource type="Script" path="res://scripts/world/world.gd" id="0_world"] [ext_resource type="Script" path="res://scripts/world/terrain.gd" id="1_terrain"] [ext_resource type="Script" path="res://scripts/world/props.gd" id="2_props"] [ext_resource type="PackedScene" path="res://scenes/player.tscn" id="3_player"] - -[sub_resource type="Environment" id="Env_inline"] -background_mode = 2 -ambient_light_source = 3 -ambient_light_color = Color(0.6, 0.66, 0.72, 1) -ambient_light_energy = 0.4 -tonemap_mode = 3 -fog_enabled = true -fog_light_color = Color(0.72, 0.78, 0.82, 1) -fog_density = 0.012 +[ext_resource type="Environment" path="res://scenes/default_env.tres" id="4_env"] [sub_resource type="LabelSettings" id="Label_1"] font_size = 16 @@ -20,9 +12,10 @@ outline_size = 4 outline_color = Color(0, 0, 0, 0.7) [node name="World" type="Node3D"] +script = ExtResource("0_world") [node name="WorldEnvironment" type="WorldEnvironment" parent="."] -environment = SubResource("Env_inline") +environment = ExtResource("4_env") [node name="Sun" type="DirectionalLight3D" parent="."] transform = Transform3D(0.86, -0.35, 0.37, 0, 0.72, 0.69, -0.51, -0.6, 0.62, 0, 40, 0) diff --git a/scripts/world/props.gd b/scripts/world/props.gd index fa6103a..f0478d1 100644 --- a/scripts/world/props.gd +++ b/scripts/world/props.gd @@ -2,18 +2,24 @@ extends Node3D ## M0 mood props — a few low-poly trees and rocks built from primitives in code, ## so the scene looks "alive" with no imported assets. These are placeholders; ## real low-poly models (free asset packs, see docs/ROADMAP.md M0) replace them. +## +## place_on(terrain) is called by world.gd once the terrain exists, so each prop +## sits on the real ground height instead of floating at Y=0. -func _ready() -> void: - # A deterministic scattering so it looks intentional, not random each run. +var _terrain: Node3D + +func place_on(terrain: Node3D) -> void: + _terrain = terrain var rng := RandomNumberGenerator.new() rng.seed = 42 for i in range(18): var x := rng.randf_range(-55.0, 55.0) var z := rng.randf_range(-55.0, 55.0) + var y: float = _terrain.sample_height(x, z) if rng.randf() < 0.65: - _make_tree(Vector3(x, 0, z), rng) + _make_tree(Vector3(x, y, z), rng) else: - _make_rock(Vector3(x, 0, z), rng) + _make_rock(Vector3(x, y, z), rng) func _make_tree(pos: Vector3, rng: RandomNumberGenerator) -> void: var root := Node3D.new() diff --git a/scripts/world/terrain.gd b/scripts/world/terrain.gd index ab68b48..7862701 100644 --- a/scripts/world/terrain.gd +++ b/scripts/world/terrain.gd @@ -2,27 +2,35 @@ extends Node3D ## Terrain generator — M0 placeholder. ## ## Builds ONE heightmap chunk from deterministic noise, so the world already -## looks like rolling ground rather than a flat plane. This is intentionally -## simple: a single chunk, generated at _ready(). Chunk streaming, biomes and -## server-side determinism (see docs/ARCHITECTURE.md §4) come in later milestones. +## looks like rolling ground rather than a flat plane. Single chunk, generated at +## _ready(). Chunk streaming, biomes and server-side determinism (see +## docs/ARCHITECTURE.md §4) come in later milestones. ## ## The mesh is generated in code (no asset needed) so the project runs the moment ## Godot opens it, with nothing to import. +## +## Exposes `sample_height(x, z)` so other systems (player spawn, prop placement) +## can sit exactly on the surface instead of guessing a Y and falling through. + +signal ready_built ## emitted once the mesh + collision exist @export var size: int = 128 # chunk is size x size metres @export var resolution: int = 96 # vertices per side (higher = smoother/heavier) @export var height_scale: float = 10.0 @export var noise_seed: int = 1337 +var _noise: FastNoiseLite +var built: bool = false + func _ready() -> void: _build() func _build() -> void: - var noise := FastNoiseLite.new() - noise.seed = noise_seed - noise.noise_type = FastNoiseLite.TYPE_PERLIN - noise.frequency = 0.012 - noise.fractal_octaves = 4 + _noise = FastNoiseLite.new() + _noise.seed = noise_seed + _noise.noise_type = FastNoiseLite.TYPE_PERLIN + _noise.frequency = 0.012 + _noise.fractal_octaves = 4 var st := SurfaceTool.new() st.begin(Mesh.PRIMITIVE_TRIANGLES) @@ -40,21 +48,21 @@ func _build() -> void: var x1 := (x + 1) * step - half var z1 := (z + 1) * step - half - var p00 := Vector3(x0, _height(noise, x0, z0), z0) - var p10 := Vector3(x1, _height(noise, x1, z0), z0) - var p01 := Vector3(x0, _height(noise, x0, z1), z1) - var p11 := Vector3(x1, _height(noise, x1, z1), z1) + var p00 := Vector3(x0, sample_height(x0, z0), z0) + var p10 := Vector3(x1, sample_height(x1, z0), z0) + var p01 := Vector3(x0, sample_height(x0, z1), z1) + var p11 := Vector3(x1, sample_height(x1, z1), z1) _add_tri(st, p00, p01, p11, half, uv_scale) _add_tri(st, p00, p11, p10, half, uv_scale) # Normals give the lighting its shape. Tangents are only needed for normal - # maps, which this low-poly material doesn't use — so we skip them (and avoid - # the "UVs required to generate tangents" error). + # maps, which this low-poly material doesn't use — so we skip them. st.generate_normals() var mesh := st.commit() var mi := MeshInstance3D.new() + mi.name = "TerrainMesh" mi.mesh = mesh # Simple ground material. Low-poly look = flat-ish colour + lighting does the rest. @@ -64,16 +72,28 @@ func _build() -> void: mi.material_override = mat add_child(mi) - # Collision so the player can walk on it. + # Collision so the player can walk on it. Build the body BEFORE marking built + # so anything spawning on ready_built already has ground under it. var body := StaticBody3D.new() + body.name = "TerrainBody" var shape := CollisionShape3D.new() - var col := mesh.create_trimesh_shape() - shape.shape = col + shape.shape = mesh.create_trimesh_shape() body.add_child(shape) add_child(body) -func _height(noise: FastNoiseLite, x: float, z: float) -> float: - return noise.get_noise_2d(x, z) * height_scale + built = true + ready_built.emit() + +## World-space ground height at (x, z). Safe to call before or after _build() +## because it lazily creates the noise object. +func sample_height(x: float, z: float) -> float: + if _noise == null: + _noise = FastNoiseLite.new() + _noise.seed = noise_seed + _noise.noise_type = FastNoiseLite.TYPE_PERLIN + _noise.frequency = 0.012 + _noise.fractal_octaves = 4 + return _noise.get_noise_2d(x, z) * height_scale func _add_tri(st: SurfaceTool, a: Vector3, b: Vector3, c: Vector3, half: float, uv_scale: float) -> void: st.set_uv(Vector2((a.x + half) * uv_scale, (a.z + half) * uv_scale)) diff --git a/scripts/world/world.gd b/scripts/world/world.gd new file mode 100644 index 0000000..c5ff5be --- /dev/null +++ b/scripts/world/world.gd @@ -0,0 +1,31 @@ +extends Node3D +## World controller — M0. +## +## Owns spawn ordering so the player never falls through ungenerated ground: +## waits for the terrain to finish building, then drops the player onto the +## surface height and lets gravity settle them. Also tells the props where the +## ground is so they sit on it instead of floating. + +@onready var terrain: Node3D = $Terrain +@onready var props: Node3D = $Props +@onready var player: CharacterBody3D = $Player + +func _ready() -> void: + # Freeze the player until the ground exists. + player.set_physics_process(false) + if terrain.built: + _on_terrain_ready() + else: + terrain.ready_built.connect(_on_terrain_ready) + +func _on_terrain_ready() -> void: + # Let props place themselves on the surface. + if props.has_method("place_on"): + props.place_on(terrain) + + # Spawn the player on the ground at (0,0), a little above so they settle down + # onto the collision rather than starting embedded in it. + var h: float = terrain.sample_height(0.0, 0.0) + player.global_position = Vector3(0.0, h + 2.0, 0.0) + player.velocity = Vector3.ZERO + player.set_physics_process(true)