J1: the cliff tunnel (seam, six propped stretches, dark, lights), travel Valley <-> Cove through the sea cave, a save file per worldlet; New World has no Where (--dev-worldlets to try others)
This commit is contained in:
@@ -15,6 +15,8 @@ const DAY_BIAS := 0.2
|
||||
var day := 1
|
||||
var time := 8.0 / 24.0
|
||||
var frozen := false ## title-screen backdrop: hold a fixed time
|
||||
var underground := 0.0 ## 0..1: how far into the tunnel you are (no daylight; world.gd sets it)
|
||||
const AERIAL := 0.55 ## the fog's sky tint with distance (scenes/default_env.tres)
|
||||
|
||||
var sun: DirectionalLight3D
|
||||
var moon: DirectionalLight3D
|
||||
@@ -123,6 +125,26 @@ func apply(force: bool) -> void:
|
||||
env.ambient_light_energy = lerpf(0.3, 1.0, daylight) * lerpf(1.0, 0.85, overcast) + w_flash * 3.0
|
||||
env.fog_light_energy = lerpf(0.25, 1.0, daylight) * lerpf(1.0, 0.7, overcast) + w_flash * 2.0
|
||||
env.volumetric_fog_density = lerpf(0.0025, 0.0045, daylight) * lerpf(1.0, 2.0, overcast)
|
||||
if underground > 0.0:
|
||||
# In the tunnel (J1): no sun, moon or sky, only the lights you bring.
|
||||
var u := underground
|
||||
sun.light_energy *= 1.0 - u
|
||||
sun.visible = sun.light_energy > 0.01
|
||||
sun.shadow_enabled = sun.visible
|
||||
moon.light_energy *= 1.0 - u
|
||||
moon.visible = moon.light_energy > 0.01
|
||||
env.ambient_light_energy = lerpf(env.ambient_light_energy, 0.015, u)
|
||||
env.ambient_light_sky_contribution = 1.0 - u
|
||||
env.fog_light_energy = lerpf(env.fog_light_energy, 0.0, u)
|
||||
env.volumetric_fog_density *= 1.0 - u
|
||||
# (Sky reflections and the fog's sky-coloured distance would light the
|
||||
# rock blue-grey all the way down: off.)
|
||||
env.fog_aerial_perspective = lerpf(AERIAL, 0.0, u)
|
||||
env.reflected_light_source = Environment.REFLECTION_SOURCE_DISABLED if u > 0.5 else Environment.REFLECTION_SOURCE_SKY
|
||||
else:
|
||||
env.ambient_light_sky_contribution = 1.0
|
||||
env.fog_aerial_perspective = AERIAL
|
||||
env.reflected_light_source = Environment.REFLECTION_SOURCE_SKY
|
||||
env.fog_density = BASE_FOG * w_fog
|
||||
|
||||
_sky_timer -= get_process_delta_time()
|
||||
|
||||
@@ -32,6 +32,7 @@ const FLOWER_COLORS := [
|
||||
|
||||
var terrain: Node3D
|
||||
var pier: Node3D ## the Cove's pier, kept clear of scenery (set by the world)
|
||||
var keep_clear: Array = [] ## [[Vector3, radius]]: the tunnel's seam, the sea cave
|
||||
var _player: Node3D
|
||||
var _spawn := Vector3.ZERO
|
||||
var _forest_noise := FastNoiseLite.new()
|
||||
@@ -422,6 +423,9 @@ func _scatter_trees() -> void:
|
||||
func _on_clifftop(x: float, z: float) -> bool:
|
||||
if pier != null and pier.covers(x, z, 2.0):
|
||||
return true
|
||||
for c in keep_clear:
|
||||
if Vector2(x - c[0].x, z - c[0].z).length() < float(c[1]):
|
||||
return true
|
||||
return terrain.sea_weight(x, z) > 0.5 and not terrain.inside_boundary(x, z, 10.0) \
|
||||
and terrain.inside_boundary(x, z, -8.0)
|
||||
|
||||
|
||||
120
scripts/world/sea_cave.gd
Normal file
120
scripts/world/sea_cave.gd
Normal file
@@ -0,0 +1,120 @@
|
||||
extends Node3D
|
||||
## The Cove's sea cave (J1): the bottom of the Valley's cliff tunnel, seen
|
||||
## from the shore. A dark opening in the rocks at the end of the beach,
|
||||
## where the headland meets the sand. [E] goes in and up the tunnel, back to
|
||||
## the Valley, once that tunnel has been dug through from the top. (Until
|
||||
## then the cave runs back into solid rock.)
|
||||
##
|
||||
## Placed from the terrain alone, like the pier, so it's the same for
|
||||
## everyone; nothing about it is saved here (the tunnel is the Valley's).
|
||||
|
||||
var world: Node3D
|
||||
var to := "valley"
|
||||
var front := Vector3.ZERO ## where you stand in front of it (arriving from the tunnel)
|
||||
|
||||
## Places the cave if this worldlet has a way back by tunnel from a sea cave.
|
||||
func place(w: Node3D) -> bool:
|
||||
world = w
|
||||
var t: Node3D = w.terrain
|
||||
var way: Dictionary = {}
|
||||
for c in w.crossings():
|
||||
if c is Dictionary and c.get("by", "") == "tunnel" and c.get("where", "") == "sea_cave":
|
||||
way = c
|
||||
if way.is_empty() or not t._has_beach:
|
||||
return false
|
||||
to = String(way.to)
|
||||
# At the end of the beach, where the headland starts: the first dry,
|
||||
# gentle spot coming in from the waterline.
|
||||
var a := deg_to_rad(t.beach_angle_deg + t.beach_half_width_deg + t.beach_fade_deg * 0.35)
|
||||
var dir := Vector2(cos(a), sin(a))
|
||||
var at := Vector3.INF
|
||||
for i in range(60):
|
||||
var r: float = t._shore_d() - 2.0 - i * 0.5 - t._rim_offset(dir.x, dir.y)
|
||||
var p := dir * r
|
||||
var h: float = t.sample_height(p.x, p.y)
|
||||
if h > t.water_level + 0.5 and t.sample_normal(p.x, p.y).y > 0.86 and t.inside_boundary(p.x, p.y, 3.0):
|
||||
at = Vector3(p.x, h, p.y)
|
||||
break
|
||||
if at == Vector3.INF:
|
||||
return false
|
||||
# Facing back along the beach (towards its middle), so you see it from the sand.
|
||||
var mid: Vector2 = t.shore_point(-6.0)
|
||||
var face := Vector3(mid.x - at.x, 0.0, mid.y - at.z).normalized()
|
||||
global_transform = Transform3D(Basis.looking_at(face, Vector3.UP), at)
|
||||
front = at + face * 2.4
|
||||
front.y = t.sample_height(front.x, front.z)
|
||||
_build(w)
|
||||
add_to_group("sea_cave")
|
||||
return true
|
||||
|
||||
## Whether the Valley's tunnel has been dug right down to here.
|
||||
func open() -> bool:
|
||||
if not Saves.has_world():
|
||||
return false
|
||||
var s: Dictionary = Saves.load_state(Saves.current_id, to)
|
||||
return int(s.get("tunnel", {}).get("dug", 0)) >= preload("res://scripts/world/tunnel.gd").STRETCHES
|
||||
|
||||
func interact_prompt(_player: Node) -> String:
|
||||
if open():
|
||||
return "[E] Go into the sea cave and up the tunnel to the %s" % preload("res://scripts/world/terrain.gd").worldlet_name(to)
|
||||
return "A sea cave. It runs back into solid rock: the way up hasn't been dug"
|
||||
|
||||
func interact(_player: Node) -> void:
|
||||
if open():
|
||||
world.travel(to, "tunnel_bottom")
|
||||
else:
|
||||
Actions.notify.emit(interact_prompt(null))
|
||||
|
||||
func _build(w: Node3D) -> void:
|
||||
var rock := StandardMaterial3D.new()
|
||||
rock.albedo_color = Color(0.42, 0.40, 0.38)
|
||||
rock.roughness = 1.0
|
||||
if w.terrain.noise_tex != null:
|
||||
rock.albedo_texture = w.terrain.noise_tex
|
||||
rock.uv1_triplanar = true
|
||||
rock.uv1_scale = Vector3(0.18, 0.18, 0.18)
|
||||
var body := StaticBody3D.new()
|
||||
add_child(body)
|
||||
# (local -Z faces the beach; the rocks are behind, +Z)
|
||||
for part in [[Vector3(-2.0, 1.6, 1.0), Vector3(1.8, 3.6, 3.0)], [Vector3(2.0, 1.4, 1.0), Vector3(1.8, 3.2, 3.0)],
|
||||
[Vector3(0, 3.5, 1.0), Vector3(5.8, 1.4, 3.2)], [Vector3(0, 1.4, 2.4), Vector3(2.4, 3.0, 0.6)],
|
||||
[Vector3(-3.4, 1.0, 2.0), Vector3(2.0, 2.4, 3.0)], [Vector3(3.2, 0.8, 2.2), Vector3(1.8, 2.0, 2.6)]]:
|
||||
var mi := MeshInstance3D.new()
|
||||
var bm := BoxMesh.new()
|
||||
bm.size = part[1]
|
||||
mi.mesh = bm
|
||||
mi.material_override = rock
|
||||
mi.position = part[0]
|
||||
mi.rotation.y = part[0].x * 0.06
|
||||
add_child(mi)
|
||||
var cs := CollisionShape3D.new()
|
||||
var box := BoxShape3D.new()
|
||||
box.size = part[1]
|
||||
cs.shape = box
|
||||
cs.position = part[0]
|
||||
cs.rotation.y = part[0].x * 0.06
|
||||
body.add_child(cs)
|
||||
var dark := MeshInstance3D.new()
|
||||
var q := QuadMesh.new()
|
||||
q.size = Vector2(2.2, 2.8)
|
||||
dark.mesh = q
|
||||
var m := StandardMaterial3D.new()
|
||||
m.albedo_color = Color(0.02, 0.02, 0.025)
|
||||
m.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
dark.material_override = m
|
||||
dark.position = Vector3(0, 1.4, 2.08)
|
||||
dark.rotation.y = PI
|
||||
add_child(dark)
|
||||
# What you press E on: the opening (interactables layer: you walk into it).
|
||||
var spot := StaticBody3D.new()
|
||||
spot.collision_layer = 2
|
||||
spot.collision_mask = 0
|
||||
var cs2 := CollisionShape3D.new()
|
||||
var b2 := BoxShape3D.new()
|
||||
b2.size = Vector3(2.2, 2.8, 1.6)
|
||||
cs2.shape = b2
|
||||
spot.add_child(cs2)
|
||||
spot.position = Vector3(0, 1.4, 1.2)
|
||||
spot.set_script(preload("res://scripts/world/sea_cave_spot.gd"))
|
||||
spot.cave = self
|
||||
add_child(spot)
|
||||
1
scripts/world/sea_cave.gd.uid
Normal file
1
scripts/world/sea_cave.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dne5yll2h1st2
|
||||
10
scripts/world/sea_cave_spot.gd
Normal file
10
scripts/world/sea_cave_spot.gd
Normal file
@@ -0,0 +1,10 @@
|
||||
extends StaticBody3D
|
||||
## The sea cave's opening (sea_cave.gd): what you look at and press E on.
|
||||
|
||||
var cave: Node3D
|
||||
|
||||
func interact_prompt(player: Node) -> String:
|
||||
return cave.interact_prompt(player)
|
||||
|
||||
func interact(player: Node) -> void:
|
||||
cave.interact(player)
|
||||
1
scripts/world/sea_cave_spot.gd.uid
Normal file
1
scripts/world/sea_cave_spot.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cvc1bri24m1h4
|
||||
451
scripts/world/tunnel.gd
Normal file
451
scripts/world/tunnel.gd
Normal file
@@ -0,0 +1,451 @@
|
||||
extends Node3D
|
||||
## The cliff tunnel (J1, docs/WORLDLETS_DESIGN.md §10): the Valley's way down
|
||||
## to the Cove, dug by hand.
|
||||
##
|
||||
## - **The seam:** a cracked outcrop on the clifftop strip near the edge.
|
||||
## Only the antler pickaxe (tier 3) bites into it; once it gives, it's a
|
||||
## dark opening: [E] goes down into the tunnel.
|
||||
## - **The tunnel:** its own small space, out of sight beneath the island
|
||||
## (the heightmap can't have holes), so going in and out is a step through
|
||||
## a doorway. Six sloping stretches, each one turning back under the last
|
||||
## like a stair, about 24 m down in all. The end of the dug part is a rock
|
||||
## face; each stretch dug must be propped (2 wood, [E] at its mouth) before
|
||||
## the next face takes a blow. Nothing caves in: it's a cost, not a trap.
|
||||
## - **Dark:** no daylight reaches it (day_night.underground). Bring light:
|
||||
## candles and torch stands can be set anywhere on its floor, and stay.
|
||||
## - **The bottom:** the last stretch breaks into a sea cave; walking out of
|
||||
## its mouth is the crossing to the Cove (world.travel()).
|
||||
##
|
||||
## How far it's dug, the face's damage and the props are saved with the
|
||||
## Valley (to_data()); the lights are ordinary placed pieces. Single-player
|
||||
## for now: on a server it isn't built (J3 brings travel online).
|
||||
|
||||
const TunnelFace = preload("res://scripts/world/tunnel_face.gd")
|
||||
|
||||
const ORIGIN := Vector3(0.0, -260.0, 0.0) ## where the tunnel space sits (well under the island)
|
||||
const STRETCHES := 6
|
||||
const RUN := 6.0 ## each stretch: this far along...
|
||||
const DROP := 4.0 ## ...and this far down
|
||||
const W := 2.6 ## passage width
|
||||
const H := 3.0 ## passage height
|
||||
const LAND := 3.4 ## landing length, where it turns back
|
||||
const LAND_EXT := 2.0 ## how far a landing's headroom reaches into its stretches
|
||||
const GAP := 1.4 ## rock between the two lanes
|
||||
const CAVE_LEN := 7.0 ## the sea cave passage at the bottom
|
||||
const PROP_WOOD := 2
|
||||
const FACE_HP := 45.0 ## 15 blows of the antler pickaxe
|
||||
const SEAM_HP := 30.0
|
||||
|
||||
var world: Node3D
|
||||
var terrain: Node3D
|
||||
var to := "cove" ## the worldlet at the bottom
|
||||
|
||||
# Saved state.
|
||||
var opened := false ## the seam has given way
|
||||
var dug := 0 ## stretches dug (0..STRETCHES)
|
||||
var face_hp := FACE_HP ## damage to the face being dug (the seam's, before it opens)
|
||||
var props: Array = [] ## props[k] for stretch k (1-based; [0] unused)
|
||||
|
||||
# Where things are (world coordinates).
|
||||
var seam_at := Vector3.ZERO ## the seam, on the ground
|
||||
var seam_out := Vector3.ZERO ## where you stand outside it (inland)
|
||||
var top := Vector3.ZERO ## inside, at the top
|
||||
var bottom := Vector3.ZERO ## inside, at the bottom by the cave
|
||||
var cave_mouth := Vector3.ZERO ## the far end of the sea cave
|
||||
var aabb := AABB()
|
||||
|
||||
var _rock_mat: StandardMaterial3D
|
||||
var _face_mat: StandardMaterial3D
|
||||
var _wood_mat: StandardMaterial3D
|
||||
var _rock: CSGCombiner3D
|
||||
var _cuts: Node3D
|
||||
var _face: StaticBody3D
|
||||
var _seam: Node3D
|
||||
var _spots: Node3D
|
||||
|
||||
## Builds the tunnel for `w` if its worldlet has a way on by tunnel from
|
||||
## its sea cliff. Returns false (and builds nothing) otherwise.
|
||||
func setup(w: Node3D) -> bool:
|
||||
world = w
|
||||
terrain = w.terrain
|
||||
var way: Dictionary = {}
|
||||
for c in w.crossings():
|
||||
if c is Dictionary and c.get("by", "") == "tunnel" and c.get("where", "") == "sea_cliff":
|
||||
way = c
|
||||
if way.is_empty() or not terrain._has_sea:
|
||||
return false
|
||||
to = String(way.to)
|
||||
props.resize(STRETCHES + 1)
|
||||
props.fill(false)
|
||||
_make_materials()
|
||||
_place_seam()
|
||||
_layout()
|
||||
_rebuild()
|
||||
add_to_group("tunnel")
|
||||
return true
|
||||
|
||||
# --- where it is ----------------------------------------------------------------
|
||||
|
||||
func _lane_z(k: int) -> float:
|
||||
return 0.0 if k % 2 == 1 else W + GAP
|
||||
|
||||
func _landing_x(k: int) -> float:
|
||||
return (k % 2) * (LAND + RUN)
|
||||
|
||||
func _floor_y(k: int) -> float:
|
||||
return -k * DROP
|
||||
|
||||
## Inside the tunnel's space (with a margin), in world coordinates.
|
||||
func contains(p: Vector3) -> bool:
|
||||
return aabb.grow(1.0).has_point(p)
|
||||
|
||||
func _layout() -> void:
|
||||
var x0 := -LAND * 0.5 - 3.0 - CAVE_LEN
|
||||
var x1 := LAND * 1.5 + RUN + 3.0
|
||||
var y0 := _floor_y(STRETCHES) - 3.0
|
||||
var y1 := H + 3.0
|
||||
var z0 := -W * 0.5 - 3.0
|
||||
var z1 := W * 1.5 + GAP + 3.0
|
||||
aabb = AABB(ORIGIN + Vector3(x0, y0, z0), Vector3(x1 - x0, y1 - y0, z1 - z0))
|
||||
top = ORIGIN + Vector3(_landing_x(0) - LAND * 0.25, 0.0, _lane_z(1))
|
||||
var zb := _lane_z(STRETCHES)
|
||||
bottom = ORIGIN + Vector3(_landing_x(STRETCHES), _floor_y(STRETCHES), zb)
|
||||
cave_mouth = bottom + Vector3(-LAND * 0.5 - CAVE_LEN + 0.6, 0.0, 0.0)
|
||||
|
||||
## The seam: on the clifftop strip, a few metres in from the edge, at an
|
||||
## angle the seed picks.
|
||||
func _place_seam() -> void:
|
||||
var rng := RandomNumberGenerator.new()
|
||||
rng.seed = terrain.noise_seed + 404
|
||||
for _try in range(40):
|
||||
var a: float = terrain.sea_angle_deg + rng.randf_range(-18.0, 18.0)
|
||||
var p: Vector2 = terrain.coast_point(a, -7.0)
|
||||
if not terrain.inside_boundary(p.x, p.y, 2.0):
|
||||
continue
|
||||
var n: Vector3 = terrain.sample_normal(p.x, p.y)
|
||||
if n.y < 0.9:
|
||||
continue
|
||||
seam_at = Vector3(p.x, terrain.sample_height(p.x, p.y), p.y)
|
||||
break
|
||||
var inland := -Vector3(seam_at.x, 0.0, seam_at.z).normalized()
|
||||
seam_out = seam_at + inland * 2.2
|
||||
seam_out.y = terrain.sample_height(seam_out.x, seam_out.z)
|
||||
|
||||
# --- digging ---------------------------------------------------------------------
|
||||
|
||||
## Why the face at `stage` won't take a blow ("" = it will).
|
||||
func dig_refusal(stage: int) -> String:
|
||||
if stage >= 2:
|
||||
for k in range(1, stage):
|
||||
if not props[k]:
|
||||
return "It's loose: prop the last stretch first ([E] at its mouth, %d wood)" % PROP_WOOD
|
||||
return ""
|
||||
|
||||
func face_hit(f: Node) -> void:
|
||||
face_hp = f.hp
|
||||
|
||||
func face_broken(f: Node) -> void:
|
||||
face_hp = FACE_HP
|
||||
if f.stage == 0:
|
||||
opened = true
|
||||
Actions.notify.emit("The seam gives way: a dark passage leads down into the cliff")
|
||||
else:
|
||||
dug = f.stage
|
||||
if dug >= STRETCHES:
|
||||
Actions.notify.emit("You break through into a sea cave. You can hear the waves")
|
||||
else:
|
||||
Actions.notify.emit("The rock gives. Prop this stretch before you dig on")
|
||||
_rebuild.call_deferred()
|
||||
|
||||
## [E] at the mouth of stretch `k`: sets a timber prop there.
|
||||
func set_prop(player: Node, k: int) -> bool:
|
||||
if k < 1 or k > dug or props[k]:
|
||||
return false
|
||||
if player.inventory.count("wood") < PROP_WOOD:
|
||||
Actions.notify.emit("A timber prop needs %d wood" % PROP_WOOD)
|
||||
return false
|
||||
player.inventory.remove("wood", PROP_WOOD)
|
||||
props[k] = true
|
||||
Actions.notify.emit("Propped. The next face is safe to dig")
|
||||
_rebuild.call_deferred()
|
||||
return true
|
||||
|
||||
# --- going in and out ----------------------------------------------------------
|
||||
|
||||
func go_in(player: Node3D) -> void:
|
||||
_move(player, top + Vector3(0, 0.1, 0), Vector3(1, 0, 0))
|
||||
|
||||
func go_out(player: Node3D) -> void:
|
||||
_move(player, seam_out + Vector3(0, 0.2, 0), -Vector3(seam_at.x, 0.0, seam_at.z).normalized())
|
||||
|
||||
func _move(player: Node3D, at: Vector3, facing: Vector3) -> void:
|
||||
player.global_position = at
|
||||
player.velocity = Vector3.ZERO
|
||||
player.rotation = Vector3(0.0, atan2(-facing.x, -facing.z), 0.0)
|
||||
|
||||
# --- saving ----------------------------------------------------------------------
|
||||
|
||||
func to_data() -> Dictionary:
|
||||
var p: Array = []
|
||||
for k in range(1, STRETCHES + 1):
|
||||
if props[k]:
|
||||
p.append(k)
|
||||
return {"opened": opened, "dug": dug, "face_hp": face_hp, "props": p}
|
||||
|
||||
func load_data(d: Dictionary) -> void:
|
||||
opened = bool(d.get("opened", false))
|
||||
dug = clampi(int(d.get("dug", 0)), 0, STRETCHES)
|
||||
face_hp = float(d.get("face_hp", FACE_HP))
|
||||
props.fill(false)
|
||||
for k in d.get("props", []):
|
||||
if int(k) >= 1 and int(k) <= STRETCHES:
|
||||
props[int(k)] = true
|
||||
_rebuild()
|
||||
|
||||
# --- building it ---------------------------------------------------------------
|
||||
|
||||
func _make_materials() -> void:
|
||||
_rock_mat = StandardMaterial3D.new()
|
||||
_rock_mat.albedo_color = Color(0.40, 0.38, 0.36)
|
||||
_rock_mat.roughness = 1.0
|
||||
if terrain.noise_tex != null:
|
||||
_rock_mat.albedo_texture = terrain.noise_tex
|
||||
_rock_mat.uv1_triplanar = true
|
||||
_rock_mat.uv1_scale = Vector3(0.18, 0.18, 0.18)
|
||||
_face_mat = _rock_mat.duplicate()
|
||||
_face_mat.albedo_color = Color(0.30, 0.27, 0.25)
|
||||
_wood_mat = StandardMaterial3D.new()
|
||||
_wood_mat.albedo_color = Color(0.42, 0.29, 0.17)
|
||||
_wood_mat.roughness = 0.9
|
||||
|
||||
func _rebuild() -> void:
|
||||
for n in [_rock, _seam, _spots, _face]:
|
||||
if n != null and is_instance_valid(n):
|
||||
n.queue_free()
|
||||
_face = null
|
||||
_build_rock()
|
||||
_build_seam()
|
||||
_spots = Node3D.new()
|
||||
_spots.name = "Spots"
|
||||
_spots.top_level = true
|
||||
add_child(_spots)
|
||||
# The face being dug: at the mouth of the next stretch (or the seam).
|
||||
if opened and dug < STRETCHES:
|
||||
var k := dug + 1
|
||||
var mouth := _mouth(k)
|
||||
_face = _make_face(k, mouth + Vector3(0, H * 0.5, 0), Vector3(0.5, H, W + 0.2))
|
||||
# Props set (posts and a beam), and spots to set the missing ones.
|
||||
for k in range(1, dug + 1):
|
||||
var mouth := _mouth(k)
|
||||
if props[k]:
|
||||
_add_prop_mesh(mouth)
|
||||
else:
|
||||
_add_spot(mouth + Vector3(0, H * 0.5, 0), Vector3(0.6, H, W), "prop", k)
|
||||
_add_spot(ORIGIN + Vector3(_landing_x(0) - LAND * 0.5 + 0.35, H * 0.5, _lane_z(1)), Vector3(0.4, H, W), "out", 0)
|
||||
if dug >= STRETCHES:
|
||||
_add_mouth()
|
||||
|
||||
## The start of stretch k, on the floor of landing k-1 (world coordinates).
|
||||
func _mouth(k: int) -> Vector3:
|
||||
var s := signf(_landing_x(k) - _landing_x(k - 1))
|
||||
return ORIGIN + Vector3(_landing_x(k - 1) + s * LAND * 0.5, _floor_y(k - 1), _lane_z(k))
|
||||
|
||||
func _build_rock() -> void:
|
||||
_rock = CSGCombiner3D.new()
|
||||
_rock.name = "Rock"
|
||||
_rock.use_collision = true
|
||||
_rock.collision_layer = 1
|
||||
_rock.top_level = true
|
||||
add_child(_rock)
|
||||
_rock.global_position = ORIGIN
|
||||
var base := CSGBox3D.new()
|
||||
base.size = aabb.size
|
||||
base.position = aabb.get_center() - ORIGIN
|
||||
base.material = _rock_mat
|
||||
_rock.add_child(base)
|
||||
if not opened:
|
||||
return
|
||||
# Landings: the top one on lane A only; the rest span both lanes.
|
||||
for k in range(0, dug + 1):
|
||||
var x := _landing_x(k)
|
||||
var y := _floor_y(k)
|
||||
var zlo := -W * 0.5 if k < STRETCHES else _lane_z(k) - W * 0.5
|
||||
var zhi := W * 0.5 if k == 0 else W * 1.5 + GAP
|
||||
if k == STRETCHES:
|
||||
zhi = _lane_z(k) + W * 0.5
|
||||
_cut(Vector3(x, y + H * 0.5, (zlo + zhi) * 0.5), Vector3(LAND, H, zhi - zlo), 0.0)
|
||||
# Headroom LAND_EXT on into the stretch going down from here (in its
|
||||
# lane only): a sloping cut's end is square to the slope, so on its
|
||||
# own it would leave a lip of rock across the top of it at head height.
|
||||
if k < dug:
|
||||
var side := signf(_landing_x(k + 1) - x)
|
||||
_cut(Vector3(x + side * (LAND + LAND_EXT) * 0.5, y + H * 0.5, _lane_z(k + 1)), Vector3(LAND_EXT, H, W), 0.0)
|
||||
# The stretches dug: sloping boxes from one landing down to the next.
|
||||
for k in range(1, dug + 1):
|
||||
var a := _mouth(k) - ORIGIN
|
||||
var s := signf(_landing_x(k) - _landing_x(k - 1))
|
||||
var b := Vector3(_landing_x(k) - s * LAND * 0.5, _floor_y(k), _lane_z(k))
|
||||
var d := b - a
|
||||
var length := Vector2(d.x, d.y).length()
|
||||
var ang := atan2(d.y, d.x)
|
||||
var up := Vector3(-sin(ang), cos(ang), 0.0)
|
||||
if up.y < 0.0:
|
||||
up = -up
|
||||
_cut((a + b) * 0.5 + up * H * 0.5, Vector3(length + 0.3, H, W), ang)
|
||||
# The sea cave, off the bottom landing.
|
||||
if dug >= STRETCHES:
|
||||
var c := bottom - ORIGIN + Vector3(-LAND * 0.5 - CAVE_LEN * 0.5, 0.0, 0.0)
|
||||
_cut(c + Vector3(0, H * 0.6, 0), Vector3(CAVE_LEN + 0.2, H * 1.2, W + 0.8), 0.0)
|
||||
|
||||
func _cut(center: Vector3, size: Vector3, z_rot: float) -> void:
|
||||
var b := CSGBox3D.new()
|
||||
b.operation = CSGShape3D.OPERATION_SUBTRACTION
|
||||
b.size = size
|
||||
b.position = center
|
||||
b.rotation = Vector3(0.0, 0.0, z_rot)
|
||||
b.material = _rock_mat
|
||||
_rock.add_child(b)
|
||||
|
||||
func _make_face(stage: int, center: Vector3, size: Vector3) -> StaticBody3D:
|
||||
var f: StaticBody3D = TunnelFace.new()
|
||||
f.name = "Face%d" % stage
|
||||
f.tunnel = self
|
||||
f.stage = stage
|
||||
f.uid = "tunnel_face_%d" % stage
|
||||
f.max_hp = SEAM_HP if stage == 0 else FACE_HP
|
||||
f.hp = clampf(face_hp, 0.5, f.max_hp)
|
||||
f.top_level = true
|
||||
f.collision_layer = 1
|
||||
f.add_to_group("harvestable")
|
||||
var cs := CollisionShape3D.new()
|
||||
var box := BoxShape3D.new()
|
||||
box.size = size
|
||||
cs.shape = box
|
||||
f.add_child(cs)
|
||||
var mi := MeshInstance3D.new()
|
||||
var bm := BoxMesh.new()
|
||||
bm.size = size
|
||||
mi.mesh = bm
|
||||
mi.material_override = _face_mat
|
||||
f.add_child(mi)
|
||||
add_child(f)
|
||||
f.global_position = center
|
||||
return f
|
||||
|
||||
## The seam on the clifftop: an outcrop with a cracked face, then (dug) a
|
||||
## dark doorway into the cliff.
|
||||
func _build_seam() -> void:
|
||||
_seam = Node3D.new()
|
||||
_seam.name = "Seam"
|
||||
_seam.top_level = true
|
||||
add_child(_seam)
|
||||
var inland := -Vector3(seam_at.x, 0.0, seam_at.z).normalized()
|
||||
_seam.global_transform = Transform3D(Basis.looking_at(inland, Vector3.UP), seam_at)
|
||||
# (local -Z faces inland, towards the valley; the outcrop is behind, +Z)
|
||||
var body := StaticBody3D.new()
|
||||
_seam.add_child(body)
|
||||
for part in [[Vector3(-1.5, 1.2, 0.4), Vector3(1.0, 2.6, 2.0)], [Vector3(1.5, 1.1, 0.4), Vector3(1.0, 2.4, 2.0)],
|
||||
[Vector3(0, 2.6, 0.4), Vector3(4.0, 0.9, 2.2)], [Vector3(0, 1.1, 1.1), Vector3(2.2, 2.4, 0.6)]]:
|
||||
var mi := MeshInstance3D.new()
|
||||
var bm := BoxMesh.new()
|
||||
bm.size = part[1]
|
||||
mi.mesh = bm
|
||||
mi.material_override = _rock_mat
|
||||
mi.position = part[0]
|
||||
_seam.add_child(mi)
|
||||
var cs := CollisionShape3D.new()
|
||||
var box := BoxShape3D.new()
|
||||
box.size = part[1]
|
||||
cs.shape = box
|
||||
cs.position = part[0]
|
||||
body.add_child(cs)
|
||||
if not opened:
|
||||
_face = _make_face(0, _seam.global_transform * Vector3(0, 1.1, -0.35), Vector3(2.0, 2.2, 0.5))
|
||||
_face.global_basis = _seam.global_basis
|
||||
else:
|
||||
var dark := MeshInstance3D.new()
|
||||
var q := QuadMesh.new()
|
||||
q.size = Vector2(2.0, 2.1)
|
||||
dark.mesh = q
|
||||
var m := StandardMaterial3D.new()
|
||||
m.albedo_color = Color(0.02, 0.02, 0.02)
|
||||
m.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
dark.material_override = m
|
||||
dark.position = Vector3(0, 1.05, 0.78)
|
||||
dark.rotation.y = PI # (a quad faces +Z: turn it to face inland)
|
||||
_seam.add_child(dark)
|
||||
var spot := _spot_body(Vector3(2.0, 2.1, 0.4), "in", 0)
|
||||
_seam.add_child(spot)
|
||||
spot.position = Vector3(0, 1.05, -0.2)
|
||||
|
||||
## A box you can look at and press E on (layer 2: you walk through it).
|
||||
func _spot_body(size: Vector3, what: String, k: int) -> StaticBody3D:
|
||||
var s := StaticBody3D.new()
|
||||
s.set_script(preload("res://scripts/world/tunnel_spot.gd"))
|
||||
s.tunnel = self
|
||||
s.what = what
|
||||
s.k = k
|
||||
s.collision_layer = 2
|
||||
s.collision_mask = 0
|
||||
var cs := CollisionShape3D.new()
|
||||
var box := BoxShape3D.new()
|
||||
box.size = size
|
||||
cs.shape = box
|
||||
s.add_child(cs)
|
||||
return s
|
||||
|
||||
func _add_spot(center: Vector3, size: Vector3, what: String, k: int) -> void:
|
||||
var s := _spot_body(size, what, k)
|
||||
_spots.add_child(s)
|
||||
s.global_position = center
|
||||
|
||||
## Two posts and a beam across the mouth of a stretch.
|
||||
func _add_prop_mesh(mouth: Vector3) -> void:
|
||||
for part in [[Vector3(0, H * 0.5 - 0.1, -W * 0.5 + 0.18), Vector3(0.22, H - 0.2, 0.22)],
|
||||
[Vector3(0, H * 0.5 - 0.1, W * 0.5 - 0.18), Vector3(0.22, H - 0.2, 0.22)],
|
||||
[Vector3(0, H - 0.25, 0), Vector3(0.26, 0.24, W)]]:
|
||||
var mi := MeshInstance3D.new()
|
||||
var bm := BoxMesh.new()
|
||||
bm.size = part[1]
|
||||
mi.mesh = bm
|
||||
mi.material_override = _wood_mat
|
||||
_spots.add_child(mi)
|
||||
mi.global_position = mouth + part[0]
|
||||
|
||||
## The sea cave's mouth: grey daylight and the sound of the sea; walk into
|
||||
## it to go out onto the Cove's shore.
|
||||
func _add_mouth() -> void:
|
||||
var glow := MeshInstance3D.new()
|
||||
var q := QuadMesh.new()
|
||||
q.size = Vector2(W + 0.8, H * 1.2)
|
||||
glow.mesh = q
|
||||
var m := StandardMaterial3D.new()
|
||||
m.albedo_color = Color(0.75, 0.82, 0.88)
|
||||
m.emission_enabled = true
|
||||
m.emission = Color(0.6, 0.7, 0.78)
|
||||
m.emission_energy_multiplier = 1.6
|
||||
m.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
glow.material_override = m
|
||||
_spots.add_child(glow)
|
||||
glow.global_position = cave_mouth + Vector3(-0.55, H * 0.6, 0.0)
|
||||
glow.rotation.y = PI * 0.5
|
||||
var light := OmniLight3D.new()
|
||||
light.light_color = Color(0.7, 0.8, 0.9)
|
||||
light.light_energy = 1.2
|
||||
light.omni_range = 6.0
|
||||
_spots.add_child(light)
|
||||
light.global_position = cave_mouth + Vector3(0.8, H * 0.6, 0.0)
|
||||
var area := Area3D.new()
|
||||
area.collision_layer = 0
|
||||
area.collision_mask = 1 | 2 | 4 | 8 | 16 | 32
|
||||
var cs := CollisionShape3D.new()
|
||||
var box := BoxShape3D.new()
|
||||
box.size = Vector3(1.0, H, W + 0.6)
|
||||
cs.shape = box
|
||||
area.add_child(cs)
|
||||
_spots.add_child(area)
|
||||
area.global_position = cave_mouth + Vector3(0, H * 0.5, 0)
|
||||
area.body_entered.connect(func(b):
|
||||
if b == world.player:
|
||||
world.travel(to, "sea_cave"))
|
||||
_add_spot(cave_mouth + Vector3(1.2, H * 0.5, 0), Vector3(0.4, H, W), "cave", 0)
|
||||
1
scripts/world/tunnel.gd.uid
Normal file
1
scripts/world/tunnel.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dxkd1tmf7rs5b
|
||||
44
scripts/world/tunnel_face.gd
Normal file
44
scripts/world/tunnel_face.gd
Normal file
@@ -0,0 +1,44 @@
|
||||
extends StaticBody3D
|
||||
## A rock face to dig through (J1): the seam on the clifftop, or the end of
|
||||
## the tunnel dug so far. It answers the same questions a boulder does
|
||||
## (Actions.harvest_hit), so a swing of the pick works on it, but the tunnel
|
||||
## (tunnel.gd) owns the rules: which pick, whether it's propped, and what
|
||||
## happens when it gives.
|
||||
|
||||
var tunnel: Node3D
|
||||
var stage := 0 ## 0 = the seam; k = the face at the start of stretch k
|
||||
var uid := ""
|
||||
var tool_kind := "pickaxe"
|
||||
var min_tier := 3 ## the antler pickaxe
|
||||
var max_hp := 45.0
|
||||
var hp := 45.0
|
||||
var depleted := false
|
||||
var kind := "rock"
|
||||
var chip_color := Color(0.42, 0.40, 0.38)
|
||||
|
||||
func display_name() -> String:
|
||||
return "Cracked seam" if stage == 0 else "Rock face"
|
||||
|
||||
## Why the next blow won't land ("" = it will), before the tool check.
|
||||
func refusal() -> String:
|
||||
return tunnel.dig_refusal(stage)
|
||||
|
||||
func interact_prompt(_player: Node) -> String:
|
||||
var why := refusal()
|
||||
var left := ceili(hp / 3.0)
|
||||
if not why.is_empty():
|
||||
return "%s: %s" % [display_name(), why]
|
||||
return "%s: [LMB] dig with the antler pickaxe (about %d blows left)" % [display_name(), left]
|
||||
|
||||
func interact(player: Node) -> void:
|
||||
Actions.tell(player, interact_prompt(player))
|
||||
|
||||
## One blow (from Actions.harvest_hit). Returns what it knocks loose.
|
||||
func hit(damage: float, _at: Vector3, _from: Vector3) -> Dictionary:
|
||||
hp = maxf(hp - damage, 0.0)
|
||||
tunnel.face_hit(self)
|
||||
if hp <= 0.0 and not depleted:
|
||||
depleted = true
|
||||
tunnel.face_broken(self)
|
||||
return {"stone": 3}
|
||||
return {"stone": 1} if randf() < 0.4 else {}
|
||||
1
scripts/world/tunnel_face.gd.uid
Normal file
1
scripts/world/tunnel_face.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ctu24jeo68pkx
|
||||
35
scripts/world/tunnel_spot.gd
Normal file
35
scripts/world/tunnel_spot.gd
Normal file
@@ -0,0 +1,35 @@
|
||||
extends StaticBody3D
|
||||
## Something to press E on in or by the cliff tunnel (tunnel.gd): the
|
||||
## seam's doorway ("in"), the way out at the top ("out"), the mouth of a
|
||||
## stretch that needs propping ("prop", stretch k), and the sea cave's
|
||||
## mouth ("cave"). On the interactables layer, so you walk through it.
|
||||
|
||||
var tunnel: Node3D
|
||||
var what := ""
|
||||
var k := 0
|
||||
|
||||
func interact_prompt(player: Node) -> String:
|
||||
match what:
|
||||
"in":
|
||||
return "[E] Go down into the tunnel"
|
||||
"out":
|
||||
return "[E] Climb out onto the clifftop"
|
||||
"prop":
|
||||
var have: int = player.inventory.count("wood")
|
||||
return "Unpropped: [E] Set a timber prop (%d wood, you have %d)" % [tunnel.PROP_WOOD, have]
|
||||
"cave":
|
||||
return "The sea cave: walk on out to the %s" % Terrain.worldlet_name(tunnel.to)
|
||||
return ""
|
||||
|
||||
func interact(player: Node) -> void:
|
||||
match what:
|
||||
"in":
|
||||
tunnel.go_in(player)
|
||||
"out":
|
||||
tunnel.go_out(player)
|
||||
"prop":
|
||||
tunnel.set_prop(player, k)
|
||||
"cave":
|
||||
tunnel.world.travel(tunnel.to, "sea_cave")
|
||||
|
||||
const Terrain = preload("res://scripts/world/terrain.gd")
|
||||
1
scripts/world/tunnel_spot.gd.uid
Normal file
1
scripts/world/tunnel_spot.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://wpqf817odym0
|
||||
@@ -54,6 +54,9 @@ var _spawn_yaw := 0.0
|
||||
var _pieces: Node3D
|
||||
var hud: CanvasLayer
|
||||
var pier: Node3D ## the Cove's pier (scripts/world/pier.gd), or null
|
||||
var tunnel: Node3D ## the cliff tunnel (scripts/world/tunnel.gd, J1), or null
|
||||
var sea_cave: Node3D ## the Cove's end of it (scripts/world/sea_cave.gd), or null
|
||||
var traveling := false ## on the way to another worldlet (the scene is about to change)
|
||||
var _session_time := 0.0 ## seconds played since the last save
|
||||
var _autosave_left := AUTOSAVE_SECONDS
|
||||
var _menu_cam: Camera3D
|
||||
@@ -362,6 +365,25 @@ func _on_terrain_ready() -> void:
|
||||
props.pier = pr
|
||||
else:
|
||||
pr.free()
|
||||
# The journey's ways on (J1): the Valley's cliff tunnel and the Cove's sea
|
||||
# cave. Single-player for now (J3 brings travel online).
|
||||
if not server_mode and not menu_mode and not is_online():
|
||||
var tn := preload("res://scripts/world/tunnel.gd").new()
|
||||
tn.name = "Tunnel"
|
||||
add_child(tn)
|
||||
if tn.setup(self):
|
||||
tunnel = tn
|
||||
props.keep_clear.append([tn.seam_at, 4.0])
|
||||
else:
|
||||
tn.free()
|
||||
var sc := preload("res://scripts/world/sea_cave.gd").new()
|
||||
sc.name = "SeaCave"
|
||||
add_child(sc)
|
||||
if sc.place(self):
|
||||
sea_cave = sc
|
||||
props.keep_clear.append([sc.global_position, 6.0])
|
||||
else:
|
||||
sc.free()
|
||||
props.place_on(terrain, player if not menu_mode else _menu_cam, _spawn)
|
||||
# The wolf den: part of the scenery, so it's there from day 1 (empty).
|
||||
den = preload("res://scripts/creatures/den.gd").new()
|
||||
@@ -424,6 +446,7 @@ func _on_terrain_ready() -> void:
|
||||
if not state.is_empty():
|
||||
apply_save(state)
|
||||
print("[world] loaded '%s'" % Saves.current_id)
|
||||
_arrive(Saves.take_arrival(Saves.current_id))
|
||||
player.set_physics_process(true)
|
||||
set_physics_process(true)
|
||||
creatures.setup(self)
|
||||
@@ -652,6 +675,7 @@ func collect_save() -> Dictionary:
|
||||
"bear": bears.save_data() if bears != null else _saved_bear,
|
||||
"barrow": barrow.save_data() if barrow != null else {},
|
||||
"stones": stones.save_data() if stones != null else {},
|
||||
"tunnel": tunnel.to_data() if tunnel != null else {},
|
||||
}
|
||||
|
||||
func _shared_state(harvested: Dictionary, regrow: Dictionary, taken: Array, dropped: Array, pieces: Array) -> Dictionary:
|
||||
@@ -797,6 +821,8 @@ func _apply_save(s: Dictionary, with_player: bool) -> void:
|
||||
barrow.load_data(s.get("barrow", {}))
|
||||
if stones != null:
|
||||
stones.load_data(s.get("stones", {}))
|
||||
if tunnel != null:
|
||||
tunnel.load_data(s.get("tunnel", {}))
|
||||
# The bear (bear_range.gd): applied when it's set up, after loading.
|
||||
_saved_bear = s.get("bear", {})
|
||||
if bears != null:
|
||||
@@ -822,14 +848,82 @@ func _respawn() -> void:
|
||||
player.get_node("Camera3D").rotation.x = 0.0
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
_current(delta)
|
||||
var p := player.global_position
|
||||
var under := underground_at(p)
|
||||
day_night.underground = move_toward(day_night.underground, 1.0 if under else 0.0, delta * 3.0)
|
||||
if weather_fx != null:
|
||||
weather_fx.visible = not under
|
||||
# (No water down there: the tunnel is far below the lake's surface.)
|
||||
player.water_level = -INF if under else terrain.water_level
|
||||
if under:
|
||||
return # (the tunnel is its own space, out of sight under the island)
|
||||
_current(delta)
|
||||
var half: float = terrain.size * 0.5
|
||||
if p.y < terrain.min_height - 30.0 or absf(p.x) > half or absf(p.z) > half \
|
||||
or not terrain.inside_boundary(p.x, p.z, -4.0):
|
||||
push_warning("[world] player left the map at %s; respawning" % p)
|
||||
_respawn()
|
||||
|
||||
## In the cliff tunnel (J1): no daylight, no weather, sheltered.
|
||||
func underground_at(p: Vector3) -> bool:
|
||||
return tunnel != null and tunnel.contains(p)
|
||||
|
||||
# --- the journey (J1, docs/WORLDLETS_DESIGN.md §10) ------------------------------
|
||||
|
||||
## Goes on to worldlet `to`: saves this one, takes your character (bag,
|
||||
## stats, what you wear) and the clock along, and loads the other, arriving
|
||||
## at `arrive` ("sea_cave", "tunnel_bottom"). Each worldlet keeps its own
|
||||
## state file (Saves.state_path), so what you built here waits for you.
|
||||
func travel(to: String, arrive: String) -> void:
|
||||
if traveling or not Saves.has_world() or is_online() or server_mode:
|
||||
return
|
||||
traveling = true
|
||||
var id := Saves.current_id
|
||||
var here: Dictionary = collect_save()
|
||||
Saves.save_state(id, here, _session_time)
|
||||
_session_time = 0.0
|
||||
var there: Dictionary = Saves.load_state(id, to)
|
||||
if there.is_empty():
|
||||
there = {"time": here.time} # (a new place: the same day and hour)
|
||||
there["player"] = here.player
|
||||
Saves.save_state(id, there, 0.0, to)
|
||||
Saves.set_worldlet(id, to, arrive)
|
||||
print("[world] travelling to %s (%s)" % [to, arrive])
|
||||
if hud != null:
|
||||
hud.toast("On to the %s..." % Terrain.worldlet_name(to))
|
||||
travelled.emit(to)
|
||||
if get_tree().current_scene == self: # (in a test the world isn't the scene: the test loads the next one)
|
||||
get_tree().change_scene_to_file.call_deferred(scene_file_path if not scene_file_path.is_empty() else "res://scenes/world.tscn")
|
||||
|
||||
signal travelled(to: String) ## saved and on the way to another worldlet
|
||||
|
||||
## Just arrived from another worldlet: put you where you came in.
|
||||
func _arrive(at: String) -> void:
|
||||
if at.is_empty():
|
||||
return
|
||||
var pos := Vector3.INF
|
||||
var facing := Vector3.FORWARD
|
||||
match at:
|
||||
"sea_cave":
|
||||
if sea_cave != null:
|
||||
pos = sea_cave.front
|
||||
facing = (sea_cave.front - sea_cave.global_position).normalized()
|
||||
"tunnel_bottom":
|
||||
if tunnel != null and tunnel.dug >= tunnel.STRETCHES:
|
||||
pos = tunnel.bottom + Vector3(0.8, 0.0, 0.0)
|
||||
facing = Vector3(1, 0, 0)
|
||||
if pos == Vector3.INF:
|
||||
push_warning("[world] nowhere to arrive at '%s': the start instead" % at)
|
||||
_respawn()
|
||||
return
|
||||
player.global_position = pos + Vector3(0, 0.2, 0)
|
||||
player.velocity = Vector3.ZERO
|
||||
player.rotation = Vector3(0.0, atan2(-facing.x, -facing.z), 0.0)
|
||||
if hud != null:
|
||||
hud.toast("The %s" % Terrain.worldlet_name(worldlet_id()))
|
||||
|
||||
const Terrain = preload("res://scripts/world/terrain.gd")
|
||||
|
||||
## Off a beach (W2), a current carries anyone swimming too far out back
|
||||
## towards the shore: the sea's version of the invisible wall. It's faster
|
||||
## than you can swim, so you can't push through it, and it says so once.
|
||||
@@ -858,9 +952,10 @@ func placement_valid(def: Dictionary, xf: Transform3D) -> bool:
|
||||
if BuildPiece.is_build(def):
|
||||
return build.check(def, xf).is_empty()
|
||||
var p := xf.origin
|
||||
if not terrain.inside_boundary(p.x, p.z, 1.5):
|
||||
var under := underground_at(p) # (candles and torch stands down the tunnel: its floor is the ground)
|
||||
if not under and not terrain.inside_boundary(p.x, p.z, 1.5):
|
||||
return false
|
||||
if p.y < terrain.water_level + 0.15:
|
||||
if not under and p.y < terrain.water_level + 0.15:
|
||||
return false
|
||||
var fp: Array = def.place.get("footprint", [1.0, 1.0])
|
||||
var hx := float(fp[0]) * 0.5
|
||||
|
||||
Reference in New Issue
Block a user