diff --git a/manifest.json b/manifest.json index be820cb..e27943c 100644 --- a/manifest.json +++ b/manifest.json @@ -3,7 +3,7 @@ "name": "RPG World", "version": "1.0.0", "author": "ct", - "description": "Top-down tile world in the spirit of RPG Maker on top of the novel engine: maps of square tiles in three layers (back, main, front) with a collision layer, walking on the grid with WASD or the arrows, events that talk, teleport or start battles with the action key E/F or on touch, random encounters. A map is a logical scene, so entry scenarios, ambience, saving and the dialog box work on maps unchanged; battles run on the turn-based platform with the game's battle core and return to the same cell. Scenario actions: rpg.battle, rpg.teleport.", + "description": "Top-down tile world in the spirit of RPG Maker on top of the 2D engine mods: maps of square tiles in three layers (back, main, front) with a collision layer, walking on the grid with WASD or the arrows, events that talk, teleport or start battles with the action key E/F or on touch, random encounters. A map is a logical scene, so entry scenarios, ambience, saving and the dialog box work on maps unchanged; battles run on the turn-based platform with the game's battle core and return to the same cell. Scenario actions: rpg.battle, rpg.teleport.", "category": "gameplay", "sources": [ "src/**/*.cs" diff --git a/src/Data/RpgMap.cs b/src/Data/RpgMap.cs index 4552d9f..143fac7 100644 --- a/src/Data/RpgMap.cs +++ b/src/Data/RpgMap.cs @@ -9,7 +9,7 @@ namespace Cthangover.Mods.Rpg /// scene that shows it: entering scene X (any switch_scene X, a /// teleport, the return from a battle, loading a save) mounts map X /// over the shell. Because the scene stays an ordinary scene, everything the - /// novel engine attaches to scenes works on a map unchanged — entry + /// engine attaches to scenes works on a map unchanged — entry /// scenarios, ambience and music from scenes/X.json, the default /// background (which also becomes the backdrop of battles started here), /// saving rules. diff --git a/src/World/RpgWorldHost.cs b/src/World/RpgWorldHost.cs index 3cc942c..75290b9 100644 --- a/src/World/RpgWorldHost.cs +++ b/src/World/RpgWorldHost.cs @@ -14,7 +14,7 @@ namespace Cthangover.Mods.Rpg /// It follows the scene lifecycle: entering a scene whose name is a map id /// mounts that map () and shows the dark backdrop; /// any other scene leaves the host empty and invisible, so a game can mix - /// novel scenes and map scenes freely. The shell is rebuilt after battles, + /// picture-and-dialogue scenes and map scenes freely. The shell is rebuilt after battles, /// so the host is too; the position survives in . /// /// is the host of the live shell, used by teleports diff --git a/test_suites/walk/src/RpgWalkTestDriver.cs b/test_suites/walk/src/RpgWalkTestDriver.cs index 7f3649a..f4452cc 100644 --- a/test_suites/walk/src/RpgWalkTestDriver.cs +++ b/test_suites/walk/src/RpgWalkTestDriver.cs @@ -1,6 +1,9 @@ using System.Collections.Generic; using Cthangover.Core.Battle; using Cthangover.Core.Bootstrap; +using Cthangover.Core.Characters; +using Cthangover.Core.Factories.Impls; +using Cthangover.Core.Games; using Cthangover.Core.Scenes; using Cthangover.Core.Settings; using Cthangover.Core.Utils; @@ -26,6 +29,7 @@ namespace Cthangover.Mods.Rpg.Tests private const string Village = "rpg_village"; private const string House = "rpg_elder_house"; private const string WinFlag = "rpg_test_win"; + private const string SaveSlot = "slot_9901"; private readonly string shotsDir; private readonly List failures = new(); @@ -83,6 +87,7 @@ namespace Cthangover.Mods.Rpg.Tests return; Check(GameData.Instance.Runtime.CharacterData.Characters.ContainsKey("rpg_hero"), "party.add put the hero into the party"); Check(View.Player.Cell == new Vector2I(15, 14), $"player at the spawn (15,14), got {View.Player.Cell}"); + CheckActionTexts(); Shot("village"); Press(Key.W, true); Next(); @@ -188,11 +193,46 @@ namespace Cthangover.Mods.Rpg.Tests Godot.Engine.TimeScale = 1; Check(View.Player.Cell == new Vector2I(6, 8), $"the battle returns to the same cell, got {View.Player.Cell}"); Check(GameData.Instance.Runtime.Flags.TryGetValue(WinFlag, out var won) && won == "1", "the win flag is set after the victory"); + // The menu's path: save in the game, go to the main menu, load. + SaveService.Save(SaveSlot); + GameSession.OpenMainMenu(GetTree()); + Next(); + return; + case 16: + if (RpgWorldHost.Current != null || ++waitTicks < 5) + return; + Check(GameSession.LoadAndEnter(this, SaveSlot), "the save loads"); + Next(); + return; + case 17: + if (View?.Map?.ID != House || GodotSceneService.IsTransitioning || ++waitTicks < 5) + return; + Shot("loaded"); + var dialog = RpgInput.FindDialogBox(); + Check(dialog != null && !dialog.IsRunning && !dialog.IsVisibleInTree(), $"no dialog box after loading (running={dialog?.IsRunning}, visible={dialog?.IsVisibleInTree()})"); + Check(View.Player.Cell == new Vector2I(6, 8), $"the loaded game puts the player on the saved cell, got {View.Player.Cell}"); + SaveService.DeleteSave(SaveSlot); Finish(); return; } } + /// + /// The battle shows the names and descriptions of the hero's action cards: + /// their keys must be translated by a mod of this game, not only by + /// another game's content mod. + /// + private void CheckActionTexts() + { + var hero = CharacterFactory.Instance.Get("rpg_hero"); + Check(hero?.Actions?.Count > 0, "the hero has action cards"); + foreach (var action in hero?.Actions ?? new List()) + { + foreach (var key in new[] { action.Name, action.Description }) + Check(!string.IsNullOrEmpty(key) && TranslationServer.Translate(key) != key, $"action text '{key}' is translated"); + } + } + /// Advances a running dialog with the action key; true while it is still running. private bool AdvanceDialog() {