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) # 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 # 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