From 603c24e17779d0e36bdf0f30cabff0ba3ad7146f Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Wed, 11 Feb 2026 16:52:27 +0100 Subject: [PATCH] feat(client): initialize Godot 4 project boilerplate (epic #277) Set up the complete Godot 4.6 client foundation as a pure renderer (D-020): - project.godot with 2D rendering, autoloads, input actions - Main scene: Game > World (TileMapLayer, Entities, FogOverlay) + Camera2D + UILayer - Autoloads: SimBridge (connection state machine + test mode), GameState, InputMapper - Rendering stubs: WorldRenderer, EntityRenderer, FogRenderer - UI stubs: HUD (health/perception/time), Minimap, MonologueDisplay - Input mapping: WASD/arrows, E (interact), Tab (perception), Esc (menu), Space (pause) - gdUnit4 smoke tests: scene loads, autoloads registered (2/2 passing) Co-Authored-By: Claude Opus 4.6 --- client/.gdunit4.settings | 5 ++ client/{ => assets}/.gitkeep | 0 client/assets/audio/.gitkeep | 0 client/assets/sprites/.gitkeep | 0 client/assets/tilesets/.gitkeep | 0 client/project.godot | 82 +++++++++++++++++++ client/scenes/main.tscn | 33 ++++++++ client/scripts/autoloads/.gitkeep | 0 client/scripts/autoloads/game_state.gd | 28 +++++++ client/scripts/autoloads/game_state.gd.uid | 1 + client/scripts/autoloads/input_mapper.gd | 58 +++++++++++++ client/scripts/autoloads/input_mapper.gd.uid | 1 + client/scripts/autoloads/sim_bridge.gd | 80 ++++++++++++++++++ client/scripts/autoloads/sim_bridge.gd.uid | 1 + client/scripts/main.gd | 31 +++++++ client/scripts/main.gd.uid | 1 + client/scripts/rendering/entity_renderer.gd | 75 +++++++++++++++++ .../scripts/rendering/entity_renderer.gd.uid | 1 + client/scripts/rendering/fog_renderer.gd | 16 ++++ client/scripts/rendering/fog_renderer.gd.uid | 1 + client/scripts/rendering/world_renderer.gd | 20 +++++ .../scripts/rendering/world_renderer.gd.uid | 1 + client/tests/test_main_scene.gd | 27 ++++++ client/tests/test_main_scene.gd.uid | 1 + client/ui/.gitkeep | 0 client/ui/hud.gd | 24 ++++++ client/ui/hud.gd.uid | 1 + client/ui/hud.tscn | 38 +++++++++ client/ui/minimap.gd | 15 ++++ client/ui/minimap.gd.uid | 1 + client/ui/minimap.tscn | 32 ++++++++ client/ui/monologue_display.gd | 43 ++++++++++ client/ui/monologue_display.gd.uid | 1 + client/ui/monologue_display.tscn | 38 +++++++++ 34 files changed, 656 insertions(+) create mode 100644 client/.gdunit4.settings rename client/{ => assets}/.gitkeep (100%) create mode 100644 client/assets/audio/.gitkeep create mode 100644 client/assets/sprites/.gitkeep create mode 100644 client/assets/tilesets/.gitkeep create mode 100644 client/project.godot create mode 100644 client/scenes/main.tscn create mode 100644 client/scripts/autoloads/.gitkeep create mode 100644 client/scripts/autoloads/game_state.gd create mode 100644 client/scripts/autoloads/game_state.gd.uid create mode 100644 client/scripts/autoloads/input_mapper.gd create mode 100644 client/scripts/autoloads/input_mapper.gd.uid create mode 100644 client/scripts/autoloads/sim_bridge.gd create mode 100644 client/scripts/autoloads/sim_bridge.gd.uid create mode 100644 client/scripts/main.gd create mode 100644 client/scripts/main.gd.uid create mode 100644 client/scripts/rendering/entity_renderer.gd create mode 100644 client/scripts/rendering/entity_renderer.gd.uid create mode 100644 client/scripts/rendering/fog_renderer.gd create mode 100644 client/scripts/rendering/fog_renderer.gd.uid create mode 100644 client/scripts/rendering/world_renderer.gd create mode 100644 client/scripts/rendering/world_renderer.gd.uid create mode 100644 client/tests/test_main_scene.gd create mode 100644 client/tests/test_main_scene.gd.uid create mode 100644 client/ui/.gitkeep create mode 100644 client/ui/hud.gd create mode 100644 client/ui/hud.gd.uid create mode 100644 client/ui/hud.tscn create mode 100644 client/ui/minimap.gd create mode 100644 client/ui/minimap.gd.uid create mode 100644 client/ui/minimap.tscn create mode 100644 client/ui/monologue_display.gd create mode 100644 client/ui/monologue_display.gd.uid create mode 100644 client/ui/monologue_display.tscn diff --git a/client/.gdunit4.settings b/client/.gdunit4.settings new file mode 100644 index 000000000..bf6b67797 --- /dev/null +++ b/client/.gdunit4.settings @@ -0,0 +1,5 @@ +{ + "test_root_folder": "res://tests", + "report_format": "JSON", + "auto_save": true +} diff --git a/client/.gitkeep b/client/assets/.gitkeep similarity index 100% rename from client/.gitkeep rename to client/assets/.gitkeep diff --git a/client/assets/audio/.gitkeep b/client/assets/audio/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/client/assets/sprites/.gitkeep b/client/assets/sprites/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/client/assets/tilesets/.gitkeep b/client/assets/tilesets/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/client/project.godot b/client/project.godot new file mode 100644 index 000000000..f13a795fd --- /dev/null +++ b/client/project.godot @@ -0,0 +1,82 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; but you can still edit the file manually. +; This file is specific to Godot 4.x format. + +config_version=5 + +[application] + +config/name="The Settled Reach" +run/main_scene="res://scenes/main.tscn" +config/features=PackedStringArray("4.6", "GL Compatibility") +config/icon="res://icon.svg" + +[autoload] + +SimBridge="*res://scripts/autoloads/sim_bridge.gd" +GameState="*res://scripts/autoloads/game_state.gd" +InputMapper="*res://scripts/autoloads/input_mapper.gd" + +[display] + +window/size/viewport_width=1920 +window/size/viewport_height=1080 +window/size/resizable=true +window/stretch/mode="canvas_items" + +[input] + +move_north={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":87,"key_label":0,"unicode":119,"location":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194320,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} +move_south={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":115,"location":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194322,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} +move_east={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"location":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194321,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} +move_west={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":97,"location":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194319,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} +interact={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":69,"key_label":0,"unicode":101,"location":0,"echo":false,"script":null) +] +} +perception_mode={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194306,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} +open_menu={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194305,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} +pause={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":32,"key_label":0,"unicode":32,"location":0,"echo":false,"script":null) +] +} + +[editor_plugins] + +enabled=PackedStringArray("res://addons/gdUnit4/plugin.cfg") + +[rendering] + +renderer/rendering_method="gl_compatibility" +renderer/rendering_method.mobile="gl_compatibility" diff --git a/client/scenes/main.tscn b/client/scenes/main.tscn new file mode 100644 index 000000000..e51e917f6 --- /dev/null +++ b/client/scenes/main.tscn @@ -0,0 +1,33 @@ +[gd_scene load_steps=8 format=3 uid="uid://bswrmh7w8dbgm"] + +[ext_resource type="Script" path="res://scripts/main.gd" id="1_main"] +[ext_resource type="Script" path="res://scripts/rendering/world_renderer.gd" id="2_world"] +[ext_resource type="Script" path="res://scripts/rendering/entity_renderer.gd" id="3_entity"] +[ext_resource type="Script" path="res://scripts/rendering/fog_renderer.gd" id="4_fog"] +[ext_resource type="PackedScene" path="res://ui/hud.tscn" id="5_hud"] +[ext_resource type="PackedScene" path="res://ui/minimap.tscn" id="6_minimap"] +[ext_resource type="PackedScene" path="res://ui/monologue_display.tscn" id="7_monologue"] + +[node name="Game" type="Node2D"] +script = ExtResource("1_main") + +[node name="World" type="Node2D" parent="."] +script = ExtResource("2_world") + +[node name="TileMapLayer" type="TileMapLayer" parent="World"] + +[node name="Entities" type="Node2D" parent="World"] +script = ExtResource("3_entity") + +[node name="FogOverlay" type="Node2D" parent="World"] +script = ExtResource("4_fog") + +[node name="Camera2D" type="Camera2D" parent="."] + +[node name="UILayer" type="CanvasLayer" parent="."] + +[node name="HUD" parent="UILayer" instance=ExtResource("5_hud")] + +[node name="Minimap" parent="UILayer" instance=ExtResource("6_minimap")] + +[node name="MonologueDisplay" parent="UILayer" instance=ExtResource("7_monologue")] diff --git a/client/scripts/autoloads/.gitkeep b/client/scripts/autoloads/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/client/scripts/autoloads/game_state.gd b/client/scripts/autoloads/game_state.gd new file mode 100644 index 000000000..a54ab0cbd --- /dev/null +++ b/client/scripts/autoloads/game_state.gd @@ -0,0 +1,28 @@ +extends Node + +# Updated each frame from ObserverSnapshot data +var current_snapshot: Dictionary = {} +var player_position: Vector2 = Vector2.ZERO +var visible_entities: Array = [] +var fog_state: Dictionary = {} +var hud_data: Dictionary = {} + +func apply_snapshot(snapshot: Dictionary) -> void: + current_snapshot = snapshot + + # Parse player data + if snapshot.has("player") and snapshot.player.has("position"): + var pos = snapshot.player.position + player_position = Vector2(pos[0], pos[1]) + + # Parse entities + if snapshot.has("entities"): + visible_entities = snapshot.entities + + # Parse fog state + if snapshot.has("fog"): + fog_state = snapshot.fog + + # Parse HUD data + if snapshot.has("hud"): + hud_data = snapshot.hud diff --git a/client/scripts/autoloads/game_state.gd.uid b/client/scripts/autoloads/game_state.gd.uid new file mode 100644 index 000000000..b3290876c --- /dev/null +++ b/client/scripts/autoloads/game_state.gd.uid @@ -0,0 +1 @@ +uid://dojylr6xlag0s diff --git a/client/scripts/autoloads/input_mapper.gd b/client/scripts/autoloads/input_mapper.gd new file mode 100644 index 000000000..79bf2e931 --- /dev/null +++ b/client/scripts/autoloads/input_mapper.gd @@ -0,0 +1,58 @@ +extends Node + +# Semantic actions — NO raw key codes cross the bridge +enum Action { + MOVE_NORTH, MOVE_SOUTH, MOVE_EAST, MOVE_WEST, + INTERACT, USE_PERCEPTION_MODE, OPEN_MENU, PAUSE +} + +var input_queue: Array[Dictionary] = [] + +func _unhandled_input(event: InputEvent) -> void: + # Only process key press events (not releases or repeats) + if not event is InputEventKey: + return + if not event.pressed or event.echo: + return + + var action: Action = -1 + var action_name: String = "" + + # Map input actions to semantic Action enum + if event.is_action_pressed("move_north"): + action = Action.MOVE_NORTH + action_name = "move_north" + elif event.is_action_pressed("move_south"): + action = Action.MOVE_SOUTH + action_name = "move_south" + elif event.is_action_pressed("move_east"): + action = Action.MOVE_EAST + action_name = "move_east" + elif event.is_action_pressed("move_west"): + action = Action.MOVE_WEST + action_name = "move_west" + elif event.is_action_pressed("interact"): + action = Action.INTERACT + action_name = "interact" + elif event.is_action_pressed("perception_mode"): + action = Action.USE_PERCEPTION_MODE + action_name = "perception_mode" + elif event.is_action_pressed("open_menu"): + action = Action.OPEN_MENU + action_name = "open_menu" + elif event.is_action_pressed("pause"): + action = Action.PAUSE + action_name = "pause" + + # Queue the action if valid + if action != -1: + input_queue.append({ + "action": action, + "timestamp_msec": Time.get_ticks_msec() + }) + get_viewport().set_input_as_handled() + +func flush_queue() -> Array[Dictionary]: + var queue = input_queue.duplicate() + input_queue.clear() + return queue diff --git a/client/scripts/autoloads/input_mapper.gd.uid b/client/scripts/autoloads/input_mapper.gd.uid new file mode 100644 index 000000000..cadc1c078 --- /dev/null +++ b/client/scripts/autoloads/input_mapper.gd.uid @@ -0,0 +1 @@ +uid://censpds5c1g8r diff --git a/client/scripts/autoloads/sim_bridge.gd b/client/scripts/autoloads/sim_bridge.gd new file mode 100644 index 000000000..66c032db7 --- /dev/null +++ b/client/scripts/autoloads/sim_bridge.gd @@ -0,0 +1,80 @@ +extends Node + +# Connection states +enum ConnectionState { DISCONNECTED, CONNECTING, CONNECTED, ERROR } + +var state: ConnectionState = ConnectionState.DISCONNECTED +var test_mode: bool = true # Enable test mode for development without Rust server + +# Signals +signal connection_state_changed(old_state: ConnectionState, new_state: ConnectionState) +signal snapshot_received(snapshot: Dictionary) + +func _ready() -> void: + if test_mode: + print("SimBridge: Running in test mode (hardcoded snapshot)") + +# Change connection state and emit signal +func _set_state(new_state: ConnectionState) -> void: + if state != new_state: + var old_state = state + state = new_state + connection_state_changed.emit(old_state, new_state) + +# Connect to simulation server (real implementation comes later) +func connect_to_sim() -> void: + _set_state(ConnectionState.CONNECTING) + # TODO: Actual connection logic when IPC/MessagePack is implemented + if test_mode: + _set_state(ConnectionState.CONNECTED) + else: + _set_state(ConnectionState.ERROR) + +# Disconnect from simulation server +func disconnect_from_sim() -> void: + _set_state(ConnectionState.DISCONNECTED) + +# Send input to simulation (real implementation comes later) +func send_input(player_input: Dictionary) -> void: + if state != ConnectionState.CONNECTED: + return + # TODO: Serialize and send via MessagePack when IPC is implemented + pass + +# Poll for snapshot from simulation +func poll_snapshot() -> Variant: + if state != ConnectionState.CONNECTED: + return null + + if test_mode: + var snapshot = _test_snapshot() + snapshot_received.emit(snapshot) + return snapshot + + # TODO: Actual polling logic when IPC/MessagePack is implemented + return null + +# Hardcoded test snapshot for development +func _test_snapshot() -> Dictionary: + return { + "tick": Time.get_ticks_msec() / 100, # Increment over time for testing + "player": { + "position": [10, 10], + "health": 100 + }, + "entities": [ + { + "id": 1, + "type": "npc", + "position": [12, 8], + "name": "Test NPC" + }, + ], + "fog": { + "radius": 8 + }, + "hud": { + "perception_mode": "baseline", + "time": "08:00" + } + } diff --git a/client/scripts/autoloads/sim_bridge.gd.uid b/client/scripts/autoloads/sim_bridge.gd.uid new file mode 100644 index 000000000..452d080ab --- /dev/null +++ b/client/scripts/autoloads/sim_bridge.gd.uid @@ -0,0 +1 @@ +uid://cmjlrxs0vkvbb diff --git a/client/scripts/main.gd b/client/scripts/main.gd new file mode 100644 index 000000000..879aa6723 --- /dev/null +++ b/client/scripts/main.gd @@ -0,0 +1,31 @@ +extends Node2D + +@onready var world_renderer = $World +@onready var hud = $UILayer/HUD + +func _ready() -> void: + print("The Settled Reach — client initialized") + + # Connect to simulation (will use test mode initially) + SimBridge.connect_to_sim() + +func _process(_delta: float) -> void: + # Main game loop: poll snapshot, apply state, flush input + var snapshot = SimBridge.poll_snapshot() + if snapshot != null: + GameState.apply_snapshot(snapshot) + + # Update renderers with new state + if world_renderer and world_renderer.has_method("update_from_state"): + world_renderer.update_from_state() + + # Update HUD + if hud and hud.has_method("update_from_hud_data"): + hud.update_from_hud_data(GameState.hud_data) + if snapshot.has("player") and snapshot.player.has("health"): + hud.update_health(snapshot.player.health) + + # Send queued input to simulation + var inputs = InputMapper.flush_queue() + for input in inputs: + SimBridge.send_input(input) diff --git a/client/scripts/main.gd.uid b/client/scripts/main.gd.uid new file mode 100644 index 000000000..f2d9236e9 --- /dev/null +++ b/client/scripts/main.gd.uid @@ -0,0 +1 @@ +uid://lcfje72ikd6b diff --git a/client/scripts/rendering/entity_renderer.gd b/client/scripts/rendering/entity_renderer.gd new file mode 100644 index 000000000..808517e81 --- /dev/null +++ b/client/scripts/rendering/entity_renderer.gd @@ -0,0 +1,75 @@ +extends Node2D + +# Entity renderer — manages entity sprites under the Entities node +# Creates/updates/removes Sprite2D children based on entity data + +var entity_nodes: Dictionary = {} # 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("id"): + continue + + var entity_id = entity_data.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 (placeholder visual) +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(32, 32) + entity_node.pivot_offset = Vector2(16, 16) + + # Color based on type + if entity_data.get("type") == "npc": + entity_node.color = Color(0.3, 0.6, 0.9) # Blue for NPCs + else: + entity_node.color = Color(0.8, 0.8, 0.8) # Gray for unknown + + add_child(entity_node) + entity_nodes[entity_id] = entity_node + + _update_entity_node(entity_id, entity_data) + +# Update an existing entity node +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 + if entity_data.has("position"): + var pos = entity_data.position + entity_node.position = Vector2(pos[0] * 32, pos[1] * 32) # 32px grid + +# 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) diff --git a/client/scripts/rendering/entity_renderer.gd.uid b/client/scripts/rendering/entity_renderer.gd.uid new file mode 100644 index 000000000..4af88844e --- /dev/null +++ b/client/scripts/rendering/entity_renderer.gd.uid @@ -0,0 +1 @@ +uid://civev5c8xvtc3 diff --git a/client/scripts/rendering/fog_renderer.gd b/client/scripts/rendering/fog_renderer.gd new file mode 100644 index 000000000..446419d32 --- /dev/null +++ b/client/scripts/rendering/fog_renderer.gd @@ -0,0 +1,16 @@ +extends Node2D + +# Fog renderer — manages fog of war overlay +# Controls visibility based on player position and fog radius + +func _ready() -> void: + print("FogRenderer: Initialized") + +# Update fog visibility (stub for now) +func update_fog(fog_data: Dictionary, player_pos: Vector2) -> void: + # TODO: Implement fog of war rendering + # This will control what the player can see based on: + # - fog_data.radius (visibility radius) + # - player_pos (center of visible area) + # - Perception mode state (affects visibility) + pass diff --git a/client/scripts/rendering/fog_renderer.gd.uid b/client/scripts/rendering/fog_renderer.gd.uid new file mode 100644 index 000000000..627d877d6 --- /dev/null +++ b/client/scripts/rendering/fog_renderer.gd.uid @@ -0,0 +1 @@ +uid://cfyv4qt7yybib diff --git a/client/scripts/rendering/world_renderer.gd b/client/scripts/rendering/world_renderer.gd new file mode 100644 index 000000000..419090e40 --- /dev/null +++ b/client/scripts/rendering/world_renderer.gd @@ -0,0 +1,20 @@ +extends Node2D + +# World renderer — manages all visual representation from GameState +# Attached to the World node in main.tscn + +@onready var entity_renderer = $Entities +@onready var fog_renderer = $FogOverlay + +func _ready() -> void: + print("WorldRenderer: Initialized") + +# Called each frame to update visuals from game state +func update_from_state() -> void: + # Update entity sprites + if entity_renderer and entity_renderer.has_method("update_entities"): + entity_renderer.update_entities(GameState.visible_entities) + + # Update fog overlay + if fog_renderer and fog_renderer.has_method("update_fog"): + fog_renderer.update_fog(GameState.fog_state, GameState.player_position) diff --git a/client/scripts/rendering/world_renderer.gd.uid b/client/scripts/rendering/world_renderer.gd.uid new file mode 100644 index 000000000..ac11fde64 --- /dev/null +++ b/client/scripts/rendering/world_renderer.gd.uid @@ -0,0 +1 @@ +uid://djon0gnkn6sh2 diff --git a/client/tests/test_main_scene.gd b/client/tests/test_main_scene.gd new file mode 100644 index 000000000..ed266e4b6 --- /dev/null +++ b/client/tests/test_main_scene.gd @@ -0,0 +1,27 @@ +## Smoke tests for main scene initialization +## Run with: godot4 --headless --path . -s res://addons/gdUnit4/bin/GdUnitCmdTool.gd --ignoreHeadlessMode -a res://tests/ +class_name TestMainScene +extends GdUnitTestSuite + +# Smoke test: main scene loads without errors +func test_main_scene_loads() -> void: + var scene = load("res://scenes/main.tscn") + assert_that(scene).is_not_null() + + var instance = scene.instantiate() + assert_that(instance).is_not_null() + + # Add to scene tree briefly to trigger _ready() + auto_free(instance) + add_child(instance) + + # Verify key nodes exist + assert_that(instance.get_node("World")).is_not_null() + assert_that(instance.get_node("Camera2D")).is_not_null() + assert_that(instance.get_node("UILayer")).is_not_null() + +# Test: autoloads exist +func test_autoloads_registered() -> void: + assert_that(SimBridge).is_not_null() + assert_that(GameState).is_not_null() + assert_that(InputMapper).is_not_null() diff --git a/client/tests/test_main_scene.gd.uid b/client/tests/test_main_scene.gd.uid new file mode 100644 index 000000000..e7d247f0b --- /dev/null +++ b/client/tests/test_main_scene.gd.uid @@ -0,0 +1 @@ +uid://gfl5h6frlu3k diff --git a/client/ui/.gitkeep b/client/ui/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/client/ui/hud.gd b/client/ui/hud.gd new file mode 100644 index 000000000..eb59d387f --- /dev/null +++ b/client/ui/hud.gd @@ -0,0 +1,24 @@ +extends Control + +# HUD — displays player health, perception mode, time, etc. + +@onready var health_label: Label = $MarginContainer/VBoxContainer/HealthLabel +@onready var perception_label: Label = $MarginContainer/VBoxContainer/PerceptionLabel +@onready var time_label: Label = $MarginContainer/VBoxContainer/TimeLabel + +func _ready() -> void: + print("HUD: Initialized") + +# Update HUD from snapshot data +func update_from_hud_data(data: Dictionary) -> void: + # Update perception mode display + if data.has("perception_mode"): + perception_label.text = "Mode: " + data.perception_mode.capitalize() + + # Update time display (per D-031) + if data.has("time"): + time_label.text = "Time: " + data.time + +# Update player health (from player data, not hud_data) +func update_health(health: int) -> void: + health_label.text = "Health: " + str(health) diff --git a/client/ui/hud.gd.uid b/client/ui/hud.gd.uid new file mode 100644 index 000000000..64923ffeb --- /dev/null +++ b/client/ui/hud.gd.uid @@ -0,0 +1 @@ +uid://dgy4mkke84c70 diff --git a/client/ui/hud.tscn b/client/ui/hud.tscn new file mode 100644 index 000000000..4e6d2b32f --- /dev/null +++ b/client/ui/hud.tscn @@ -0,0 +1,38 @@ +[gd_scene load_steps=2 format=3 uid="uid://cq1y5w3hmxr8b"] + +[ext_resource type="Script" path="res://ui/hud.gd" id="1_hud"] + +[node name="HUD" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 +script = ExtResource("1_hud") + +[node name="MarginContainer" type="MarginContainer" parent="."] +layout_mode = 1 +anchors_preset = 10 +anchor_right = 1.0 +offset_bottom = 100.0 +grow_horizontal = 2 +theme_override_constants/margin_left = 16 +theme_override_constants/margin_top = 16 +theme_override_constants/margin_right = 16 + +[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"] +layout_mode = 2 + +[node name="HealthLabel" type="Label" parent="MarginContainer/VBoxContainer"] +layout_mode = 2 +text = "Health: 100" + +[node name="PerceptionLabel" type="Label" parent="MarginContainer/VBoxContainer"] +layout_mode = 2 +text = "Mode: Baseline" + +[node name="TimeLabel" type="Label" parent="MarginContainer/VBoxContainer"] +layout_mode = 2 +text = "Time: 08:00" diff --git a/client/ui/minimap.gd b/client/ui/minimap.gd new file mode 100644 index 000000000..327471267 --- /dev/null +++ b/client/ui/minimap.gd @@ -0,0 +1,15 @@ +extends Control + +# Minimap — small viewport showing local area +# Placeholder implementation for now + +@onready var viewport_container: SubViewportContainer = $SubViewportContainer + +func _ready() -> void: + print("Minimap: Initialized") + +# Update minimap view (stub) +func update_minimap(player_pos: Vector2, entities: Array) -> void: + # TODO: Render minimap view + # This will show a top-down view of the local area + pass diff --git a/client/ui/minimap.gd.uid b/client/ui/minimap.gd.uid new file mode 100644 index 000000000..7cba20495 --- /dev/null +++ b/client/ui/minimap.gd.uid @@ -0,0 +1 @@ +uid://b6bqe4ydk8evo diff --git a/client/ui/minimap.tscn b/client/ui/minimap.tscn new file mode 100644 index 000000000..17c52b2df --- /dev/null +++ b/client/ui/minimap.tscn @@ -0,0 +1,32 @@ +[gd_scene load_steps=2 format=3 uid="uid://b5x7k2mwpq3vc"] + +[ext_resource type="Script" path="res://ui/minimap.gd" id="1_minimap"] + +[node name="Minimap" type="Control"] +layout_mode = 3 +anchors_preset = 1 +anchor_left = 1.0 +anchor_right = 1.0 +offset_left = -200.0 +offset_bottom = 200.0 +grow_horizontal = 0 +mouse_filter = 2 +script = ExtResource("1_minimap") + +[node name="SubViewportContainer" type="SubViewportContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="Background" type="ColorRect" parent="SubViewportContainer"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_right = 200.0 +offset_bottom = 200.0 +grow_horizontal = 2 +grow_vertical = 2 +color = Color(0.1, 0.1, 0.1, 0.7) diff --git a/client/ui/monologue_display.gd b/client/ui/monologue_display.gd new file mode 100644 index 000000000..429a28b24 --- /dev/null +++ b/client/ui/monologue_display.gd @@ -0,0 +1,43 @@ +extends Control + +# Internal monologue display (per D-015) +# Shows character's internal thoughts as text overlay + +@onready var text_panel: PanelContainer = $PanelContainer +@onready var text_label: RichTextLabel = $PanelContainer/MarginContainer/RichTextLabel + +var fade_timer: float = 0.0 +var fade_duration: float = 5.0 # Display duration before fade +var is_visible: bool = false + +func _ready() -> void: + print("MonologueDisplay: Initialized") + text_panel.modulate.a = 0.0 + is_visible = false + +func _process(delta: float) -> void: + # Auto-fade after display + if is_visible: + fade_timer += delta + if fade_timer >= fade_duration: + _fade_out() + +# Show internal monologue text +func show_monologue(text: String, duration: float = 5.0) -> void: + text_label.text = text + fade_duration = duration + fade_timer = 0.0 + is_visible = true + + # Fade in + var tween = create_tween() + tween.tween_property(text_panel, "modulate:a", 1.0, 0.3) + +# Fade out the monologue +func _fade_out() -> void: + if not is_visible: + return + + is_visible = false + var tween = create_tween() + tween.tween_property(text_panel, "modulate:a", 0.0, 0.5) diff --git a/client/ui/monologue_display.gd.uid b/client/ui/monologue_display.gd.uid new file mode 100644 index 000000000..0322d9ae9 --- /dev/null +++ b/client/ui/monologue_display.gd.uid @@ -0,0 +1 @@ +uid://befq2r47mmw08 diff --git a/client/ui/monologue_display.tscn b/client/ui/monologue_display.tscn new file mode 100644 index 000000000..10dd02758 --- /dev/null +++ b/client/ui/monologue_display.tscn @@ -0,0 +1,38 @@ +[gd_scene load_steps=2 format=3 uid="uid://bj8y4v2nx7k5m"] + +[ext_resource type="Script" path="res://ui/monologue_display.gd" id="1_monologue"] + +[node name="MonologueDisplay" type="Control"] +layout_mode = 3 +anchors_preset = 12 +anchor_top = 1.0 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_top = -150.0 +grow_horizontal = 2 +grow_vertical = 0 +mouse_filter = 2 +script = ExtResource("1_monologue") + +[node name="PanelContainer" type="PanelContainer" parent="."] +layout_mode = 1 +anchors_preset = 10 +anchor_right = 1.0 +offset_left = 100.0 +offset_right = -100.0 +offset_bottom = 120.0 +grow_horizontal = 2 + +[node name="MarginContainer" type="MarginContainer" parent="PanelContainer"] +layout_mode = 2 +theme_override_constants/margin_left = 16 +theme_override_constants/margin_top = 12 +theme_override_constants/margin_right = 16 +theme_override_constants/margin_bottom = 12 + +[node name="RichTextLabel" type="RichTextLabel" parent="PanelContainer/MarginContainer"] +layout_mode = 2 +bbcode_enabled = true +text = "Internal monologue will appear here..." +fit_content = true +scroll_active = false