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