Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9bf0ec9de6 | ||
|
|
ce62db9553 | ||
|
|
98abf41a3c | ||
|
|
b9f2b82d5d | ||
|
|
a48f064f54 | ||
|
|
4f8e123418 | ||
|
|
b30f98c870 | ||
|
|
2ea57b7240 | ||
|
|
1d1192977b |
No files matched your search
@@ -0,0 +1,30 @@
|
|||||||
|
# Line endings: LF everywhere, on every OS, whatever core.autocrlf says.
|
||||||
|
* text=auto eol=lf
|
||||||
|
|
||||||
|
# cmd.exe misreads labels and goto in LF batch files, so they stay CRLF.
|
||||||
|
*.bat text eol=crlf
|
||||||
|
*.cmd text eol=crlf
|
||||||
|
|
||||||
|
# Binaries: never converted, never diffed as text.
|
||||||
|
*.png binary
|
||||||
|
*.jpg binary
|
||||||
|
*.jpeg binary
|
||||||
|
*.webp binary
|
||||||
|
*.svg text
|
||||||
|
*.ogg binary
|
||||||
|
*.wav binary
|
||||||
|
*.mp3 binary
|
||||||
|
*.ttf binary
|
||||||
|
*.otf binary
|
||||||
|
*.woff binary
|
||||||
|
*.woff2 binary
|
||||||
|
*.dll binary
|
||||||
|
*.so binary
|
||||||
|
*.dylib binary
|
||||||
|
*.pdb binary
|
||||||
|
*.exe binary
|
||||||
|
*.zip binary
|
||||||
|
*.pck binary
|
||||||
|
*.moc3 binary
|
||||||
|
*.res binary
|
||||||
|
*.scn binary
|
||||||
|
Before Width: | Height: | Size: 231 KiB After Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 371 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 381 KiB After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 162 KiB After Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 1005 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 155 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 728 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 321 KiB |
|
After Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 61 KiB |
@@ -49,12 +49,6 @@ recipe/wolf_meat_to_ration/desc = A recipe describing how to properly cook wolf
|
|||||||
characters/player/marao = Marao
|
characters/player/marao = Marao
|
||||||
characters/enemies/wolf = Wolf
|
characters/enemies/wolf = Wolf
|
||||||
characters/enemies/werewolf_girl = Werewolf
|
characters/enemies/werewolf_girl = Werewolf
|
||||||
characters/action/physics_attack/name = Basic attack
|
|
||||||
characters/action/physics_attack/description = Attacks an enemy with a weapon, with a damage multiplier of <g>1.0</g>
|
|
||||||
characters/action/defence/name = Defence
|
|
||||||
characters/action/defence/description = Protects the target with a defence multiplier of <g>1.0</g>
|
|
||||||
characters/action/stun/name = Stun
|
|
||||||
characters/action/stun/description = Stuns the target for 2 turns
|
|
||||||
characters/skills/slave_master_name = Slave Trader
|
characters/skills/slave_master_name = Slave Trader
|
||||||
characters/skills/slave_master = Gives the ability to enslave a defeated enemy with a base chance of 10%. The upkeep of the slave falls on your shoulders.
|
characters/skills/slave_master = Gives the ability to enslave a defeated enemy with a base chance of 10%. The upkeep of the slave falls on your shoulders.
|
||||||
characters/skills/effects/recruit = recruitment chance
|
characters/skills/effects/recruit = recruitment chance
|
||||||
|
|||||||
@@ -49,12 +49,6 @@ recipe/wolf_meat_to_ration/desc = Рецепт описывает как пра
|
|||||||
characters/player/marao = Марао
|
characters/player/marao = Марао
|
||||||
characters/enemies/wolf = Волк
|
characters/enemies/wolf = Волк
|
||||||
characters/enemies/werewolf_girl = Оборотень
|
characters/enemies/werewolf_girl = Оборотень
|
||||||
characters/action/physics_attack/name = Простая атака
|
|
||||||
characters/action/physics_attack/description = Атакует врага оружием, с коэффициентом урона <g>1.0</g>
|
|
||||||
characters/action/defence/name = Защита
|
|
||||||
characters/action/defence/description = Защищает цель с коэффициентом защиты <g>1.0</g>
|
|
||||||
characters/action/stun/name = Ошеломление
|
|
||||||
characters/action/stun/description = Ошеломляет цель на 2 хода
|
|
||||||
characters/skills/slave_master_name = Работорговец
|
characters/skills/slave_master_name = Работорговец
|
||||||
characters/skills/slave_master = Даёт возможность взять в рабство поверженного врага с базовым шансом в 10%. Содержание раба ложится на твои плечи.
|
characters/skills/slave_master = Даёт возможность взять в рабство поверженного врага с базовым шансом в 10%. Содержание раба ложится на твои плечи.
|
||||||
characters/skills/effects/recruit = шанс рекрутинга
|
characters/skills/effects/recruit = шанс рекрутинга
|
||||||
|
|||||||
@@ -5,5 +5,8 @@
|
|||||||
"author": "ct",
|
"author": "ct",
|
||||||
"description": "Story content of the Cthangover game: scenes, scenarios, characters, items, quests, backgrounds and the main game collection.",
|
"description": "Story content of the Cthangover game: scenes, scenarios, characters, items, quests, backgrounds and the main game collection.",
|
||||||
"category": "story",
|
"category": "story",
|
||||||
"depends": ["shell2d_base>=1", "dialog_base>=1", "interactive_base>=1", "skills_base>=1", "map_base>=1", "ui_base>=1", "characters_base>=1", "items_base>=1", "quests_base>=1", "town_pack>=1"]
|
"depends": ["shell2d_base>=1", "dialog_base>=1", "interactive_base>=1", "skills_base>=1", "map_base>=1", "ui_base>=1", "characters_base>=1", "items_base>=1", "quests_base>=1", "town_pack>=1"],
|
||||||
|
"test_suites": [
|
||||||
|
{"id": "site_shots", "activate": ["--itest=site_shots"]}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
@@ -2,5 +2,8 @@ scene: start_scene
|
|||||||
priority: 0
|
priority: 0
|
||||||
is_one_run: true
|
is_one_run: true
|
||||||
---
|
---
|
||||||
|
# The starter kit of a new game (it used to be hard-coded in items_base's inventory).
|
||||||
|
action item.add id=food/wolf_meat count=10
|
||||||
|
action item.add id=resource/branches count=10
|
||||||
switch_scene prolog_alley
|
switch_scene prolog_alley
|
||||||
end
|
end
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "town_entry",
|
"name": "town_entry",
|
||||||
"defaultBackground": ["town/town_entry_0", "town/town_entry_1", "town/town_entry_2", "town/town_entry_3", "town/town_entry_4"],
|
"defaultBackground": "town/town_entry",
|
||||||
"defaultAmbient": "",
|
"defaultAmbient": "",
|
||||||
"defaultScenario": "scenarios/town/town_entry_default.scenario"
|
"defaultScenario": "scenarios/town/town_entry_default.scenario"
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "Site Screenshots",
|
||||||
|
"description": "Takes the screenshots of the cthangover.ru site from the main game: the prologue dialog, several locations with their scenarios, the inventory, character, map, journal and skills panels, and a card battle in front of the town entry. Activated by --itest=site_shots with --game=cthangover --new and --shots=<dir> (windowed runs only); the language comes from the player's settings.",
|
||||||
|
"sources": [
|
||||||
|
"src/*.cs"
|
||||||
|
],
|
||||||
|
"depends": [
|
||||||
|
"cthangover_game>=2",
|
||||||
|
"turn_battle_base>=2"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,272 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Cthangover.Core.Bootstrap;
|
||||||
|
using Cthangover.Core.Events;
|
||||||
|
using Cthangover.Core.Scenes;
|
||||||
|
using Cthangover.Core.Settings;
|
||||||
|
using Cthangover.Core.UI;
|
||||||
|
using Cthangover.Core.Utils;
|
||||||
|
using Cthangover.Mods.Dialog;
|
||||||
|
using Cthangover.Mods.Interface.View;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace Cthangover.Mods.CthangoverGame.Tests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Walks a new game of cthangover through the frames the site shows and saves
|
||||||
|
/// a picture of each: locations entered with <see cref="SceneManager.SwitchScene"/>
|
||||||
|
/// (their own scenarios start the dialog, so every frame is a real novel
|
||||||
|
/// screen), the toolbar panels opened with <see cref="UiRegistry.Toggle"/> like
|
||||||
|
/// the toolbar buttons do, and Murakami's story at the town entry played by
|
||||||
|
/// answering its menu — the wolf scene and the card battle it starts.
|
||||||
|
/// <para/>
|
||||||
|
/// The prologue's scenarios have no locale keys (Russian only), so its dialog
|
||||||
|
/// is aborted at once and its scenes are left out. Before every switch the
|
||||||
|
/// clock is set to noon (a new game starts at night and the night lighting
|
||||||
|
/// leaves only lamps visible) and the running dialog is aborted the way the
|
||||||
|
/// scene editor's "Go to scene" does — otherwise the entered scene's scenario
|
||||||
|
/// waits behind it. Before the panels the inventory is seeded and Murakami
|
||||||
|
/// joins the party, so they are not empty. Waits are tick counts, not
|
||||||
|
/// conditions on the picture; the checks only make sure each scene, panel and
|
||||||
|
/// the battle were really reached. Result line:
|
||||||
|
/// <c>[TEST_RESULT] name=site_shots passed=N failed=M</c>, exit 0/1, and a
|
||||||
|
/// 240 s watchdog.
|
||||||
|
/// </summary>
|
||||||
|
public partial class SiteShotsDriver : Node
|
||||||
|
{
|
||||||
|
private const float TickSeconds = 0.1f;
|
||||||
|
private const float TimeoutSeconds = 240f;
|
||||||
|
private const int SettleTicks = 40;
|
||||||
|
private const int PanelTicks = 15;
|
||||||
|
private const int LineTicks = 25;
|
||||||
|
private const long MinutesPerDay = 24 * 60;
|
||||||
|
private const long Noon = 12 * 60;
|
||||||
|
private const string Prologue = "prolog_alley";
|
||||||
|
private const string StoryScene = "town_entry";
|
||||||
|
private const int AlleyOption = 1;
|
||||||
|
|
||||||
|
private static readonly string[] Locations =
|
||||||
|
{
|
||||||
|
"home_outside", "home_vestibule", "home_livingroom", "home_kitchen",
|
||||||
|
"home_bedroom", "home_workroom", "home_basement", "town_outskirts"
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly string[] Panels = { "inventory", "characters", "map", "journal", "skills" };
|
||||||
|
|
||||||
|
private readonly string shotsDir;
|
||||||
|
private readonly List<string> failures = new();
|
||||||
|
private int passed;
|
||||||
|
private int step;
|
||||||
|
private int index;
|
||||||
|
private int waitTicks;
|
||||||
|
private double elapsed;
|
||||||
|
private bool battleStarted;
|
||||||
|
|
||||||
|
/// <summary>Creates the driver; <paramref name="shotsDir"/> null disables screenshots.</summary>
|
||||||
|
public SiteShotsDriver(string shotsDir)
|
||||||
|
{
|
||||||
|
this.shotsDir = shotsDir;
|
||||||
|
Name = "SiteShotsDriver";
|
||||||
|
var timer = new Timer { WaitTime = TickSeconds, Autostart = true };
|
||||||
|
AddChild(timer);
|
||||||
|
timer.Timeout += Tick;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Listens for the battle scene.</summary>
|
||||||
|
public override void _EnterTree() => ModEventBus.Subscribe<BattleStartedEvent>(OnBattleStarted);
|
||||||
|
|
||||||
|
/// <summary>Stops listening.</summary>
|
||||||
|
public override void _ExitTree() => ModEventBus.Unsubscribe<BattleStartedEvent>(OnBattleStarted);
|
||||||
|
|
||||||
|
private SceneManager Scenes => GetTree().Root.GetNodeOrNull<SceneManager>("SceneManager");
|
||||||
|
|
||||||
|
private static DialogBox Box => SceneContextNode.FindNode<DialogBox>("DialogBox");
|
||||||
|
|
||||||
|
private void OnBattleStarted(BattleStartedEvent e) => battleStarted = true;
|
||||||
|
|
||||||
|
private void Tick()
|
||||||
|
{
|
||||||
|
if (step < 0)
|
||||||
|
return;
|
||||||
|
elapsed += TickSeconds;
|
||||||
|
if (elapsed > TimeoutSeconds)
|
||||||
|
{
|
||||||
|
Fail($"timeout at step {step} (scene='{SceneManager.GetCurrentSceneName()}')");
|
||||||
|
Finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (waitTicks > 0)
|
||||||
|
{
|
||||||
|
waitTicks--;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
RunStep();
|
||||||
|
}
|
||||||
|
catch (System.Exception ex)
|
||||||
|
{
|
||||||
|
Fail($"step {step}: {ex.GetType().Name}: {ex.Message}");
|
||||||
|
Finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RunStep()
|
||||||
|
{
|
||||||
|
switch (step)
|
||||||
|
{
|
||||||
|
case 0:
|
||||||
|
if (!Boot.IsReady || SceneManager.GetCurrentSceneName() != Prologue)
|
||||||
|
return;
|
||||||
|
Check(true, $"entered '{Prologue}'");
|
||||||
|
index = 0;
|
||||||
|
Next(PanelTicks);
|
||||||
|
return;
|
||||||
|
case 1:
|
||||||
|
GoTo(Locations[index]);
|
||||||
|
Next(0);
|
||||||
|
return;
|
||||||
|
case 2:
|
||||||
|
if (SceneManager.GetCurrentSceneName() != Locations[index])
|
||||||
|
return;
|
||||||
|
Check(true, $"entered '{Locations[index]}'");
|
||||||
|
Next(SettleTicks);
|
||||||
|
return;
|
||||||
|
case 3:
|
||||||
|
Shot("location_" + Locations[index]);
|
||||||
|
index++;
|
||||||
|
if (index < Locations.Length)
|
||||||
|
{
|
||||||
|
step = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Seed();
|
||||||
|
index = 0;
|
||||||
|
Next(PanelTicks);
|
||||||
|
return;
|
||||||
|
case 4:
|
||||||
|
UiRegistry.Toggle(Panels[index]);
|
||||||
|
Next(PanelTicks);
|
||||||
|
return;
|
||||||
|
case 5:
|
||||||
|
Check(UiHost.Instance.TryGetInstance(Panels[index], out var panel) && panel.Visible,
|
||||||
|
$"panel '{Panels[index]}' is open");
|
||||||
|
Shot("panel_" + Panels[index]);
|
||||||
|
UiRegistry.Toggle(Panels[index]);
|
||||||
|
index++;
|
||||||
|
if (index < Panels.Length)
|
||||||
|
{
|
||||||
|
step = 4;
|
||||||
|
waitTicks = PanelTicks;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Next(PanelTicks);
|
||||||
|
return;
|
||||||
|
case 6:
|
||||||
|
GoTo(StoryScene);
|
||||||
|
Next(0);
|
||||||
|
return;
|
||||||
|
case 7:
|
||||||
|
if (SceneManager.GetCurrentSceneName() != StoryScene || Box?.IsWaitingAnswer != true)
|
||||||
|
return;
|
||||||
|
Check(true, $"Murakami's story waits for an answer in '{StoryScene}'");
|
||||||
|
Next(SettleTicks);
|
||||||
|
return;
|
||||||
|
case 8:
|
||||||
|
Shot("novel_menu");
|
||||||
|
Check(Box.TrySelectVariant(AlleyOption), "the alley option is taken");
|
||||||
|
Next(SettleTicks);
|
||||||
|
return;
|
||||||
|
case 9:
|
||||||
|
Shot("novel_wolf");
|
||||||
|
Box.NextAction();
|
||||||
|
Next(SettleTicks);
|
||||||
|
return;
|
||||||
|
case 10:
|
||||||
|
Shot("novel_marao");
|
||||||
|
Next(0);
|
||||||
|
return;
|
||||||
|
case 11:
|
||||||
|
if (battleStarted)
|
||||||
|
{
|
||||||
|
Check(true, "the card battle started");
|
||||||
|
Next(SettleTicks);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Box != null && !Box.IsDialogEnded && !Box.IsWaitingAnswer)
|
||||||
|
Box.NextAction();
|
||||||
|
waitTicks = LineTicks;
|
||||||
|
return;
|
||||||
|
case 12:
|
||||||
|
Shot("battle");
|
||||||
|
Next(SettleTicks);
|
||||||
|
return;
|
||||||
|
case 13:
|
||||||
|
Shot("battle_later");
|
||||||
|
Finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Noon, no dialog left running, then the switch.</summary>
|
||||||
|
private void GoTo(string scene)
|
||||||
|
{
|
||||||
|
var time = GameData.Instance.Runtime.Time;
|
||||||
|
time.SetTime(time.Tick - time.Tick % MinutesPerDay + Noon);
|
||||||
|
Box?.Abort();
|
||||||
|
Scenes.SwitchScene(scene);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>A few stacks for the inventory and a second hero for the party panel.</summary>
|
||||||
|
private static void Seed()
|
||||||
|
{
|
||||||
|
var runtime = GameData.Instance.Runtime;
|
||||||
|
runtime.Inventory.Add("food/ration", 3);
|
||||||
|
runtime.Inventory.Add("food/wolf_meat", 4);
|
||||||
|
runtime.Inventory.Add("resource/branches", 6);
|
||||||
|
runtime.Inventory.Add("recipe/wolf_meat_to_ration");
|
||||||
|
runtime.CharacterData.AddCharacterToParty("Murakami");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Next(int ticks)
|
||||||
|
{
|
||||||
|
step++;
|
||||||
|
waitTicks = ticks;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Check(bool condition, string what)
|
||||||
|
{
|
||||||
|
if (condition)
|
||||||
|
{
|
||||||
|
passed++;
|
||||||
|
GameLogger.Log("TEST", $"[PASS] {what}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Fail(what);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Fail(string what)
|
||||||
|
{
|
||||||
|
failures.Add(what);
|
||||||
|
GameLogger.Log("TEST", $"[FAIL] {what}", LogLevel.Error);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Shot(string name)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(shotsDir) || DisplayServer.GetName() == "headless")
|
||||||
|
return;
|
||||||
|
DirAccess.MakeDirRecursiveAbsolute(shotsDir);
|
||||||
|
ViewportCapture.SavePng(GetViewport(), shotsDir.TrimEnd('/') + "/" + name + ".png");
|
||||||
|
GameLogger.Log("TEST", $"SiteShotsDriver: saved {name}.png");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Finish()
|
||||||
|
{
|
||||||
|
step = -1;
|
||||||
|
GameLogger.Log("TEST", $"[TEST_RESULT] name=site_shots passed={passed} failed={failures.Count}");
|
||||||
|
GetTree().Quit(failures.Count == 0 ? 0 : 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using Cthangover.Core.Mods;
|
||||||
|
using Cthangover.Core.Utils;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace Cthangover.Mods.CthangoverGame.Tests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Entry point of the site_shots suite: adds the driver to the tree root when
|
||||||
|
/// the exact flag <c>--itest=site_shots</c> is given (the driver quits the
|
||||||
|
/// process, so it must never run in another test). <c>--shots=<dir></c>
|
||||||
|
/// is where the pictures go; without it the driver only walks the steps.
|
||||||
|
/// </summary>
|
||||||
|
public class SiteShotsTestInitializer : IModInitializer
|
||||||
|
{
|
||||||
|
private static bool driverQueued;
|
||||||
|
|
||||||
|
/// <summary>Nothing per mod.</summary>
|
||||||
|
public void OnModLoaded(string modId)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Queues the driver once.</summary>
|
||||||
|
public void OnModResourcesReady()
|
||||||
|
{
|
||||||
|
var args = OS.GetCmdlineUserArgs();
|
||||||
|
if (driverQueued || !args.Any(a => a == "--itest=site_shots"))
|
||||||
|
return;
|
||||||
|
|
||||||
|
driverQueued = true;
|
||||||
|
var shots = args.Where(a => a.StartsWith("--shots=", System.StringComparison.Ordinal))
|
||||||
|
.Select(a => a.Substring("--shots=".Length))
|
||||||
|
.FirstOrDefault();
|
||||||
|
GameLogger.Log("TEST", $"SiteShotsTestInitializer: activating site_shots, shots={shots ?? "off"}");
|
||||||
|
(Godot.Engine.GetMainLoop() as SceneTree)?.Root?.CallDeferred("add_child", new SiteShotsDriver(shots));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||