diff --git a/client/scripts/sandbox/greybox_world.gd b/client/scripts/sandbox/greybox_world.gd index 5ca4a7bc6..caf8a6784 100644 --- a/client/scripts/sandbox/greybox_world.gd +++ b/client/scripts/sandbox/greybox_world.gd @@ -155,6 +155,10 @@ var _free_slots: Array = [[], []] var _state_colors: Array[Color] = [] var _last_tick: int = -1 var _capacity_warned := false +## Bottom-wall scan depth (tiles) along each camera-far direction — covers the +## Gauntlet's 2-thick walls plus door trim. +const BOTTOM_SCAN_DEPTH := 3 + ## 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 @@ -284,10 +288,13 @@ func _paint_new(key: Vector3i) -> void: _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) + # Any newly-learned tile can promote camera-ward walls up the scan chain: + # a floor is the promotion itself; a wall can EXTEND a chain that previously + # stopped at unknown (Gauntlet walls are 2 tiles thick — the outer row sees + # floor only THROUGH the inner row). + for d in _behind_dirs: + for step in range(1, BOTTOM_SCAN_DEPTH + 1): + _refresh_bottom_flag(key - d * step) ## Cutaway mode (design amendment, live session 2026-07-06): 0 = sightline @@ -322,6 +329,11 @@ func _compute_behind_dirs() -> void: # hides lies OPPOSITE that face -> behind_dir = -d for camera-ward d. if world_dir.z > 0.01: _behind_dirs.append(-d) + # Corner pillars: at a wall-run junction the interior floor sits DIAGONALLY + # behind (neither straight scan line reaches it) — include the combined + # diagonal in the scan set. + if _behind_dirs.size() == 2: + _behind_dirs.append(_behind_dirs[0] + _behind_dirs[1]) # Repaint flags for walls painted before the deferred computation ran. for key: Vector3i in _slots: if (_slots[key] as Vector2i).x == Store.Kind.WALL: @@ -332,11 +344,20 @@ 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 + # Scan up to BOTTOM_SCAN_DEPTH tiles along each camera-far direction, + # walking THROUGH consecutive known walls (Gauntlet walls are 2 thick): + # a known non-wall tile behind the wall run marks the whole run "bottom". + # Unknown tiles stop the scan — no assumptions past the info boundary. 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 + for step in range(1, BOTTOM_SCAN_DEPTH + 1): + var n: Vector3i = key + d * step + if not store.has_tile(n): + break + if store.kind_of(n) != Store.Kind.WALL: + flag = 1.0 + break + if flag > 0.0: break _wall_mm.multimesh.set_instance_custom_data(slot.y, Color(flag, 0.0, 0.0, 0.0)) diff --git a/client/shaders/sandbox/greybox_wall_cutaway.gdshader b/client/shaders/sandbox/greybox_wall_cutaway.gdshader index 882dc98b5..73f6f994d 100644 --- a/client/shaders/sandbox/greybox_wall_cutaway.gdshader +++ b/client/shaders/sandbox/greybox_wall_cutaway.gdshader @@ -38,19 +38,17 @@ uniform int u_cutaway_mode = 0; uniform vec3 u_base_albedo : source_color = vec3(0.66, 0.64, 0.60); varying vec3 v_world_pos; -// Bottom-wall flag (INSTANCE_CUSTOM.r, set by GreyboxWorld): 1.0 when a KNOWN -// non-wall tile sits on this wall's camera-far side — the wall would occlude -// interior the player should see, X2-style. -varying flat float v_bottom; +// Whole-box cut decision, computed per-INSTANCE in vertex() from the box's tile +// center — every wall box is either full height or the stub, no per-fragment +// falloff curves carving organic notches (the "sphere" read). X2-crisp tile +// cuts; the smoothstep bands only shape where the 0.5 threshold falls. +varying flat float v_cut; void vertex() { - // MODEL_MATRIX folds in the per-instance MultiMesh transform. - v_world_pos = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz; - v_bottom = INSTANCE_CUSTOM.r; -} - -void fragment() { - vec2 to_wall = v_world_pos.xz - u_char_pos_xz; + // MODEL_MATRIX folds in the per-instance MultiMesh transform; its origin + // is the box center — .xz is the wall tile's center in world space. + vec2 inst_xz = (MODEL_MATRIX * vec4(0.0, 0.0, 0.0, 1.0)).xz; + vec2 to_wall = inst_xz - u_char_pos_xz; // Lateral gate: full cut inside (half_w - band), fading to none at half_w. float lateral = 1.0 - smoothstep(u_corridor_half_w - u_cut_band, u_corridor_half_w, abs(to_wall.x)); // Near gate: soft start just behind the character (no hard seam at their feet). @@ -58,13 +56,20 @@ void fragment() { // Far gate: corridor ends where a WALL_H wall can no longer occlude. float far_f = 1.0 - smoothstep(u_cut_reach, u_cut_reach + u_cut_band, to_wall.y); float corridor_f = lateral * near_f * far_f; + // Bottom-wall flag (INSTANCE_CUSTOM.r, GreyboxWorld): KNOWN non-wall tile + // behind this wall run — it would occlude interior the player should see. float cut_f = corridor_f; if (u_cutaway_mode == 1) { - cut_f = v_bottom; + cut_f = INSTANCE_CUSTOM.r; } else if (u_cutaway_mode == 2) { - cut_f = max(corridor_f, v_bottom); + cut_f = max(corridor_f, INSTANCE_CUSTOM.r); } - float allowed = mix(u_wall_h, u_cut_height, cut_f); + v_cut = cut_f > 0.5 ? 1.0 : 0.0; + v_world_pos = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz; +} + +void fragment() { + float allowed = mix(u_wall_h, u_cut_height, v_cut); if (v_world_pos.y > allowed) { discard; }