diff --git a/locale/en.properties b/locale/en.properties
index 846e941..e2fd55e 100644
--- a/locale/en.properties
+++ b/locale/en.properties
@@ -1,2 +1,7 @@
mod_rpg_name = RPG World
mod_rpg_desc = Top-down tile world in the spirit of RPG Maker: three tile layers and a collision layer, grid walking with WASD, events on the action key E/F, random encounters and battles that return to the map.
+
+rpg/ui/location = EXPLORING
+rpg/ui/interact = E / F · Interact
+rpg/ui/walk = WASD / arrows · Walk
+rpg/ui/hint = Shift · Run | The journal keeps your next steps.
diff --git a/locale/ru.properties b/locale/ru.properties
index e25cc2c..94ac90b 100644
--- a/locale/ru.properties
+++ b/locale/ru.properties
@@ -1,2 +1,7 @@
mod_rpg_name = RPG-мир
mod_rpg_desc = Мир из тайлов с видом сверху в духе RPG Maker: три слоя тайлов и слой коллизий, ходьба по клеткам на WASD, события по клавише действия E/F, случайные встречи и бои с возвратом на карту.
+
+rpg/ui/location = В ПУТИ
+rpg/ui/interact = E / F · Взаимодействовать
+rpg/ui/walk = WASD / стрелки · Идти
+rpg/ui/hint = Shift · Бежать | Следующие шаги — в журнале.
diff --git a/scenes/ui/rpg_world_host.tscn b/scenes/ui/rpg_world_host.tscn
index 421e3d0..90ced5e 100644
--- a/scenes/ui/rpg_world_host.tscn
+++ b/scenes/ui/rpg_world_host.tscn
@@ -21,3 +21,64 @@ grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
color = Color(0.06, 0.07, 0.09, 1)
+
+[node name="Hud" type="Control" parent="."]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+mouse_filter = 2
+
+[node name="Location" type="VBoxContainer" parent="Hud"]
+layout_mode = 0
+offset_left = 32.0
+offset_top = 84.0
+offset_right = 530.0
+offset_bottom = 145.0
+mouse_filter = 2
+
+[node name="Eyebrow" type="Label" parent="Hud/Location"]
+layout_mode = 2
+theme_override_colors/font_color = Color(0.82, 0.72, 0.47, 1)
+theme_override_colors/font_shadow_color = Color(0.03, 0.06, 0.07, 0.95)
+theme_override_constants/shadow_offset_x = 1
+theme_override_constants/shadow_offset_y = 2
+theme_override_font_sizes/font_size = 12
+text = "rpg/ui/location"
+mouse_filter = 2
+
+[node name="Name" type="Label" parent="Hud/Location"]
+layout_mode = 2
+theme_override_colors/font_color = Color(0.94, 0.94, 0.84, 1)
+theme_override_colors/font_shadow_color = Color(0.03, 0.06, 0.07, 0.95)
+theme_override_constants/shadow_offset_x = 1
+theme_override_constants/shadow_offset_y = 2
+theme_override_font_sizes/font_size = 24
+mouse_filter = 2
+
+[node name="Controls" type="VBoxContainer" parent="Hud"]
+layout_mode = 1
+anchor_top = 1.0
+anchor_bottom = 1.0
+offset_left = 32.0
+offset_top = -76.0
+offset_right = 570.0
+offset_bottom = -22.0
+mouse_filter = 2
+
+[node name="Prompt" type="Label" parent="Hud/Controls"]
+layout_mode = 2
+theme_override_colors/font_color = Color(0.96, 0.88, 0.65, 1)
+theme_override_colors/font_shadow_color = Color(0.02, 0.04, 0.05, 1)
+theme_override_constants/shadow_offset_y = 2
+theme_override_font_sizes/font_size = 18
+mouse_filter = 2
+
+[node name="Hint" type="Label" parent="Hud/Controls"]
+layout_mode = 2
+theme_override_colors/font_color = Color(0.75, 0.83, 0.78, 1)
+theme_override_colors/font_shadow_color = Color(0.02, 0.04, 0.05, 1)
+theme_override_constants/shadow_offset_y = 2
+theme_override_font_sizes/font_size = 13
+text = "rpg/ui/hint"
+mouse_filter = 2
diff --git a/shaders/rpg_lit.gdshader b/shaders/rpg_lit.gdshader
index 138a4a9..63edb29 100644
--- a/shaders/rpg_lit.gdshader
+++ b/shaders/rpg_lit.gdshader
@@ -9,7 +9,7 @@ shader_type canvas_item;
uniform vec4 day_color : source_color = vec4(1.0, 1.0, 1.0, 1.0);
uniform vec4 morning_color : source_color = vec4(0.86, 0.72, 0.84, 1.0);
uniform vec4 evening_color : source_color = vec4(0.84, 0.72, 0.92, 1.0);
-uniform vec4 night_color : source_color = vec4(0.2, 0.26, 0.42, 1.0);
+uniform vec4 night_color : source_color = vec4(0.52, 0.62, 0.76, 1.0);
uniform vec4 time_weights = vec4(0.0, 1.0, 0.0, 0.0);
// A map that sets its own ambient (indoors) uses it instead of the time of day.
diff --git a/src/Data/RpgMap.cs b/src/Data/RpgMap.cs
index 51bd695..642efd9 100644
--- a/src/Data/RpgMap.cs
+++ b/src/Data/RpgMap.cs
@@ -61,5 +61,8 @@ namespace Cthangover.Mods.Rpg
/// lights the map ().
///
public string Ambient { get; set; }
+
+ /// Optional woodland leaves, fireflies and water ripples; empty keeps interiors still.
+ public string Atmosphere { get; set; }
}
}
diff --git a/src/World/RpgActor.cs b/src/World/RpgActor.cs
index b0f5a6f..9be9518 100644
--- a/src/World/RpgActor.cs
+++ b/src/World/RpgActor.cs
@@ -30,6 +30,10 @@ namespace Cthangover.Mods.Rpg
private Vector2 to;
private float progress = 1f;
private float animationTime;
+ private bool hasShadow;
+
+ /// Current speed in tiles/s; the player can dash without changing NPC routes.
+ public float Speed { get; set; } = TilesPerSecond;
/// Raised when a step reaches its cell.
public event Action StepFinished;
@@ -70,6 +74,8 @@ namespace Cthangover.Mods.Rpg
{
if (sprite == null)
return;
+ hasShadow = false;
+ QueueRedraw();
sprite.QueueFree();
sprite = null;
}
@@ -79,6 +85,8 @@ namespace Cthangover.Mods.Rpg
{
if (texture == null)
return;
+ hasShadow = true;
+ QueueRedraw();
sprite = new Sprite2D { Texture = texture, Hframes = 3, Vframes = 4, Centered = false, UseParentMaterial = true };
var frame = new Vector2(texture.GetWidth() / 3f, texture.GetHeight() / 4f);
sprite.Position = new Vector2((tileSize - frame.X) / 2f, tileSize - frame.Y);
@@ -155,14 +163,24 @@ namespace Cthangover.Mods.Rpg
if (!IsMoving)
return;
- progress = Mathf.Min(1f, progress + (float)delta * TilesPerSecond);
- animationTime += (float)delta * TilesPerSecond * 2f;
+ progress = Mathf.Min(1f, progress + (float)delta * Math.Clamp(Speed, 1f, 12f));
+ animationTime += (float)delta * Math.Clamp(Speed, 1f, 12f) * 2f;
Position = from.Lerp(to, progress);
UpdateFrame();
if (!IsMoving)
StepFinished?.Invoke(this);
}
+ /// A soft contact shadow anchors charsets to the ground without changing their art.
+ public override void _Draw()
+ {
+ if (!hasShadow)
+ return;
+ DrawSetTransform(new Vector2(tileSize / 2f, tileSize - 3), 0, new Vector2(1, 0.32f));
+ DrawCircle(Vector2.Zero, tileSize * 0.28f, new Color(0.04f, 0.09f, 0.1f, 0.28f));
+ DrawSetTransform(Vector2.Zero);
+ }
+
private Vector2 CellPosition(Vector2I cell) => new(cell.X * tileSize, cell.Y * tileSize);
private void UpdateFrame()
diff --git a/src/World/RpgAtmosphere.cs b/src/World/RpgAtmosphere.cs
new file mode 100644
index 0000000..69a7345
--- /dev/null
+++ b/src/World/RpgAtmosphere.cs
@@ -0,0 +1,74 @@
+namespace Cthangover.Mods.Rpg
+{
+ using System;
+ using System.Collections.Generic;
+ using Godot;
+
+ ///
+ /// Opt-in woodland motion in map coordinates: drifting leaves, fireflies and small
+ /// water ripples. Positions are deterministic; nothing enters the save or collision
+ /// model. The small fixed draw budget is independent of map dimensions.
+ ///
+ public partial class RpgAtmosphere : Node2D
+ {
+ private readonly Vector2 extent;
+ private readonly List water = new();
+ private readonly int tileSize;
+ private double elapsed;
+ private double redraw;
+
+ /// Collects water cells once; an empty Atmosphere leaves this node unmounted.
+ public RpgAtmosphere(RpgMap map, RpgMapGrid grid, int tileSize, RpgTileset tileset)
+ {
+ Name = "Atmosphere";
+ this.tileSize = tileSize;
+ extent = new Vector2(grid.Width, grid.Height) * tileSize;
+ var waterTiles = new HashSet();
+ foreach (var tile in tileset?.Tiles ?? new List())
+ if (string.Equals(tile.Tag, "water", StringComparison.OrdinalIgnoreCase))
+ waterTiles.Add(tile.Index);
+ for (var y = 0; y < grid.Height; y++)
+ for (var x = 0; x < grid.Width; x++)
+ if (waterTiles.Contains(grid.GetTile(RpgLayer.Back, x, y)))
+ water.Add(new Vector2(x, y) * tileSize);
+ }
+
+ /// Updates at 20 Hz, retaining crisp pixel positions and avoiding a particle system per tile.
+ public override void _Process(double delta)
+ {
+ elapsed += delta;
+ redraw += delta;
+ if (redraw < 0.05)
+ return;
+ redraw = 0;
+ QueueRedraw();
+ }
+
+ /// Draws small translucent accents; the ground and actor sprites retain their own palette.
+ public override void _Draw()
+ {
+ var t = (float)elapsed;
+ for (var i = 0; i < 36; i++)
+ {
+ var x = Mathf.PosMod(i * 137.3f + t * (2 + i % 3), extent.X);
+ var y = Mathf.PosMod(i * 83.7f + t * 1.4f + Mathf.Sin(t * 0.4f + i) * 7, extent.Y);
+ var p = new Vector2(Mathf.Round(x), Mathf.Round(y));
+ if (i % 3 == 0)
+ {
+ var alpha = 0.15f + 0.45f * Mathf.Pow(Mathf.Max(0, Mathf.Sin(t * 1.2f + i)), 2);
+ DrawCircle(p, 3, new Color(0.72f, 0.91f, 0.5f, alpha * 0.12f));
+ DrawRect(new Rect2(p, Vector2.One), new Color(0.95f, 1f, 0.65f, alpha));
+ }
+ else
+ DrawLine(p, p + new Vector2(2, i % 2 == 0 ? 1 : -1), new Color(0.77f, 0.68f, 0.34f, 0.32f));
+ }
+ for (var i = 0; i < water.Count; i += 2)
+ {
+ var phase = t * 0.6f + i * 1.7f;
+ var p = water[i] + new Vector2(8 + Mathf.Sin(phase) * 4, 10 + i % 13);
+ var alpha = 0.08f + 0.12f * Mathf.Max(0, Mathf.Sin(phase));
+ DrawLine(p.Round(), (p + new Vector2(tileSize * 0.25f, 0)).Round(), new Color(0.63f, 0.87f, 0.84f, alpha));
+ }
+ }
+ }
+}
diff --git a/src/World/RpgEventRunner.cs b/src/World/RpgEventRunner.cs
index e678dc4..38caae4 100644
--- a/src/World/RpgEventRunner.cs
+++ b/src/World/RpgEventRunner.cs
@@ -50,7 +50,7 @@ namespace Cthangover.Mods.Rpg
if (dialogBox == null || dialogBox.IsRunning)
return false;
- var dialog = ScenarioParser.Parse(text.ToString());
+ var dialog = new ScenarioConverter().Convert(text.ToString());
if (dialog?.Queue == null || dialog.Queue.Count == 0)
return false;
diff --git a/src/World/RpgInteractionCue.cs b/src/World/RpgInteractionCue.cs
new file mode 100644
index 0000000..3579d99
--- /dev/null
+++ b/src/World/RpgInteractionCue.cs
@@ -0,0 +1,44 @@
+namespace Cthangover.Mods.Rpg
+{
+ using Godot;
+
+ /// Highlights exactly the event the action key will run. Uses the view's target selection, so hints cannot disagree with input.
+ public partial class RpgInteractionCue : Node2D
+ {
+ private readonly RpgWorldView view;
+ private float elapsed;
+
+ /// Attaches to one map; no subscriptions or state survive unmount.
+ public RpgInteractionCue(RpgWorldView view)
+ {
+ this.view = view;
+ Name = "InteractionCue";
+ }
+
+ /// Redraws the small corner marker while an action event is in reach.
+ public override void _Process(double delta)
+ {
+ elapsed += (float)delta;
+ QueueRedraw();
+ }
+
+ /// Draws four corners around the target cell, hidden whenever another UI owns input.
+ public override void _Draw()
+ {
+ if (RpgInput.IsWalkBlocked(this))
+ return;
+ var target = view.InteractionTarget();
+ if (target == null)
+ return;
+ var rect = new Rect2(target.Position + new Vector2(2, 2), new Vector2(view.TileSize - 4, view.TileSize - 4));
+ var color = new Color(1f, 0.83f, 0.48f, 0.65f + 0.2f * Mathf.Sin(elapsed * 3));
+ foreach (var corner in new[] { rect.Position, new Vector2(rect.End.X, rect.Position.Y), rect.End, new Vector2(rect.Position.X, rect.End.Y) })
+ {
+ var dx = corner.X == rect.Position.X ? 5 : -5;
+ var dy = corner.Y == rect.Position.Y ? 5 : -5;
+ DrawLine(corner, corner + new Vector2(dx, 0), color);
+ DrawLine(corner, corner + new Vector2(0, dy), color);
+ }
+ }
+ }
+}
diff --git a/src/World/RpgWorldHost.cs b/src/World/RpgWorldHost.cs
index 75290b9..662b858 100644
--- a/src/World/RpgWorldHost.cs
+++ b/src/World/RpgWorldHost.cs
@@ -23,6 +23,9 @@ namespace Cthangover.Mods.Rpg
public partial class RpgWorldHost : Control
{
private Control backdrop;
+ private Control hud;
+ private Label mapName;
+ private Label prompt;
/// The host in the running shell, or null.
public static RpgWorldHost Current { get; private set; }
@@ -43,9 +46,22 @@ namespace Cthangover.Mods.Rpg
public override void _Ready()
{
backdrop = GetNodeOrNull("Backdrop");
+ hud = GetNodeOrNull("Hud");
+ mapName = GetNodeOrNull