Merge remote-tracking branch 'origin/client'

# Conflicts:
#	CHANGELOG.md
This commit is contained in:
2026-02-12 01:16:02 +01:00
8 changed files with 409 additions and 14 deletions
+10
View File
@@ -6,9 +6,19 @@ Format based on [Keep a Changelog](https://keepachangelog.com/).
## [Unreleased]
### Fixed
- Type safety in GameState visible_tiles loop — validates Dictionary with x/y keys before access (PR #11 review)
- Entity renderer skips redundant modulate.a writes when alpha unchanged (PR #11 review)
### Added
- Knowledge graph system (#361, #362, #363, #365) — per-entity KnowledgeGraph component (D-041), StableEntityId + EntityRegistry, KnowledgeEventQueue, decay system, 4-level confidence hierarchy
- Direct observation knowledge flow (#364) — perception emits DirectObservation/LeftLOS events to knowledge graph, entities entering/leaving LOS tracked
- Protocol v2 decoder — extracts game_time, player_facing, visible_tiles, and per-entity visibility sectors from ObserverSnapshot v2
- D-033 entity color palette (#130) — relationship-based colors (teal/green/amber/red), Phase 1 defaults by entity kind
- Peripheral vision dimming — entities in peripheral vision rendered at 50% alpha (D-015)
- Player facing direction indicator — Polygon2D triangle on player entity showing 8-directional facing
- GameState v2 fields — game_time, player_facing, visibility_sectors stored from snapshot data
- Test snapshot updated to v2 format with visibility sectors, game_time, and player_facing
- Observer visibility query (#112) — replaces unfiltered generate_snapshot with LOS-filtered compute_observer_snapshot combining shadowcasting + vision cone
- Vision cone system (#111) — forward/peripheral/blind sectors per D-015, Facing component updated on movement
- Symmetric shadowcasting (#110, #359) — Albert Ford algorithm with rational fraction slopes, benchmarked 1.2-10.5x faster than recursive, symmetry guaranteed (D-035)
+29
View File
@@ -10,6 +10,11 @@ var visible_entities: Array = []
var visible_tiles: Array = []
var visible_positions: Dictionary = {} # Vector2i -> true, for fast fog lookups
# v2 fields (D-015, D-031)
var game_time: Dictionary = {} # {day, time_of_day, day_phase, paused} or empty
var player_facing: String = "North" # 8-directional facing direction
var visibility_sectors: Dictionary = {} # Vector2i -> "Forward"/"Peripheral"
# Player entity ID — the first entity is assumed to be the player (will be
# refined when the server assigns explicit player entity IDs).
var player_entity_id: int = 1
@@ -40,3 +45,27 @@ func apply_snapshot(snapshot: Dictionary) -> void:
visible_positions.clear()
for pos in snapshot.visible_positions:
visible_positions[Vector2i(pos.x, pos.y)] = true
# v2: game_time (D-031)
if snapshot.has("game_time") and snapshot.game_time is Dictionary:
game_time = snapshot.game_time
# v2: player_facing (D-015)
if snapshot.has("player_facing") and snapshot.player_facing is String:
player_facing = snapshot.player_facing
# v2: visible_tiles with visibility sectors
# Derives visible_positions when not explicitly provided (real server mode)
if snapshot.has("visible_tiles") and snapshot.visible_tiles is Array and snapshot.visible_tiles.size() > 0:
visibility_sectors.clear()
var has_explicit_positions := snapshot.has("visible_positions")
if not has_explicit_positions:
visible_positions.clear()
for vtile in snapshot.visible_tiles:
if not vtile is Dictionary or not vtile.has("x") or not vtile.has("y"):
continue
var pos := Vector2i(vtile.x, vtile.y)
if vtile.has("visibility"):
visibility_sectors[pos] = vtile.visibility
if not has_explicit_positions:
visible_positions[pos] = true
+32
View File
@@ -240,6 +240,14 @@ func _test_snapshot() -> Dictionary:
_test_tick += 1
return {
"tick": _test_tick,
"version": 2,
"game_time": {
"day": 0,
"time_of_day": 0,
"day_phase": "Morning",
"paused": false,
},
"player_facing": "North",
"entities": [
{
"entity_id": 1,
@@ -247,6 +255,7 @@ func _test_snapshot() -> Dictionary:
"y": 10.0,
"z": 0,
"kind": { "variant": "Player", "data": null },
"visibility": "Forward",
},
{
"entity_id": 2,
@@ -254,9 +263,11 @@ func _test_snapshot() -> Dictionary:
"y": 10.0,
"z": 0,
"kind": { "variant": "Npc", "data": null },
"visibility": "Peripheral",
},
],
"tiles": _test_tiles(),
"visible_tiles": _test_visible_tiles(),
"visible_positions": _test_visible_positions(),
}
@@ -292,6 +303,27 @@ func _test_tiles() -> Array:
return tiles
# Test visible tiles with visibility sectors (v2 format)
# Tiles ahead of the player (y <= player_y) are Forward, others Peripheral.
func _test_visible_tiles() -> Array:
var vtiles: Array = []
var player_x := 10
var player_y := 10
var radius := 4
var room_x := 7
var room_y := 7
var room_w := 8
var room_h := 8
for x in range(player_x - radius, player_x + radius + 1):
for y in range(player_y - radius, player_y + radius + 1):
var dist := absf(x - player_x) + absf(y - player_y)
if dist <= radius:
if x >= room_x and x < room_x + room_w and y >= room_y and y < room_y + room_h:
var sector: String = "Forward" if y <= player_y else "Peripheral"
vtiles.append({"x": x, "y": y, "z": 0, "visibility": sector})
return vtiles
# Test visibility: player at (10,10) can see tiles within radius 4, blocked by walls
func _test_visible_positions() -> Array:
var positions: Array = []
+18
View File
@@ -4,3 +4,21 @@ class_name Constants
## Tile size in pixels — all renderers and coordinate conversions use this.
const TILE_SIZE: int = 32
# D-033: Entity relationship color palette
# Color represents the player's RELATIONSHIP to the entity, not an objective property.
# Phase 1: default colors mapped by entity kind (Player/Npc/Object/Terrain).
# Phase 2 (#361): colors derived from RelationshipState via the knowledge graph.
const ENTITY_COLOR_UNKNOWN: Color = Color("#4a9ebb") # Unknown/Neutral — cool teal
const ENTITY_COLOR_FRIENDLY: Color = Color("#6bc9a6") # Known/Friendly — soft green
const ENTITY_COLOR_POI: Color = Color("#e8c547") # Person of Interest — warm amber
const ENTITY_COLOR_HOSTILE: Color = Color("#d45d5d") # Hostile/Dangerous — muted red
const ENTITY_COLOR_OBJECT: Color = Color("#8b8ba0") # Static objects — muted grey
const ENTITY_COLOR_PLAYER: Color = Color("#e0e8ff") # Player character (detective)
# D-015: Peripheral vision dimming
const PERIPHERAL_ALPHA: float = 0.5
# Facing direction indicator
const FACING_INDICATOR_SIZE: float = 6.0
const FACING_INDICATOR_OFFSET: float = 14.0
+42 -1
View File
@@ -13,7 +13,9 @@ class_name Protocol
# -- Decode: bytes from server → GDScript types --------------------------------
## Decode an ObserverSnapshot from MessagePack bytes.
## Returns { "tick": int, "entities": Array[Dictionary] } or null on error.
## Returns decoded snapshot Dictionary or null on error.
## v2 fields (version, game_time, player_facing, visible_tiles) default to null/empty
## when decoding v1 snapshots for backward compatibility.
static func decode_snapshot(bytes: PackedByteArray) -> Variant:
var result = Messagepack.decode(bytes)
if result.status != null:
@@ -41,10 +43,41 @@ static func decode_snapshot(bytes: PackedByteArray) -> Variant:
# GDScript int is signed 64-bit. Rust tick is u64 but will not exceed 2^63
# in any realistic scenario (would require ~29 billion years at 10 ticks/game-minute per D-031).
var tick: int = raw["tick"]
# v2 fields — optional for backward compatibility
var version: Variant = raw.get("version")
var game_time: Variant = raw.get("game_time")
# player_facing: FacingDirection is a unit enum → bare string in rmp_serde
var player_facing: Variant = null
var raw_facing: Variant = raw.get("player_facing")
if raw_facing is String:
player_facing = raw_facing
# visible_tiles: Array of {x, y, z, visibility}
var visible_tiles: Array = []
var raw_vtiles: Variant = raw.get("visible_tiles")
if raw_vtiles is Array:
for raw_tile in raw_vtiles:
if raw_tile is Dictionary and raw_tile.has("x") and raw_tile.has("y") and raw_tile.has("z"):
var tile_entry := {
"x": int(raw_tile["x"]),
"y": int(raw_tile["y"]),
"z": int(raw_tile["z"]),
}
var vis: Variant = raw_tile.get("visibility")
if vis is String:
tile_entry["visibility"] = vis
visible_tiles.append(tile_entry)
return {
"tick": tick,
"entities": entities,
"decode_errors": dropped,
"version": version,
"game_time": game_time,
"player_facing": player_facing,
"visible_tiles": visible_tiles,
}
@@ -56,12 +89,20 @@ static func _decode_entity(raw: Dictionary) -> Variant:
return null
var entity_id: int = raw["entity_id"]
# v2: visibility sector (Forward/Peripheral). null for v1 entities.
var visibility: Variant = null
var raw_vis: Variant = raw.get("visibility")
if raw_vis is String:
visibility = raw_vis
return {
"entity_id": entity_id,
"x": float(raw["x"]),
"y": float(raw["y"]),
"z": int(raw["z"]),
"kind": _decode_enum_variant(raw["kind"]),
"visibility": visibility,
}
+69 -13
View File
@@ -3,7 +3,10 @@ extends Node2D
# Entity renderer — manages entity sprites under the Entities node
# Creates/updates/removes ColorRect children based on entity data
# Entity format (from Protocol): {entity_id, x, y, z, kind: {variant, 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
@@ -41,30 +44,27 @@ func update_entities(entities: Array) -> void:
for entity_id in ids_to_remove:
_remove_entity_node(entity_id)
# Create a new entity node (placeholder visual)
# 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)
# Color based on entity kind (from Protocol decoded format).
# TODO(#130): replace with D-033 relationship colors (teal/green/amber/red).
var kind_variant: String = entity_data.get("kind", {}).get("variant", "")
match kind_variant:
"Npc":
entity_node.color = Color(0.3, 0.6, 0.9) # Placeholder blue
"Player":
entity_node.color = Color(0.88, 0.91, 1.0) # Placeholder light blue
_:
entity_node.color = Color(0.8, 0.8, 0.8) # Gray for unknown
# 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
# 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
@@ -78,6 +78,19 @@ func _update_entity_node(entity_id: int, entity_data: Dictionary) -> void:
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):
@@ -86,3 +99,46 @@ func _remove_entity_node(entity_id: int) -> void:
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
+74
View File
@@ -271,6 +271,80 @@ func test_encode_player_inputs_empty() -> void:
assert_that(raw.value.size()).is_equal(0)
# -- v2 snapshot decoding -------------------------------------------------------
func test_decode_snapshot_v2_full() -> void:
var bytes = _load_fixture("snapshot_v2_full")
var snapshot = Protocol.decode_snapshot(bytes)
assert_that(snapshot).is_not_null()
assert_that(snapshot.tick).is_equal(500)
assert_that(snapshot.version).is_equal(2)
# game_time
assert_that(snapshot.game_time).is_not_null()
assert_that(snapshot.game_time.day).is_equal(1)
assert_that(snapshot.game_time.time_of_day).is_equal(720)
assert_that(snapshot.game_time.paused).is_false()
# player_facing (unit enum → bare string)
assert_that(snapshot.player_facing).is_equal("Southeast")
# entities with visibility sector
assert_that(snapshot.entities.size()).is_equal(1)
assert_that(snapshot.entities[0].visibility).is_equal("Forward")
# visible_tiles
assert_that(snapshot.visible_tiles.size()).is_equal(3)
assert_that(snapshot.visible_tiles[0].x).is_equal(10)
assert_that(snapshot.visible_tiles[0].visibility).is_equal("Forward")
assert_that(snapshot.visible_tiles[1].visibility).is_equal("Peripheral")
func test_existing_fixtures_have_v2_fields() -> void:
# All fixtures are generated by v2 fixture_snapshot() — verify decoder extracts v2 fields
for fixture_name in ["snapshot_one_npc", "snapshot_empty", "snapshot_player", "snapshot_multi_entity"]:
var bytes = _load_fixture(fixture_name)
var snapshot = Protocol.decode_snapshot(bytes)
assert_that(snapshot).is_not_null()
assert_that(snapshot.version).is_equal(2)
assert_that(snapshot.player_facing).is_equal("North")
assert_that(snapshot.game_time).is_not_null()
func test_multi_entity_visibility_sectors() -> void:
# snapshot_multi_entity has: Player=Forward, Npc=Peripheral, Object=Forward, Terrain=Forward
var bytes = _load_fixture("snapshot_multi_entity")
var snapshot = Protocol.decode_snapshot(bytes)
assert_that(snapshot.entities[0].visibility).is_equal("Forward")
assert_that(snapshot.entities[1].visibility).is_equal("Peripheral")
assert_that(snapshot.entities[2].visibility).is_equal("Forward")
assert_that(snapshot.entities[3].visibility).is_equal("Forward")
# -- v1 backward compatibility (no v2 fields → graceful null defaults) --------
func test_decode_v1_snapshot_graceful_defaults() -> void:
# Minimal v1 snapshot — only tick + entities, no v2 fields
var v1_raw := {"tick": 10, "entities": [
{"entity_id": 1, "x": 5.0, "y": 5.0, "z": 0, "kind": "Player"},
]}
var encoded: Variant = Messagepack.encode(v1_raw)
assert_that(encoded.status).is_null()
var snapshot: Variant = Protocol.decode_snapshot(encoded.value)
assert_that(snapshot).is_not_null()
assert_that(snapshot.tick).is_equal(10)
assert_that(snapshot.entities.size()).is_equal(1)
# v2 fields should be null/empty, not crash
assert_that(snapshot.version).is_null()
assert_that(snapshot.game_time).is_null()
assert_that(snapshot.player_facing).is_null()
assert_that(snapshot.visible_tiles.size()).is_equal(0)
# Entity should have null visibility
assert_that(snapshot.entities[0].visibility).is_null()
# -- Batch input fixture (D-030 Layer 1 bidirectional symmetry) ----------------
func test_decode_batch_input_fixture() -> void:
+135
View File
@@ -22,6 +22,11 @@ var _test_entities: Array = [
{"entity_id": 2, "x": 7.0, "y": 5.0, "z": 0, "kind": {"variant": "Npc", "data": null}},
]
var _test_entities_v2: Array = [
{"entity_id": 1, "x": 5.0, "y": 5.0, "z": 0, "kind": {"variant": "Player", "data": null}, "visibility": "Forward"},
{"entity_id": 2, "x": 7.0, "y": 5.0, "z": 0, "kind": {"variant": "Npc", "data": null}, "visibility": "Peripheral"},
]
# -- Constants --
@@ -72,6 +77,42 @@ func test_game_state_same_count_different_visibility_new_tick() -> void:
assert_that(GameState.visible_positions.has(Vector2i(1, 1))).is_false()
assert_that(GameState.visible_positions.has(Vector2i(2, 2))).is_true()
func test_game_state_stores_game_time() -> void:
var gt := {"day": 1, "time_of_day": 720, "day_phase": "Evening", "paused": false}
GameState.apply_snapshot({"tick": 1, "game_time": gt})
assert_that(GameState.game_time.day).is_equal(1)
assert_that(GameState.game_time.time_of_day).is_equal(720)
func test_game_state_stores_player_facing() -> void:
GameState.apply_snapshot({"tick": 1, "player_facing": "Southeast"})
assert_that(GameState.player_facing).is_equal("Southeast")
func test_game_state_derives_visible_positions_from_visible_tiles() -> void:
var vtiles := [
{"x": 5, "y": 5, "z": 0, "visibility": "Forward"},
{"x": 6, "y": 5, "z": 0, "visibility": "Peripheral"},
]
GameState.apply_snapshot({"tick": 1, "visible_tiles": vtiles})
assert_that(GameState.visible_positions.has(Vector2i(5, 5))).is_true()
assert_that(GameState.visible_positions.has(Vector2i(6, 5))).is_true()
assert_that(GameState.visibility_sectors[Vector2i(5, 5)]).is_equal("Forward")
assert_that(GameState.visibility_sectors[Vector2i(6, 5)]).is_equal("Peripheral")
func test_game_state_skips_malformed_visible_tiles() -> void:
var vtiles := [
{"x": 5, "y": 5, "z": 0, "visibility": "Forward"},
null,
"not_a_dict",
{"z": 0}, # missing x, y
{"x": 6, "y": 6, "z": 0, "visibility": "Peripheral"},
]
GameState.apply_snapshot({"tick": 1, "visible_tiles": vtiles})
# Only the 2 valid entries should be stored
assert_that(GameState.visible_positions.size()).is_equal(2)
assert_that(GameState.visible_positions.has(Vector2i(5, 5))).is_true()
assert_that(GameState.visible_positions.has(Vector2i(6, 6))).is_true()
assert_that(GameState.visibility_sectors.size()).is_equal(2)
func test_game_state_warns_on_missing_player() -> void:
GameState.player_entity_id = 999
GameState.player_position = Vector2(5, 5)
@@ -130,6 +171,20 @@ func test_sim_bridge_test_tiles_contain_all_types() -> void:
assert_that(types.has("wall")).is_true()
assert_that(types.has("door")).is_true()
func test_sim_bridge_test_snapshot_has_v2_fields() -> void:
SimBridge._test_tick = 0
var snap = SimBridge._test_snapshot()
assert_that(snap.has("version")).is_true()
assert_that(snap.version).is_equal(2)
assert_that(snap.has("game_time")).is_true()
assert_that(snap.has("player_facing")).is_true()
assert_that(snap.has("visible_tiles")).is_true()
assert_that(snap.visible_tiles.size()).is_greater(0)
var vtile = snap.visible_tiles[0]
assert_that(vtile.has("visibility")).is_true()
# Entities should have visibility
assert_that(snap.entities[0].has("visibility")).is_true()
# -- EntityRenderer: lifecycle --
@@ -195,6 +250,86 @@ func test_entity_renderer_empty_entities_clears_all() -> void:
renderer.queue_free()
# -- EntityRenderer: D-033 colors and v2 features --
func test_entity_renderer_player_uses_d033_color() -> void:
var renderer := _make_entity_renderer()
renderer.update_entities(_test_entities_v2)
var player_node = renderer.entity_nodes[1] as ColorRect
assert_that(player_node.color).is_equal(Constants.ENTITY_COLOR_PLAYER)
renderer.queue_free()
func test_entity_renderer_npc_uses_unknown_teal() -> void:
var renderer := _make_entity_renderer()
renderer.update_entities(_test_entities_v2)
var npc_node = renderer.entity_nodes[2] as ColorRect
assert_that(npc_node.color).is_equal(Constants.ENTITY_COLOR_UNKNOWN)
renderer.queue_free()
func test_entity_renderer_object_uses_grey() -> void:
var renderer := _make_entity_renderer()
var obj := [{"entity_id": 3, "x": 1.0, "y": 1.0, "z": 0, "kind": {"variant": "Object", "data": null}, "visibility": "Forward"}]
renderer.update_entities(obj)
var node = renderer.entity_nodes[3] as ColorRect
assert_that(node.color).is_equal(Constants.ENTITY_COLOR_OBJECT)
renderer.queue_free()
func test_entity_renderer_peripheral_entity_dimmed() -> void:
var renderer := _make_entity_renderer()
renderer.update_entities(_test_entities_v2)
var npc_node = renderer.entity_nodes[2]
assert_that(npc_node.modulate.a).is_equal_approx(Constants.PERIPHERAL_ALPHA, 0.01)
renderer.queue_free()
func test_entity_renderer_forward_entity_full_alpha() -> void:
var renderer := _make_entity_renderer()
renderer.update_entities(_test_entities_v2)
var player_node = renderer.entity_nodes[1]
assert_that(player_node.modulate.a).is_equal_approx(1.0, 0.01)
renderer.queue_free()
func test_entity_renderer_player_has_facing_indicator() -> void:
GameState.player_entity_id = 1
var renderer := _make_entity_renderer()
renderer.update_entities(_test_entities_v2)
var player_node = renderer.entity_nodes[1]
var indicator = player_node.get_node_or_null("FacingIndicator")
assert_that(indicator != null).is_true()
assert_that(indicator is Polygon2D).is_true()
renderer.queue_free()
func test_entity_renderer_facing_indicator_rotation_accuracy() -> void:
GameState.player_entity_id = 1
var renderer := _make_entity_renderer()
var directions := {
"North": 0.0,
"Northeast": PI / 4.0,
"East": PI / 2.0,
"Southeast": 3.0 * PI / 4.0,
"South": PI,
"Southwest": 5.0 * PI / 4.0,
"West": 3.0 * PI / 2.0,
"Northwest": 7.0 * PI / 4.0,
}
renderer.update_entities(_test_entities_v2)
var player_node = renderer.entity_nodes[1]
var indicator = player_node.get_node_or_null("FacingIndicator")
for dir_name in directions:
GameState.player_facing = dir_name
renderer.update_entities(_test_entities_v2)
assert_that(indicator.rotation).is_equal_approx(directions[dir_name], 0.001)
renderer.queue_free()
func test_entity_renderer_npc_has_no_facing_indicator() -> void:
GameState.player_entity_id = 1
var renderer := _make_entity_renderer()
renderer.update_entities(_test_entities_v2)
var npc_node = renderer.entity_nodes[2]
var indicator = npc_node.get_node_or_null("FacingIndicator")
assert_that(indicator == null).is_true()
renderer.queue_free()
# -- FogRenderer: position registration --
func _make_fog_renderer() -> TileMapLayer: