Remove stale smoothing re-enable comments from main.gd (Hoshe #1). Add DIALOGUE_MAX_WIDTH=640 regression test (Hoshe #2). Extract GROUND_FLOOR const in tile_renderer (Tyre #3). Clean up entity_renderer migration comment (Hoshe #3). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
203 lines
8.0 KiB
GDScript
203 lines
8.0 KiB
GDScript
class_name EntityRenderer
|
|
extends Node2D
|
|
|
|
# Entity renderer — manages entity sprites under the Entities node
|
|
# 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.
|
|
|
|
const TILE_SIZE: int = Constants.TILE_SIZE
|
|
# D-044: 24x32 entity footprint within 32x32 visual tile (64x64 source scaled to 32px runtime)
|
|
const ENTITY_WIDTH: int = 24
|
|
const ENTITY_HEIGHT: int = 32
|
|
const ENTITY_OFFSET_X: float = (TILE_SIZE - ENTITY_WIDTH) / 2.0 # center horizontally
|
|
const ENTITY_OFFSET_Y: float = (TILE_SIZE - ENTITY_HEIGHT) / 2.0 # center vertically for placeholder. Migration: switch to bottom-anchor (offset = TILE_SIZE - ENTITY_HEIGHT) when real sprites land for correct y-sort ordering.
|
|
|
|
# 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)
|
|
var _entity_relationships: Dictionary = {} # #521: entity_id -> String (last relationship)
|
|
var _entity_tweens: Dictionary = {} # #521: entity_id -> {target: Color, elapsed: float}
|
|
|
|
# #521: Color transition duration in seconds (D-033: "0.5s fade")
|
|
const COLOR_FADE_DURATION: float = 0.5
|
|
|
|
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)
|
|
|
|
# #521: Advance color transitions (manual lerp, testable without SceneTree)
|
|
var finished_ids: Array = []
|
|
for entity_id in _entity_tweens.keys():
|
|
if not entity_nodes.has(entity_id):
|
|
finished_ids.append(entity_id)
|
|
continue
|
|
var tween_data: Dictionary = _entity_tweens[entity_id]
|
|
tween_data.elapsed += delta
|
|
var t := clampf(tween_data.elapsed / COLOR_FADE_DURATION, 0.0, 1.0)
|
|
var node_c: ColorRect = entity_nodes[entity_id] as ColorRect
|
|
node_c.color = tween_data.from.lerp(tween_data.target, t)
|
|
if t >= 1.0:
|
|
finished_ids.append(entity_id)
|
|
for eid in finished_ids:
|
|
_entity_tweens.erase(eid)
|
|
|
|
|
|
# Update entities from snapshot data
|
|
func update_entities(entities: Array) -> void:
|
|
var active_ids: Array = []
|
|
|
|
# Create or update entities
|
|
for entity_data in entities:
|
|
if not entity_data.has("entity_id"):
|
|
continue
|
|
|
|
var entity_id = entity_data.entity_id
|
|
active_ids.append(entity_id)
|
|
|
|
# Create entity node if it doesn't exist
|
|
if not entity_nodes.has(entity_id):
|
|
_create_entity_node(entity_id, entity_data)
|
|
else:
|
|
_update_entity_node(entity_id, entity_data)
|
|
|
|
# Remove entities that are no longer visible
|
|
var ids_to_remove: Array = []
|
|
for entity_id in entity_nodes.keys():
|
|
if entity_id not in active_ids:
|
|
ids_to_remove.append(entity_id)
|
|
|
|
for entity_id in ids_to_remove:
|
|
_remove_entity_node(entity_id)
|
|
|
|
# Create a new entity node with D-033 color and optional facing indicator
|
|
func _create_entity_node(entity_id: int, entity_data: Dictionary) -> void:
|
|
var entity_node = ColorRect.new()
|
|
entity_node.name = "Entity_" + str(entity_id)
|
|
entity_node.size = Vector2(ENTITY_WIDTH, ENTITY_HEIGHT)
|
|
entity_node.pivot_offset = Vector2(ENTITY_WIDTH / 2.0, ENTITY_HEIGHT / 2.0)
|
|
|
|
# D-033 color by relationship (#521)
|
|
entity_node.color = _color_for_kind(entity_data)
|
|
|
|
add_child(entity_node)
|
|
entity_nodes[entity_id] = entity_node
|
|
_entity_relationships[entity_id] = entity_data.get("relationship", "Unknown")
|
|
|
|
# Add facing indicator for the player entity
|
|
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_X,
|
|
floorf(entity_data.y) * TILE_SIZE + ENTITY_OFFSET_Y
|
|
)
|
|
entity_node.position = target
|
|
_entity_targets[entity_id] = target
|
|
|
|
_update_entity_node(entity_id, entity_data)
|
|
|
|
# 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 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_targets[entity_id] = Vector2(
|
|
floorf(entity_data.x) * TILE_SIZE + ENTITY_OFFSET_X,
|
|
floorf(entity_data.y) * TILE_SIZE + ENTITY_OFFSET_Y
|
|
)
|
|
|
|
# #521: Detect relationship change → fade D-033 color (0.5s via _process)
|
|
var new_rel: String = entity_data.get("relationship", "Unknown")
|
|
var old_rel: String = _entity_relationships.get(entity_id, "Unknown")
|
|
if new_rel != old_rel:
|
|
_entity_relationships[entity_id] = new_rel
|
|
var new_color := _color_for_kind(entity_data)
|
|
_entity_tweens[entity_id] = {
|
|
"from": entity_node.color,
|
|
"target": new_color,
|
|
"elapsed": 0.0,
|
|
}
|
|
|
|
# Note: modulate.a (peripheral dimming below) and color (D-033 tint above)
|
|
# are compositionally independent — both can change simultaneously without
|
|
# interference. If alpha tweening is added later, coordinate with color tween.
|
|
|
|
# v2: Peripheral vision dimming (D-015)
|
|
# null visibility (v1 backward compat) defaults to full alpha
|
|
var visibility: Variant = entity_data.get("visibility")
|
|
var target_alpha := Constants.PERIPHERAL_ALPHA if visibility == "Peripheral" else 1.0
|
|
if not is_equal_approx(entity_node.modulate.a, target_alpha):
|
|
entity_node.modulate.a = target_alpha
|
|
|
|
# D-054: Update facing indicator from client-side mouse angle (not server).
|
|
# InputMapper.facing_angle is a continuous float — smoother than octant snapping.
|
|
if entity_id == GameState.player_entity_id:
|
|
var indicator = entity_node.get_node_or_null("FacingIndicator")
|
|
if indicator != null:
|
|
# facing_angle: 0=East, -PI/2=North. Indicator: 0=North (up).
|
|
# Rotate from North basis: add PI/2 to convert.
|
|
indicator.rotation = InputMapper.facing_angle + PI / 2.0
|
|
|
|
# Remove an entity node
|
|
func _remove_entity_node(entity_id: int) -> void:
|
|
if not entity_nodes.has(entity_id):
|
|
return
|
|
|
|
var entity_node = entity_nodes[entity_id]
|
|
entity_node.queue_free()
|
|
entity_nodes.erase(entity_id)
|
|
_entity_targets.erase(entity_id)
|
|
_entity_relationships.erase(entity_id)
|
|
_entity_tweens.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:
|
|
return Constants.color_for_entity_kind(entity_data)
|
|
|
|
# Add a facing direction indicator triangle to the player entity
|
|
func _add_facing_indicator(parent_node: Control) -> void:
|
|
var indicator := Polygon2D.new()
|
|
indicator.name = "FacingIndicator"
|
|
var s := Constants.FACING_INDICATOR_SIZE
|
|
var offset := Constants.FACING_INDICATOR_OFFSET
|
|
# Triangle pointing up (North), offset from center. Rotates around (0,0).
|
|
indicator.polygon = PackedVector2Array([
|
|
Vector2(0, -offset - s),
|
|
Vector2(-s * 0.6, -offset + s * 0.4),
|
|
Vector2(s * 0.6, -offset + s * 0.4),
|
|
])
|
|
indicator.color = Constants.ENTITY_COLOR_PLAYER
|
|
# Position at center of parent ColorRect — rotation around this point
|
|
indicator.position = Vector2(ENTITY_WIDTH / 2.0, ENTITY_HEIGHT / 2.0)
|
|
parent_node.add_child(indicator)
|