fix(client): entity lerp, fog shader safety, type fixes
Entity renderer: add framerate-independent position lerping so entities slide between tiles instead of snapping. Tuned for Sprint snappiness and Walk/Careful fluidity. Fog shader: set ColorRect to transparent fallback so a shader load failure doesn't paint solid white over the world. Track camera position (not player position) so fog stays synced during smooth camera pan. Restructure GLSL to avoid early return (some GPU drivers miscompile it). Minor type fixes: typed Array[Vector2] in cursor_renderer tick drawing, untyped Array in inventory_grid to avoid Godot typed-array cast issues. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -214,8 +214,8 @@ func _draw() -> void:
|
||||
|
||||
|
||||
func _draw_ticks(color: Color, thickness: float) -> void:
|
||||
var dirs := [Vector2.UP, Vector2.RIGHT, Vector2.DOWN, Vector2.LEFT]
|
||||
for dir in dirs:
|
||||
var dirs: Array[Vector2] = [Vector2.UP, Vector2.RIGHT, Vector2.DOWN, Vector2.LEFT]
|
||||
for dir: Vector2 in dirs:
|
||||
var d := dir.rotated(_rot)
|
||||
draw_line(d * _gap, d * (_gap + _len), color, thickness, true)
|
||||
|
||||
|
||||
@@ -5,6 +5,10 @@ extends Node2D
|
||||
# Creates/updates/removes ColorRect children based on entity data
|
||||
# Entity format (from Protocol v2): {entity_id, x, y, z, kind: {variant, data}, visibility}
|
||||
#
|
||||
# Position lerping: entity sprites smoothly slide between tiles instead of snapping.
|
||||
# The server moves entities in discrete tile steps; the lerp makes this look fluid.
|
||||
# Speed is tuned so Sprint feels snappy and Walk/Careful/Crouch feel deliberate.
|
||||
#
|
||||
# D-033 colors: Phase 1 defaults by entity kind. Phase 2 (#361) will derive
|
||||
# color from RelationshipState via the knowledge graph.
|
||||
|
||||
@@ -12,11 +16,31 @@ const TILE_SIZE: int = Constants.TILE_SIZE
|
||||
const ENTITY_SIZE: int = 24
|
||||
const ENTITY_OFFSET: float = (TILE_SIZE - ENTITY_SIZE) / 2.0 # center within tile
|
||||
|
||||
var entity_nodes: Dictionary = {} # entity_id -> Node2D mapping
|
||||
# Lerp speed — framerate-independent exponential smoothing.
|
||||
# At 12.0: ~70% there after 0.1s, ~95% after 0.25s.
|
||||
# Fast enough for Sprint snappiness, slow enough for Walk to show sliding.
|
||||
const LERP_SPEED: float = 12.0
|
||||
|
||||
var entity_nodes: Dictionary = {} # entity_id -> Node2D
|
||||
var _entity_targets: Dictionary = {} # entity_id -> Vector2 (target pixel position)
|
||||
|
||||
func _ready() -> void:
|
||||
print("EntityRenderer: Initialized")
|
||||
|
||||
|
||||
func _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)
|
||||
for entity_id in entity_nodes.keys():
|
||||
if not _entity_targets.has(entity_id):
|
||||
continue
|
||||
var node = entity_nodes[entity_id]
|
||||
var target: Vector2 = _entity_targets[entity_id]
|
||||
if not node.position.is_equal_approx(target):
|
||||
node.position = node.position.lerp(target, weight)
|
||||
|
||||
|
||||
# Update entities from snapshot data
|
||||
func update_entities(entities: Array) -> void:
|
||||
var active_ids: Array = []
|
||||
@@ -62,19 +86,28 @@ func _create_entity_node(entity_id: int, entity_data: Dictionary) -> void:
|
||||
if entity_id == GameState.player_entity_id:
|
||||
_add_facing_indicator(entity_node)
|
||||
|
||||
# Snap to initial position (no lerp on first appearance)
|
||||
if entity_data.has("x") and entity_data.has("y"):
|
||||
var target := Vector2(
|
||||
floorf(entity_data.x) * TILE_SIZE + ENTITY_OFFSET,
|
||||
floorf(entity_data.y) * TILE_SIZE + ENTITY_OFFSET
|
||||
)
|
||||
entity_node.position = target
|
||||
_entity_targets[entity_id] = target
|
||||
|
||||
_update_entity_node(entity_id, entity_data)
|
||||
|
||||
# Update an existing entity node (position, visibility dimming, facing)
|
||||
# Update an existing entity node (target position, visibility dimming, facing)
|
||||
func _update_entity_node(entity_id: int, entity_data: Dictionary) -> void:
|
||||
if not entity_nodes.has(entity_id):
|
||||
return
|
||||
|
||||
var entity_node = entity_nodes[entity_id]
|
||||
|
||||
# Update position from x, y fields (Protocol format), centered within tile.
|
||||
# Update target position — the lerp in _process() will smoothly move there.
|
||||
# Server sends tile-center coords (tile 16 → 16.5), floor to get tile index.
|
||||
if entity_data.has("x") and entity_data.has("y"):
|
||||
entity_node.position = Vector2(
|
||||
_entity_targets[entity_id] = Vector2(
|
||||
floorf(entity_data.x) * TILE_SIZE + ENTITY_OFFSET,
|
||||
floorf(entity_data.y) * TILE_SIZE + ENTITY_OFFSET
|
||||
)
|
||||
@@ -100,6 +133,7 @@ func _remove_entity_node(entity_id: int) -> void:
|
||||
var entity_node = entity_nodes[entity_id]
|
||||
entity_node.queue_free()
|
||||
entity_nodes.erase(entity_id)
|
||||
_entity_targets.erase(entity_id)
|
||||
|
||||
# D-033 color by entity kind — delegates to Constants.color_for_entity_kind
|
||||
static func _color_for_kind(entity_data: Dictionary) -> Color:
|
||||
|
||||
@@ -11,13 +11,18 @@ const TILE_SIZE := float(Constants.TILE_SIZE)
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
# Create the fog overlay ColorRect
|
||||
# Create the fog overlay ColorRect — transparent fallback so a shader failure
|
||||
# doesn't paint solid white over the entire world (z:900 covers everything).
|
||||
_fog_rect = ColorRect.new()
|
||||
_fog_rect.name = "FogRect"
|
||||
_fog_rect.color = Color.TRANSPARENT
|
||||
add_child(_fog_rect)
|
||||
|
||||
# Load shader and create material
|
||||
var shader := load("res://shaders/fog.gdshader") as Shader
|
||||
if shader == null:
|
||||
push_error("FogShader: failed to load res://shaders/fog.gdshader — fog disabled")
|
||||
return
|
||||
_shader_mat = ShaderMaterial.new()
|
||||
_shader_mat.shader = shader
|
||||
_fog_rect.material = _shader_mat
|
||||
@@ -38,14 +43,19 @@ func _ready() -> void:
|
||||
|
||||
|
||||
func update_fog() -> void:
|
||||
if _shader_mat == null:
|
||||
return # Shader failed to load — fog disabled
|
||||
|
||||
# 1. Update FogState textures from GameState
|
||||
FogState.update_from_state()
|
||||
|
||||
# 2. Position ColorRect to cover the current viewport
|
||||
# 2. Position ColorRect to cover the current viewport.
|
||||
# Use the camera's actual position (not player position) so the fog rect
|
||||
# tracks the smoothed camera and never desynchronizes during smooth pan.
|
||||
var vp_size := get_viewport().get_visible_rect().size
|
||||
var cam := get_viewport().get_camera_2d()
|
||||
var zoom := cam.zoom if cam else Vector2(2.0, 2.0)
|
||||
var camera_pos := GameState.player_position * TILE_SIZE
|
||||
var camera_pos := cam.global_position if cam else GameState.player_position * TILE_SIZE
|
||||
var half_view := vp_size / (2.0 * zoom)
|
||||
_fog_rect.position = camera_pos - half_view
|
||||
_fog_rect.size = vp_size / zoom
|
||||
|
||||
+30
-31
@@ -38,37 +38,36 @@ void fragment() {
|
||||
// Outside known map → unexplored
|
||||
if (tex_uv.x < 0.0 || tex_uv.x > 1.0 || tex_uv.y < 0.0 || tex_uv.y > 1.0) {
|
||||
COLOR = vec4(UNEXPLORED_COLOR, 1.0);
|
||||
return;
|
||||
}
|
||||
|
||||
float vis = texture(visibility_tex, tex_uv).r;
|
||||
float explored = texture(exploration_tex, tex_uv).r;
|
||||
|
||||
if (vis > PERIPHERAL_LOW) {
|
||||
// In or near vision cone
|
||||
if (vis > CLEAR_THRESHOLD) {
|
||||
// Layer 1: Clear — soft edge gradient
|
||||
float edge = smoothstep(CLEAR_THRESHOLD, 1.0, vis);
|
||||
COLOR = vec4(0.0, 0.0, 0.0, 1.0 - edge);
|
||||
} else {
|
||||
// Layer 2: Light fog (peripheral + forward edge)
|
||||
// D-059: animated Perlin noise, 8-10s cycle
|
||||
float noise_val = texture(noise_tex, tile * 0.03 + vec2(time * 0.11, time * 0.07)).r;
|
||||
float coverage = smoothstep(PERIPHERAL_LOW, CLEAR_THRESHOLD, vis);
|
||||
// Blend from heavy fog (alpha ~0.55) to lighter fog near clear edge
|
||||
float alpha = mix(0.55, 0.25, coverage) + noise_val * 0.1;
|
||||
COLOR = vec4(DARK_OVERLAY, alpha);
|
||||
}
|
||||
} else if (explored > 0.3) {
|
||||
// Layer 3: Deep fog (previously explored, no longer in LOS)
|
||||
// D-059: near-monochrome, ~10% zone temperature tint, 15-20s breathing cycle
|
||||
vec3 zone_tint = texture(zone_tint_tex, tex_uv).rgb;
|
||||
float noise_val = texture(noise_tex, tile * 0.015 + vec2(time * 0.045, time * 0.03)).r;
|
||||
vec3 tint_color = mix(vec3(0.04), zone_tint, 0.1);
|
||||
float alpha = mix(0.78, 0.90, noise_val); // Fog breathes
|
||||
COLOR = vec4(tint_color, alpha);
|
||||
} else {
|
||||
// Layer 5: Unexplored, no maps — information zero
|
||||
COLOR = vec4(UNEXPLORED_COLOR, 1.0);
|
||||
float vis = texture(visibility_tex, tex_uv).r;
|
||||
float explored = texture(exploration_tex, tex_uv).r;
|
||||
|
||||
if (vis > PERIPHERAL_LOW) {
|
||||
// In or near vision cone
|
||||
if (vis > CLEAR_THRESHOLD) {
|
||||
// Layer 1: Clear — soft edge gradient
|
||||
float edge = smoothstep(CLEAR_THRESHOLD, 1.0, vis);
|
||||
COLOR = vec4(0.0, 0.0, 0.0, 1.0 - edge);
|
||||
} else {
|
||||
// Layer 2: Light fog (peripheral + forward edge)
|
||||
// D-059: animated Perlin noise, 8-10s cycle
|
||||
float noise_val = texture(noise_tex, tile * 0.03 + vec2(time * 0.11, time * 0.07)).r;
|
||||
float coverage = smoothstep(PERIPHERAL_LOW, CLEAR_THRESHOLD, vis);
|
||||
// Blend from heavy fog (alpha ~0.55) to lighter fog near clear edge
|
||||
float alpha = mix(0.55, 0.25, coverage) + noise_val * 0.1;
|
||||
COLOR = vec4(DARK_OVERLAY, alpha);
|
||||
}
|
||||
} else if (explored > 0.3) {
|
||||
// Layer 3: Deep fog (previously explored, no longer in LOS)
|
||||
// D-059: near-monochrome, ~10% zone temperature tint, 15-20s breathing cycle
|
||||
vec3 zone_tint = texture(zone_tint_tex, tex_uv).rgb;
|
||||
float noise_val = texture(noise_tex, tile * 0.015 + vec2(time * 0.045, time * 0.03)).r;
|
||||
vec3 tint_color = mix(vec3(0.04), zone_tint, 0.1);
|
||||
float alpha = mix(0.78, 0.90, noise_val); // Fog breathes
|
||||
COLOR = vec4(tint_color, alpha);
|
||||
} else {
|
||||
// Layer 5: Unexplored, no maps — information zero
|
||||
COLOR = vec4(UNEXPLORED_COLOR, 1.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ const SLOT_TEXT := Color("#c8d0e0")
|
||||
const SLOT_TEXT_DIM := Color("#8b8ba0")
|
||||
const HOTKEY_SIZE := 10
|
||||
|
||||
var _slots: Array[Dictionary] = [] # [{item_id, name, slot}] from GameState
|
||||
var _slots: Array = [] # [{item_id, name, slot}] from GameState
|
||||
var _slot_nodes: Array[Control] = []
|
||||
var _selected_slot: int = -1
|
||||
|
||||
|
||||
Reference in New Issue
Block a user