From 34a21d4cd07893be5ff766cc6acf3e5fde10fe31 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Sun, 5 Apr 2026 11:51:30 +0200 Subject: [PATCH] =?UTF-8?q?feat(rendering):=20GameplayRenderer=20base=20cl?= =?UTF-8?q?ass=20=E2=80=94=20occlusion=20for=20all=20renderers=20(D-170)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New GameplayRenderer extends Node2D with built-in occlusion support. Subclasses override _gameplay_process() and _gameplay_draw() instead of _process() and _draw() — these are skipped when a fullscreen implant app covers gameplay. Migrated all 5 gameplay renderers: - CursorRenderer: hover detection + bracket drawing paused - EntityRenderer: position lerping paused - FogEntities: fog dot rendering paused - SoundIndicatorRenderer: directional arrows paused - WorldRenderer: tile/fog/entity updates paused No more manual occlusion wiring — the base class handles everything. Co-Authored-By: Claude Opus 4.6 (1M context) --- client/scripts/rendering/cursor_renderer.gd | 10 ++-- client/scripts/rendering/entity_renderer.gd | 6 +-- client/scripts/rendering/fog_entities.gd | 6 +-- client/scripts/rendering/gameplay_renderer.gd | 53 +++++++++++++++++++ .../rendering/sound_indicator_renderer.gd | 6 +-- client/scripts/rendering/world_renderer.gd | 12 ++--- 6 files changed, 70 insertions(+), 23 deletions(-) create mode 100644 client/scripts/rendering/gameplay_renderer.gd diff --git a/client/scripts/rendering/cursor_renderer.gd b/client/scripts/rendering/cursor_renderer.gd index b0b1748f5..d5c111b8d 100644 --- a/client/scripts/rendering/cursor_renderer.gd +++ b/client/scripts/rendering/cursor_renderer.gd @@ -1,5 +1,5 @@ class_name CursorRenderer -extends Node2D +extends GameplayRenderer ## Cursor state machine — 4 geometric states, 150ms linear transitions (D-056). ## Insert-styled cursor on z-layer 7 (UILayer CanvasLayer). @@ -54,7 +54,7 @@ var _bracket_a: float = 0.0 var _alpha: float = 0.45 -func _ready() -> void: +func _renderer_ready() -> void: Input.set_mouse_mode(Input.MOUSE_MODE_HIDDEN) z_index = 100 # D-170: cursor must always render above all HUD/implant layers _from = _snapshot() @@ -69,7 +69,7 @@ func _notification(what: int) -> void: visible = true -func _process(delta: float) -> void: +func _gameplay_process(delta: float) -> void: if not _mouse_inside: return _time += delta @@ -243,8 +243,8 @@ func _interpolate() -> void: # --- Drawing --- -func _draw() -> void: - # _hover_offset is computed in _detect_hover() during _process(), not recalculated +func _gameplay_draw() -> void: + # _hover_offset is computed in _detect_hover() during _gameplay_process(), not recalculated # here. This is safe because Camera2D (tree sibling, earlier in scene order) applies # its smoothing transform before CursorRenderer._process() reads canvas_transform. # Nothing modifies the canvas transform between _process() and _draw(). diff --git a/client/scripts/rendering/entity_renderer.gd b/client/scripts/rendering/entity_renderer.gd index e67894535..332216de5 100644 --- a/client/scripts/rendering/entity_renderer.gd +++ b/client/scripts/rendering/entity_renderer.gd @@ -1,5 +1,5 @@ class_name EntityRenderer -extends Node2D +extends GameplayRenderer # Entity renderer — manages entity sprites under the Entities node # Creates/updates/removes Sprite2D children based on entity data @@ -35,11 +35,11 @@ var _entity_tweens: Dictionary = {} # #521: entity_id -> {from: Color, target: var _entity_facing: Dictionary = {} # #540: entity_id -> String ("north"/"east"/"south"/"west") -func _ready() -> void: +func _renderer_ready() -> void: print("EntityRenderer: Initialized") -func _process(delta: float) -> void: +func _gameplay_process(delta: float) -> void: # Lerp all entity visual positions toward their targets each frame. # Uses framerate-independent exponential smoothing. var weight := 1.0 - exp(-LERP_SPEED * delta) diff --git a/client/scripts/rendering/fog_entities.gd b/client/scripts/rendering/fog_entities.gd index ec931a53f..342ed2ddc 100644 --- a/client/scripts/rendering/fog_entities.gd +++ b/client/scripts/rendering/fog_entities.gd @@ -1,5 +1,5 @@ class_name FogEntities -extends Node2D +extends GameplayRenderer ## Fog entity visualization — renders entities undergoing cognitive delay recognition ## in the fog layer. Between FogOverlay (z:900) and InsertOverlay (CanvasLayer 10). @@ -50,7 +50,7 @@ var _pings: Array = [] # [{pos: Vector2, elapsed: float}] var _time: float = 0.0 -func _process(delta: float) -> void: +func _gameplay_process(delta: float) -> void: # Early return when idle — skip queue_redraw() when nothing to draw if _entities.is_empty() and _pings.is_empty(): return @@ -118,7 +118,7 @@ func update_from_state() -> void: _entities.erase(eid) -func _draw() -> void: +func _gameplay_draw() -> void: # Breathing pulse — shared across all entities var pulse_t := fmod(_time, PULSE_PERIOD) / PULSE_PERIOD var pulse_alpha := lerpf(PULSE_MIN_ALPHA, PULSE_MAX_ALPHA, 0.5 + 0.5 * sin(pulse_t * TAU)) diff --git a/client/scripts/rendering/gameplay_renderer.gd b/client/scripts/rendering/gameplay_renderer.gd new file mode 100644 index 000000000..3edb56a6e --- /dev/null +++ b/client/scripts/rendering/gameplay_renderer.gd @@ -0,0 +1,53 @@ +class_name GameplayRenderer +extends Node2D +## Base class for renderers that should pause when a fullscreen implant app +## covers the gameplay view (D-170). +## +## Subclasses override _gameplay_process(delta) and _gameplay_draw() instead +## of _process() and _draw(). These are only called when gameplay is visible. +## +## Usage: +## extends GameplayRenderer +## func _gameplay_process(delta: float) -> void: ... +## func _gameplay_draw() -> void: ... + +var gameplay_occluded: bool = false + + +func _ready() -> void: + HudGroups.gameplay_occluded.connect(_on_gameplay_occluded) + _renderer_ready() + + +## Override this instead of _ready() in subclasses. +func _renderer_ready() -> void: + pass + + +func _on_gameplay_occluded(occluded: bool) -> void: + gameplay_occluded = occluded + if occluded: + # Force a blank redraw so stale frames don't linger + queue_redraw() + + +func _process(delta: float) -> void: + if gameplay_occluded: + return + _gameplay_process(delta) + + +func _draw() -> void: + if gameplay_occluded: + return + _gameplay_draw() + + +## Override in subclasses — called every frame when gameplay is visible. +func _gameplay_process(_delta: float) -> void: + pass + + +## Override in subclasses — called for draw when gameplay is visible. +func _gameplay_draw() -> void: + pass diff --git a/client/scripts/rendering/sound_indicator_renderer.gd b/client/scripts/rendering/sound_indicator_renderer.gd index 3c3ec7092..252c01979 100644 --- a/client/scripts/rendering/sound_indicator_renderer.gd +++ b/client/scripts/rendering/sound_indicator_renderer.gd @@ -1,5 +1,5 @@ class_name SoundIndicatorRenderer -extends Node2D +extends GameplayRenderer ## Medium-range sound indicators (#126, D-018). ## Renders directional arrows at the fog boundary for sounds outside LOS. @@ -35,7 +35,7 @@ const COLOR_DANGER: Color = Constants.ENTITY_COLOR_HOSTILE # Alert / threat / g var _indicators: Array = [] -func _process(delta: float) -> void: +func _gameplay_process(delta: float) -> void: if _indicators.is_empty(): return @@ -84,7 +84,7 @@ func update_sound_events(events: Array) -> void: queue_redraw() -func _draw() -> void: +func _gameplay_draw() -> void: if _indicators.is_empty(): return diff --git a/client/scripts/rendering/world_renderer.gd b/client/scripts/rendering/world_renderer.gd index 1b8572a9a..c7aeec906 100644 --- a/client/scripts/rendering/world_renderer.gd +++ b/client/scripts/rendering/world_renderer.gd @@ -1,4 +1,4 @@ -extends Node2D +extends GameplayRenderer # World renderer — manages all visual representation from GameState # Attached to the World node in main.tscn @@ -14,7 +14,6 @@ extends Node2D # FogOverlay (Node2D) — z:900 fog shader (OUTSIDE FogGroup) var _last_tick: int = -1 -var _occluded: bool = false # D-170: skip rendering when fullscreen implant app is covering us @onready var tile_renderer = $FogGroup/FloorTiles @onready var fog_renderer = $FogOverlay @@ -22,19 +21,14 @@ var _occluded: bool = false # D-170: skip rendering when fullscreen implant app @onready var sound_indicator_renderer = $SoundIndicators # #126 D-018 medium-range indicators -func _ready() -> void: - HudGroups.gameplay_occluded.connect(_on_gameplay_occluded) +func _renderer_ready() -> void: print("WorldRenderer: Initialized (D-049 z-stack)") -func _on_gameplay_occluded(occluded: bool) -> void: - _occluded = occluded - - # Called each frame to update visuals from game state. # Uses tick-based invalidation — re-renders all layers when a new snapshot arrives. func update_from_state() -> void: - if _occluded: + if gameplay_occluded: return # D-170: skip rendering while fullscreen implant app is active var tick := GameState.current_tick if tick == _last_tick: