extends "res://scenes/maps/map_geiss.gd"
## LARGE-SCALE TERRACES (2026-10-06): TerraceGraph alone — a smooth ~2 km, ~300 m
## landform cut into 25 m terraces — the terrain the Geiss field will later sit on as its
## detail octave. Judged on its own first, so no Geiss detail here yet.
##
## Everything else is map_geiss (fly camera, HUD, autoshot, GEISS_CAM / GEISS_AUTOSHOT):
## only the graph, the material and the start camera are overridden.
##
## Material: undersea_rock WITHOUT the field-normal splice (mesh normals) — the splice is
## under diagnosis and must not decide whether this map can be looked at. The altitude
## ramp is stretched over the terrace range so each band of treads reads as a colour.
func _field_graph() -> ProcNodeGraph:
return TerraceGraph.load_terraces()
## TEXTURED, MASKED BY THE COLOURING (user 2026-10-06): terrain_layers.gdshader turns the
## rock shader's colour masks — altitude bands sand / ochre / olive / slate, and sediment
## on up-facing surfaces — into weights over these texture-pack sets, in that order. One
## line per mask to swap a set.
const LAYER_SETS := ["polyhaven/clean_pebbles", "trimsheets/stone_base", "polyhaven/cliff_side",
"polyhaven/castle_wall_slates", "polyhaven/clean_pebbles"]
const LAYER_RES := 1024
const LAYERS_SHADER := preload("res://scenes/maps/undersea/terrain_layers.gdshader")
func _field_material(_gen: Object, graph: ProcNodeGraph) -> ShaderMaterial:
var kn := TerraceGraph.tknobs_for(graph)
var mat := ShaderMaterial.new()
mat.shader = LAYERS_SHADER
# THE LOOK IS DATA (TerrainMaterial): LAYER_SETS' defaults, overridden by what the
# room saved through the Terrain material panel.
_tm = TerrainMaterial.new(mat, LAYER_RES)
_tm.base_sets = LAYER_SETS.duplicate()
var why := _tm.apply(TerrainMaterial.merged(TerrainMaterial.defaults(LAYER_SETS), _saved_material()))
if why != "":
# LOUD, and the plain rock shading rather than layer 0 (the magenta checker).
push_warning("[geiss-terraces] %s — plain rock shading" % why)
_tm = null
mat.shader = ROCK_SHADER
mat.set_shader_parameter(&"strata_period", 6.0)
mat.set_shader_parameter(&"water_y", -1.0e6) # no sea here: no abyss tint
else:
print("[geiss-terraces] textured layers %s (array layers %s)"
% [str(_tm.cfg["layers"].map(func(l: Dictionary) -> String: return l["set"])),
str(mat.get_shader_parameter(&"layer_index"))])
mat.set_shader_parameter(&"ground_y", float(kn["ground_y"]) * field_scale)
mat.set_shader_parameter(&"band_lo", -0.6 * float(kn["relief"]) * field_scale)
mat.set_shader_parameter(&"band_hi", 0.6 * float(kn["relief"]) * field_scale)
return mat
## ---- the Terrain material panel (TerrainMaterial) ------------------------------
##
## Saved per map in the room: a durable world:* key, so every peer sees the same look and
## it survives a reload. Remote changes come back through state_changed.
const MATERIAL_KEY := "world:terrain:material"
var _tm: TerrainMaterial = null
var _tm_save_pending := false
var _tm_panel_id := ""
func _ready() -> void:
super()
var gs := get_node_or_null(^"/root/GameState")
if gs != null and gs.has_signal("state_changed"):
gs.state_changed.connect(_on_material_state)
if _tm != null:
_tm.save = _save_material
_register_material_panel.call_deferred()
func _exit_tree() -> void:
var dock := get_node_or_null(^"/root/BuildDock")
if dock != null and _tm_panel_id != "" and dock.has_method("unregister"):
dock.unregister(_tm_panel_id)
super._exit_tree()
func _saved_material() -> Variant:
var gs := get_node_or_null(^"/root/GameState")
return gs.get_state(MATERIAL_KEY) if gs != null else null
## A peer (or a late room sync) changed the look: apply it unless it is ours.
func _on_material_state(key: String, value: Variant, _ch: int) -> void:
if _tm == null or not (key == MATERIAL_KEY or key.ends_with(":" + MATERIAL_KEY)):
return
if value is Dictionary and value != _tm.cfg:
_tm.apply(TerrainMaterial.merged(TerrainMaterial.defaults(LAYER_SETS), value))
## Debounced: a slider drag is many changes, the room gets the last one.
func _save_material(_cfg: Dictionary) -> void:
if _tm_save_pending:
return
_tm_save_pending = true
get_tree().create_timer(0.4).timeout.connect(func() -> void:
_tm_save_pending = false
var gs := get_node_or_null(^"/root/GameState")
if gs != null and _tm != null:
gs.set_state(MATERIAL_KEY, _tm.cfg.duplicate(true)))
func _register_material_panel() -> void:
var dock := get_node_or_null(^"/root/BuildDock")
if dock == null or not dock.has_method("register") or _tm == null:
return
_tm_panel_id = "terrain_material_%s" % name
dock.register(_tm_panel_id, "Terrain material", _tm.build_ui(), false)
## High and back, looking down across several terraces: 200 m above the surface under
## it, from the field (a hard-coded height can land inside the rock — map_geiss_world's
## first start did).
func _camera_start() -> Array:
var kn := TerraceGraph.tknobs_for(TerraceGraph.load_terraces())
var y := _graph_y(func(x: float, z: float) -> float: return TerraceGraph.surface_at(x, z, kn), 0.0, 600.0)
return [Vector3(0.0, y + 200.0, 600.0), -20.0]