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 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"test_root_folder": "res://tests",
|
||||
"report_format": "JSON",
|
||||
"auto_save": true
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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")]
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
uid://dojylr6xlag0s
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
uid://censpds5c1g8r
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://cmjlrxs0vkvbb
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://lcfje72ikd6b
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://civev5c8xvtc3
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
uid://cfyv4qt7yybib
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://djon0gnkn6sh2
|
||||
@@ -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()
|
||||
@@ -0,0 +1 @@
|
||||
uid://gfl5h6frlu3k
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dgy4mkke84c70
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
uid://b6bqe4ydk8evo
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://befq2r47mmw08
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user