feat(rendering): GameplayRenderer base class — occlusion for all renderers (D-170)
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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().
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user