Map events apart from the map: map_events/<map>/*.json of any mod merged by id (RpgMapEventResolver, RpgMapEventMerge); item Icon look for events; rpg_walk checks a pickup across leaving the map and loading a save

This commit is contained in:
cthangover committed 2026-10-23 19:09:37 +02:00
1 parent 281b722667
commit c4d6adf8fb
13 files changed
+396 -20

No files matched your search

+89 -8
View File
@@ -6,6 +6,7 @@ using Cthangover.Core.Factories.Impls;
using Cthangover.Core.Games;
using Cthangover.Core.Scenes;
using Cthangover.Core.Settings;
using Cthangover.Core.Items;
using Cthangover.Core.Utils;
using Cthangover.Mods.Interface.View;
using Godot;
@@ -17,6 +18,13 @@ namespace Cthangover.Mods.Rpg.Tests
/// <see cref="Input.ParseInputEvent"/>, so the world reads them exactly as
/// it reads a keyboard (physical keys, held state, unhandled action key).
/// <para/>
/// A pickup is checked across map rebuilds: the suite's own
/// <c>map_events/rpg_village/rpg_walk_test.json</c> lays a mushroom on the demo's
/// village (an event added by another mod, shaped like the map editor's
/// pickups); it is picked up, the player leaves the map and comes back, then
/// saves, loads and comes back again — it must never reappear nor give a
/// second item.
/// <para/>
/// One step per timer tick; a step either waits (returns without
/// advancing) or checks and moves on. A failed check is recorded and the
/// run goes on; an exception ends it. Result line:
@@ -31,12 +39,18 @@ namespace Cthangover.Mods.Rpg.Tests
private const string House = "rpg_elder_house";
private const string WinFlag = "rpg_test_win";
private const string SaveSlot = "slot_9901";
private const string PickupId = "rpg_walk_mushroom";
private const string PickupFlag = "rpg_village_rpg_walk_mushroom_taken";
private const string Mushroom = "rpg_demo/mushroom";
private static readonly Vector2I PickupCell = new(18, 14);
private readonly string shotsDir;
private readonly List<string> failures = new();
private int passed;
private int step;
private int waitTicks;
private int mushroomsBefore;
private double elapsed;
/// <summary>Creates the driver; <paramref name="shotsDir"/> null disables screenshots.</summary>
@@ -51,6 +65,10 @@ namespace Cthangover.Mods.Rpg.Tests
private static RpgWorldView View => RpgWorldHost.Current?.View;
private static IInventory Inventory => GameData.Instance.Runtime.Inventory;
private static string Flag(string name) => GameData.Instance.Runtime.Flags.TryGetValue(name, out var value) ? value : null;
private void Tick()
{
if (step < 0)
@@ -142,17 +160,63 @@ namespace Cthangover.Mods.Rpg.Tests
case 9:
if (AdvanceDialog())
return;
// A pickup this suite adds to the demo's village (map_events of another mod).
mushroomsBefore = Inventory.CheckCount(Mushroom);
Check(View.FindEvent(PickupId)?.Visible == true, "a test mod's map_events file adds a pickup to the demo's village");
View.PlacePlayer(PickupCell.X - 1, PickupCell.Y, RpgDirection.Right);
Press(Key.D, true);
Next();
return;
case 10:
if (View.Player.Cell.X < PickupCell.X)
return;
Press(Key.D, false);
Next();
return;
case 11:
// Stepping on the pickup starts its dialog; the flag is set when it ends.
if (AdvanceDialog() || Flag(PickupFlag) != "1")
return;
Check(Inventory.CheckCount(Mushroom) == mushroomsBefore + 1, $"stepping on the pickup adds the item, {Inventory.CheckCount(Mushroom)} now");
Check(View.FindEvent(PickupId)?.Visible == false, "the picked item leaves the map");
RpgWorldHost.Teleport(new RpgTeleport { Map = House, X = 6, Y = 8, Dir = RpgDirection.Up });
Next();
return;
case 12:
if (View?.Map?.ID != House || GodotSceneService.IsTransitioning || AdvanceDialog())
return;
RpgWorldHost.Teleport(new RpgTeleport { Map = Village, X = PickupCell.X - 1, Y = PickupCell.Y, Dir = RpgDirection.Right });
Next();
return;
case 13:
if (View?.Map?.ID != Village || GodotSceneService.IsTransitioning || AdvanceDialog())
return;
CheckPickupGone("after leaving the map and coming back");
Press(Key.D, true);
Next();
return;
case 14:
if (View.Player.Cell.X < PickupCell.X)
return;
Press(Key.D, false);
Next();
return;
case 15:
if (View.Player.IsMoving || ++waitTicks < 5)
return;
Check(RpgInput.FindDialogBox()?.IsRunning != true && Inventory.CheckCount(Mushroom) == mushroomsBefore + 1,
"stepping on the empty cell again gives nothing");
View.PlacePlayer(6, 7, RpgDirection.Up);
Press(Key.W, true);
Next();
return;
case 10:
case 16:
if (!View.Player.IsMoving && View.Map.ID == Village)
return;
Press(Key.W, false);
Next();
return;
case 11:
case 17:
if (View?.Map?.ID != House)
return;
if (AdvanceDialog())
@@ -163,14 +227,14 @@ namespace Cthangover.Mods.Rpg.Tests
ScenarioCondition.SetFlag("rpg_wolf_dead", "1");
Next();
return;
case 12:
case 18:
Check(View.FindEvent("elder_quest")?.Visible == false && View.FindEvent("elder_thanks")?.Visible == true, "a story flag swaps the elder's events at once");
ScenarioCondition.SetFlag("rpg_wolf_dead", "0");
Check(RpgBattleService.Start("rpg_slime", WinFlag), "a battle starts from the map");
Godot.Engine.TimeScale = 4;
Next();
return;
case 13:
case 19:
if (GetTree().CurrentScene is not BattleSceneContext battle)
return;
// The core's auto turn only passes, so the hero would lose: once the
@@ -184,14 +248,14 @@ namespace Cthangover.Mods.Rpg.Tests
battle.ShowWinground();
Next();
return;
case 14:
case 20:
if (GetTree().Root.FindChild("ContinueButton", true, false) is not Button button || !button.IsVisibleInTree())
return;
Shot("victory");
button.EmitSignal(BaseButton.SignalName.Pressed);
Next();
return;
case 15:
case 21:
if (View?.Map?.ID != House || GodotSceneService.IsTransitioning)
return;
Godot.Engine.TimeScale = 1;
@@ -202,19 +266,27 @@ namespace Cthangover.Mods.Rpg.Tests
GameSession.OpenMainMenu(GetTree());
Next();
return;
case 16:
case 22:
if (RpgWorldHost.Current != null || ++waitTicks < 5)
return;
Check(GameSession.LoadAndEnter(this, SaveSlot), "the save loads");
Next();
return;
case 17:
case 23:
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}");
Check(Flag(PickupFlag) == "1" && Inventory.CheckCount(Mushroom) == mushroomsBefore + 1, "the save keeps the pickup's flag and the picked item");
RpgWorldHost.Teleport(new RpgTeleport { Map = Village, X = PickupCell.X - 1, Y = PickupCell.Y, Dir = RpgDirection.Right });
Next();
return;
case 24:
if (View?.Map?.ID != Village || GodotSceneService.IsTransitioning || AdvanceDialog())
return;
CheckPickupGone("after loading the save");
SaveService.DeleteSave(SaveSlot);
Finish();
return;
@@ -237,6 +309,15 @@ namespace Cthangover.Mods.Rpg.Tests
}
}
/// <summary>The map was built again (a teleport or a load): the picked item must stay gone and give nothing.</summary>
private void CheckPickupGone(string when)
{
var pickup = View.FindEvent(PickupId);
Check(pickup != null && !pickup.Visible, $"the picked item does not come back {when}");
Check(View.EventAt(PickupCell, RpgEventTrigger.Touch) == null, $"its cell has no pickup to step on {when}");
Check(Inventory.CheckCount(Mushroom) == mushroomsBefore + 1, $"the inventory still holds exactly the one picked item {when}");
}
/// <summary>Advances a running dialog with the action key; true while it is still running.</summary>
private bool AdvanceDialog()
{