feat(client): cutaway modes — corridor / bottom-walls-low / both, C-key live toggle (T-1088)

Live-session two-minds resolution: the trippy sightline corridor stays as a
mode (candidate signature look); mode 1 drops any wall with a KNOWN non-wall
tile on its camera-far side to the stub (X2 bottom-walls read — the player
sees the room without walking to it); mode 2 combines. Bottom-wall flag rides
MultiMesh INSTANCE_CUSTOM.r, updated on paint and when newly-learned floors
promote camera-ward walls; camera-far directions derived from the live
WorldRoot basis, never hand-derived. Default stays corridor pending the
feel verdict.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-06 19:16:14 +02:00
co-authored by Claude Fable 5
parent 2db7dd25e1
commit e32a041778
4 changed files with 113 additions and 4 deletions
+74 -3
View File
@@ -155,6 +155,11 @@ var _free_slots: Array = [[], []]
var _state_colors: Array[Color] = []
var _last_tick: int = -1
var _capacity_warned := false
## Cutaway mode: 0 = sightline corridor, 1 = bottom walls low, 2 = both.
var cutaway_mode: int = 0
## Camera-far sim-neighbor deltas for the bottom-wall rule (see
## _compute_behind_dirs — derived from the live WorldRoot basis).
var _behind_dirs: Array[Vector3i] = []
func _ready() -> void:
@@ -190,7 +195,12 @@ func _ready() -> void:
_wall_material.set_shader_parameter("u_cut_height", SandboxConstants.CUT_HEIGHT)
_wall_material.set_shader_parameter("u_corridor_half_w", SandboxConstants.CUT_CORRIDOR_HALF_W)
_wall_material.set_shader_parameter("u_cut_band", SandboxConstants.CUT_BAND)
_wall_mm = _make_layer("WallMM", wall_mesh, _wall_material)
_wall_mm = _make_layer("WallMM", wall_mesh, _wall_material, true)
# Camera-far neighbor directions for the bottom-wall rule — derived from the
# actual WorldRoot basis at runtime (never hand-derived; the E/W-mirror
# lesson). A wall is a "bottom wall" when a KNOWN non-wall tile sits on its
# camera-far side. Deferred: global_transform is not final until tree-ready.
_compute_behind_dirs.call_deferred()
## Push the world->sim transform into the floor grid shader (deferred from
@@ -227,13 +237,17 @@ func set_cutaway_char_pos(world_pos: Vector3, reach_m: float) -> void:
_wall_material.set_shader_parameter("u_cut_reach", reach_m)
func _make_layer(layer_name: String, mesh: Mesh, material: ShaderMaterial) -> MultiMeshInstance3D:
func _make_layer(
layer_name: String, mesh: Mesh, material: ShaderMaterial, with_custom := false
) -> MultiMeshInstance3D:
var mm := MultiMesh.new()
# Format flags must be set before instance_count (buffer layout is fixed at
# allocation). use_colors routes set_instance_color() to the shaders' COLOR
# built-in, which multiplies albedo (§3.2).
# built-in, which multiplies albedo (§3.2). use_custom_data (walls only)
# carries the bottom-wall flag to INSTANCE_CUSTOM.r for the cutaway modes.
mm.transform_format = MultiMesh.TRANSFORM_3D
mm.use_colors = true
mm.use_custom_data = with_custom
mm.mesh = mesh
mm.instance_count = INSTANCE_CAPACITY
for i in INSTANCE_CAPACITY:
@@ -267,6 +281,63 @@ func _paint_new(key: Vector3i) -> void:
mm.set_instance_transform(slot, _tile_xform(key, kind))
mm.set_instance_color(slot, _state_colors[store.state_of(key)])
_slots[key] = Vector2i(kind, slot)
if kind == Store.Kind.WALL:
_refresh_bottom_flag(key)
else:
# A newly-learned floor can promote camera-ward walls to bottom walls.
for d in _behind_dirs:
_refresh_bottom_flag(key - d)
## Cutaway mode (design amendment, live session 2026-07-06): 0 = sightline
## corridor, 1 = bottom walls low, 2 = both. Pushed to the wall shader.
func set_cutaway_mode(mode: int) -> void:
cutaway_mode = mode % 3
_wall_material.set_shader_parameter("u_cutaway_mode", cutaway_mode)
print("GreyboxWorld: cutaway mode %d (%s)" % [
cutaway_mode, ["sightline corridor", "bottom walls low", "both"][cutaway_mode]
])
# Dev affordance (same pattern as the camera's T tilt cycle): C cycles the
# cutaway mode live so the corridor-vs-bottom-walls call is made by feel.
func _unhandled_input(event: InputEvent) -> void:
if event is InputEventKey:
var key := event as InputEventKey
if key.pressed and not key.echo and key.physical_keycode == KEY_C:
set_cutaway_mode(cutaway_mode + 1)
get_viewport().set_input_as_handled()
## World-space camera-far sim directions (as tile-key deltas): the wall face
## pointing toward the camera hides the tile at key + behind_dir. Computed from
## this node's global basis so the WorldRoot rotation can never desync it.
func _compute_behind_dirs() -> void:
_behind_dirs.clear()
for d: Vector3i in [Vector3i(1, 0, 0), Vector3i(-1, 0, 0), Vector3i(0, 1, 0), Vector3i(0, -1, 0)]:
# Sim x -> local X, sim y -> local Z (SandboxSpace convention).
var world_dir := global_transform.basis * Vector3(float(d.x), 0.0, float(d.y))
# Camera looks world -Z, so a camera-ward face points +Z; the tile it
# hides lies OPPOSITE that face -> behind_dir = -d for camera-ward d.
if world_dir.z > 0.01:
_behind_dirs.append(-d)
# Repaint flags for walls painted before the deferred computation ran.
for key: Vector3i in _slots:
if (_slots[key] as Vector2i).x == Store.Kind.WALL:
_refresh_bottom_flag(key)
func _refresh_bottom_flag(key: Vector3i) -> void:
var slot: Vector2i = _slots.get(key, Vector2i(-1, -1))
if slot.y < 0 or slot.x != Store.Kind.WALL:
return
var flag := 0.0
for d in _behind_dirs:
var n: Vector3i = key + d
if store.has_tile(n) and store.kind_of(n) != Store.Kind.WALL:
flag = 1.0
break
_wall_mm.multimesh.set_instance_custom_data(slot.y, Color(flag, 0.0, 0.0, 0.0))
func _repaint_color(key: Vector3i) -> void: