Files
settled-reach/client/scripts/rendering/entity_renderer.gd
T
jpmschweitzerandClaude Opus 4.6 f3793bf16e feat(client): D-033 entity colors, peripheral dimming, facing indicator (#130)
Replace hardcoded entity colors with D-033 relationship palette:
Player=#e0e8ff, Npc=unknown teal #4a9ebb, Object=grey #8b8ba0.
Phase 1 defaults by entity kind; Phase 2 (#361) will derive color
from RelationshipState via knowledge graph.

Entities in peripheral vision dimmed to 50% alpha (D-015).
Player entity gets a Polygon2D triangle indicator showing facing
direction, rotated from GameState.player_facing.

82 tests total, 0 failures.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 00:53:28 +01:00

145 lines
4.9 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}
#
# 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
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
func _ready() -> void:
print("EntityRenderer: Initialized")
# 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_SIZE, ENTITY_SIZE)
entity_node.pivot_offset = Vector2(ENTITY_SIZE / 2.0, ENTITY_SIZE / 2.0)
# D-033 color by entity kind (Phase 1 default)
# TODO(#361): derive from RelationshipState via knowledge graph
entity_node.color = _color_for_kind(entity_data)
add_child(entity_node)
entity_nodes[entity_id] = entity_node
# Add facing indicator for the player entity
if entity_id == GameState.player_entity_id:
_add_facing_indicator(entity_node)
_update_entity_node(entity_id, entity_data)
# Update an existing entity node (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
if entity_data.has("x") and entity_data.has("y"):
entity_node.position = Vector2(
entity_data.x * TILE_SIZE + ENTITY_OFFSET,
entity_data.y * TILE_SIZE + ENTITY_OFFSET
)
# v2: Peripheral vision dimming (D-015)
var visibility: Variant = entity_data.get("visibility")
if visibility == "Peripheral":
entity_node.modulate.a = Constants.PERIPHERAL_ALPHA
else:
entity_node.modulate.a = 1.0
# v2: Update facing indicator rotation (player entity only)
if entity_id == GameState.player_entity_id:
var indicator = entity_node.get_node_or_null("FacingIndicator")
if indicator != null:
indicator.rotation = _facing_to_rotation(GameState.player_facing)
# 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)
# D-033 color by entity kind (Phase 1: defaults by kind, not relationship)
static func _color_for_kind(entity_data: Dictionary) -> Color:
var kind_variant: String = entity_data.get("kind", {}).get("variant", "")
match kind_variant:
"Player":
return Constants.ENTITY_COLOR_PLAYER
"Npc":
return Constants.ENTITY_COLOR_UNKNOWN
"Object", "Terrain":
return Constants.ENTITY_COLOR_OBJECT
_:
return Constants.ENTITY_COLOR_OBJECT
# 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_SIZE / 2.0, ENTITY_SIZE / 2.0)
parent_node.add_child(indicator)
# Convert facing direction string to rotation in radians (0 = North/up)
static func _facing_to_rotation(facing: String) -> float:
match facing:
"North": return 0.0
"Northeast": return PI / 4.0
"East": return PI / 2.0
"Southeast": return 3.0 * PI / 4.0
"South": return PI
"Southwest": return 5.0 * PI / 4.0
"West": return 3.0 * PI / 2.0
"Northwest": return 7.0 * PI / 4.0
_: return 0.0