rpg_walk: save, main menu, load; action card texts are translated

Covers the empty dialog box after loading a save and the untranslated
action cards in battle. Docs: the world is built from engine mods, not a
novel engine.
This commit is contained in:
cthangover committed 2026-10-21 14:58:58 +02:00
1 parent 5c399b150d
commit 002cd09495
4 files changed
+43 -3

No files matched your search

+1 -1
View File
@@ -3,7 +3,7 @@
"name": "RPG World", "name": "RPG World",
"version": "1.0.0", "version": "1.0.0",
"author": "ct", "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", "category": "gameplay",
"sources": [ "sources": [
"src/**/*.cs" "src/**/*.cs"
+1 -1
View File
@@ -9,7 +9,7 @@ namespace Cthangover.Mods.Rpg
/// scene that shows it: entering scene <c>X</c> (any <c>switch_scene X</c>, a /// scene that shows it: entering scene <c>X</c> (any <c>switch_scene X</c>, a
/// teleport, the return from a battle, loading a save) mounts map <c>X</c> /// teleport, the return from a battle, loading a save) mounts map <c>X</c>
/// over the shell. Because the scene stays an ordinary scene, everything the /// 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 <c>scenes/X.json</c>, the default /// scenarios, ambience and music from <c>scenes/X.json</c>, the default
/// background (which also becomes the backdrop of battles started here), /// background (which also becomes the backdrop of battles started here),
/// saving rules. /// saving rules.
+1 -1
View File
@@ -14,7 +14,7 @@ namespace Cthangover.Mods.Rpg
/// It follows the scene lifecycle: entering a scene whose name is a map id /// It follows the scene lifecycle: entering a scene whose name is a map id
/// mounts that map (<see cref="RpgWorldView"/>) and shows the dark backdrop; /// mounts that map (<see cref="RpgWorldView"/>) and shows the dark backdrop;
/// any other scene leaves the host empty and invisible, so a game can mix /// 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 <see cref="RpgState"/>. /// so the host is too; the position survives in <see cref="RpgState"/>.
/// <para/> /// <para/>
/// <see cref="Current"/> is the host of the live shell, used by teleports /// <see cref="Current"/> is the host of the live shell, used by teleports
+40
View File
@@ -1,6 +1,9 @@
using System.Collections.Generic; using System.Collections.Generic;
using Cthangover.Core.Battle; using Cthangover.Core.Battle;
using Cthangover.Core.Bootstrap; using Cthangover.Core.Bootstrap;
using Cthangover.Core.Characters;
using Cthangover.Core.Factories.Impls;
using Cthangover.Core.Games;
using Cthangover.Core.Scenes; using Cthangover.Core.Scenes;
using Cthangover.Core.Settings; using Cthangover.Core.Settings;
using Cthangover.Core.Utils; using Cthangover.Core.Utils;
@@ -26,6 +29,7 @@ namespace Cthangover.Mods.Rpg.Tests
private const string Village = "rpg_village"; private const string Village = "rpg_village";
private const string House = "rpg_elder_house"; private const string House = "rpg_elder_house";
private const string WinFlag = "rpg_test_win"; private const string WinFlag = "rpg_test_win";
private const string SaveSlot = "slot_9901";
private readonly string shotsDir; private readonly string shotsDir;
private readonly List<string> failures = new(); private readonly List<string> failures = new();
@@ -83,6 +87,7 @@ namespace Cthangover.Mods.Rpg.Tests
return; return;
Check(GameData.Instance.Runtime.CharacterData.Characters.ContainsKey("rpg_hero"), "party.add put the hero into the party"); 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}"); Check(View.Player.Cell == new Vector2I(15, 14), $"player at the spawn (15,14), got {View.Player.Cell}");
CheckActionTexts();
Shot("village"); Shot("village");
Press(Key.W, true); Press(Key.W, true);
Next(); Next();
@@ -188,11 +193,46 @@ namespace Cthangover.Mods.Rpg.Tests
Godot.Engine.TimeScale = 1; Godot.Engine.TimeScale = 1;
Check(View.Player.Cell == new Vector2I(6, 8), $"the battle returns to the same cell, got {View.Player.Cell}"); 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"); 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(); Finish();
return; return;
} }
} }
/// <summary>
/// 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.
/// </summary>
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<ActionCharacter>())
{
foreach (var key in new[] { action.Name, action.Description })
Check(!string.IsNullOrEmpty(key) && TranslationServer.Translate(key) != key, $"action text '{key}' is translated");
}
}
/// <summary>Advances a running dialog with the action key; true while it is still running.</summary> /// <summary>Advances a running dialog with the action key; true while it is still running.</summary>
private bool AdvanceDialog() private bool AdvanceDialog()
{ {