diff --git a/locale/en.properties b/locale/en.properties
new file mode 100644
index 0000000..6b0f784
--- /dev/null
+++ b/locale/en.properties
@@ -0,0 +1,2 @@
+mod_turn_battle_base_name = Turn-based Battle Engine
+mod_turn_battle_base_desc = Turn-based battle platform: battle ruleset contracts (IBattleCore), the core registry, action executors, status effects, the battle scene, victory/defeat screens and EXP/loot reports.
diff --git a/locale/ru.properties b/locale/ru.properties
new file mode 100644
index 0000000..6daf4a7
--- /dev/null
+++ b/locale/ru.properties
@@ -0,0 +1,2 @@
+mod_turn_battle_base_name = Движок пошаговых боёв
+mod_turn_battle_base_desc = Платформа пошаговых боёв: контракты боевых ядер (IBattleCore), реестр ядер, исполнители действий, статус-эффекты, боевая сцена, экраны победы/поражения и отчёты опыта и добычи.
diff --git a/manifest.json b/manifest.json
index 24b1d49..55884df 100644
--- a/manifest.json
+++ b/manifest.json
@@ -1,4 +1,4 @@
-{
+{
"id": "turn_battle_base",
"name": "Turn-based battle engine",
"version": "2.0.0",
@@ -8,5 +8,5 @@
"sources": [
"src/**/*.cs"
],
- "depends": [ "visual_novel_core>=1", "interface>=1" ]
+ "depends": [ "characters_base>=1", "visual_novel_core>=1", "interface>=1", "items_base>=1", "quests_base>=1", "time_base>=1" ]
}
diff --git a/scenes/battle/battle.tscn b/scenes/battle/battle.tscn
index eb76acd..80ceb9e 100644
--- a/scenes/battle/battle.tscn
+++ b/scenes/battle/battle.tscn
@@ -4,7 +4,7 @@
[ext_resource type="Script" path="src/BattleSceneContext.cs" id="BattleSceneContextScript"]
[ext_resource type="Script" path="src/BattleSpeedControl.cs" id="BattleSpeedControlScript"]
[ext_resource type="PackedScene" uid="uid://bjhtml8ujba7p" path="res://scenes/battle/deadground.tscn" id="DeadgroundScene"]
-[ext_resource type="Script" uid="uid://icsu2hv2c68r" path="res://Core/UI/Lights/UiLightController.cs" id="UiLightControllerScript"]
+[ext_resource type="Script" uid="uid://icsu2hv2c68r" path="res://mods/time_base/src/Lights/UiLightController.cs" id="UiLightControllerScript"]
[ext_resource type="PackedScene" uid="uid://b3yq032kqhtn4" path="res://scenes/battle/winground.tscn" id="WingroundScene"]
[node name="BattleScene" type="Control" unique_id=760066045]
diff --git a/src/Actions/BattleActionMachine.cs b/src/Actions/BattleActionMachine.cs
index 1e1fb9c..d20e2ab 100644
--- a/src/Actions/BattleActionMachine.cs
+++ b/src/Actions/BattleActionMachine.cs
@@ -2,6 +2,7 @@ using System.Collections.Generic;
using Cthangover.Core.UI;
using Godot;
+using Cthangover.Mods.Interface.Base;
namespace Cthangover.Core.Battle.Actions
{
@@ -37,12 +38,14 @@ namespace Cthangover.Core.Battle.Actions
///
public event StopMachineDelegate OnStopMachine;
+ /// Stops the machine and fires when the node leaves the tree, so subscribers never wait on a dead node.
protected override void OnExitTree()
{
base.OnExitTree();
StopMachine();
}
+ /// Drives the queued actions once per frame; fires when the queue drains.
protected override void OnProcess(double delta)
{
if (Actions.Count == 0 && CurrentAction == null)
diff --git a/src/BattleEndedEvent.cs b/src/BattleEndedEvent.cs
index 50c7159..733817f 100644
--- a/src/BattleEndedEvent.cs
+++ b/src/BattleEndedEvent.cs
@@ -1,11 +1,20 @@
namespace Cthangover.Core.Events
{
+ ///
+ /// Published when the battle concludes (victory or defeat), after the
+ /// victory/defeat screen resolves. Mods can use it for post-battle cleanup
+ /// or story state. Note: the group defeat with loot and recruitment payload
+ /// is the separate , which fires from
+ /// the victory screen.
+ ///
[LifecycleEvent]
[EventKey("battle_ended")]
public class BattleEndedEvent : ModEvent
{
+ /// Outcome reported by the battle core (e.g. "victory", "defeat").
public string Result { get; init; }
+ /// Number of turns the battle lasted.
public int Turns { get; init; }
}
}
diff --git a/src/BattleSceneContext.cs b/src/BattleSceneContext.cs
index d11a1fb..82d88a8 100644
--- a/src/BattleSceneContext.cs
+++ b/src/BattleSceneContext.cs
@@ -5,12 +5,14 @@ using Cthangover.Core.Factories.Impls;
using Cthangover.Core.Mods;
using Cthangover.Core.Settings;
using Cthangover.Core.UI;
-using Cthangover.Core.UI.Lights;
+using Cthangover.Mods.Time;
using Cthangover.Core.Characters;
using Cthangover.Core.Mods.Resolvers;
using Cthangover.Core.Utils;
using Godot;
+using Cthangover.Mods.Interface.Base;
+using Cthangover.Mods.Interface.Base.Lists;
namespace Cthangover.Core.Battle
{
///
@@ -120,6 +122,7 @@ namespace Cthangover.Core.Battle
});
}
+ /// Claims the scene singleton and registers as the provider.
protected override void OnEnterTree()
{
if (Instance != null && GodotObject.IsInstanceValid(Instance))
@@ -133,6 +136,7 @@ namespace Cthangover.Core.Battle
ActiveCharacterRegistry.Provider = this;
}
+ /// Releases the singleton slot and the active-character provider so a later scene can claim them.
protected override void OnExitTree()
{
base.OnExitTree();
@@ -154,6 +158,7 @@ namespace Cthangover.Core.Battle
///
public IBattleCore ActiveBattleCore => activeBattleCore;
+ /// Caches the battle backdrop node, resolves the active and starts the battle.
protected override void OnReady()
{
battleBg = GetNodeOrNull("BattleBg");
diff --git a/src/BattleService.cs b/src/BattleService.cs
index bdc2c41..ec66317 100644
--- a/src/BattleService.cs
+++ b/src/BattleService.cs
@@ -4,7 +4,7 @@ using Cthangover.Core.Factories.Impls;
using Cthangover.Core.Quests;
using Cthangover.Core.Scenes;
using Cthangover.Core.Settings;
-using Cthangover.Core.UI.Lights;
+using Cthangover.Mods.Time;
using Cthangover.Core.Utils;
namespace Cthangover.Core.Battle
diff --git a/src/BattleSpeedControl.cs b/src/BattleSpeedControl.cs
index 64bbfd9..7cbd738 100644
--- a/src/BattleSpeedControl.cs
+++ b/src/BattleSpeedControl.cs
@@ -8,7 +8,7 @@ namespace Cthangover.TurnBattle
{
///
/// In-battle speed toggle bar (x1/x4/x8). Directly sets
- /// Engine.TimeScale to globally accelerate the entire game loop —
+ /// Godot.Engine.TimeScale to globally accelerate the entire game loop —
/// not just animations. Persists the choice to GameData.Settings
/// so it survives scene reloads. restores 1x
/// without saving and is the part the core scene consumes through
@@ -71,25 +71,25 @@ namespace Cthangover.TurnBattle
speed = 1;
_activeSpeed = speed;
- Engine.TimeScale = speed;
+ Godot.Engine.TimeScale = speed;
UpdateHighlight();
}
///
- /// Resets to 1.0x without
+ /// Resets to 1.0x without
/// persisting to settings. Used when battle ends so the lobby
/// and menus run at normal speed regardless of the player's
/// in-battle preference.
///
public void ResetToNormal()
{
- Engine.TimeScale = 1.0f;
+ Godot.Engine.TimeScale = 1.0f;
}
private void OnSpeedSelected(int speed)
{
_activeSpeed = speed;
- Engine.TimeScale = speed;
+ Godot.Engine.TimeScale = speed;
var settings = GameData.Instance.Settings;
settings.BattleSpeed = speed;
diff --git a/src/BattleStartedEvent.cs b/src/BattleStartedEvent.cs
index c86c4d8..f6ff610 100644
--- a/src/BattleStartedEvent.cs
+++ b/src/BattleStartedEvent.cs
@@ -1,10 +1,19 @@
namespace Cthangover.Core.Events
{
+ ///
+ /// Published when a battle encounter is initialised (from
+ /// , before the battle
+ /// scene loads). Lets mods observe or prepare for the fight — e.g. pre-buff
+ /// party members or record the encounter for analytics. The
+ /// [LifecycleEvent] marker keeps it out of condition-cached flows.
+ ///
[LifecycleEvent]
[EventKey("battle_started")]
public class BattleStartedEvent : ModEvent
{
+ /// Logical name of the battle scene route being entered (e.g. "Battle").
public string BattleType { get; init; }
+ /// Template ids of the enemies participating in the encounter.
public string[] Enemies { get; init; }
}
}
diff --git a/src/CharacterDiedEvent.cs b/src/CharacterDiedEvent.cs
index 32642c0..99319f0 100644
--- a/src/CharacterDiedEvent.cs
+++ b/src/CharacterDiedEvent.cs
@@ -8,8 +8,11 @@ namespace Cthangover.Core.Events
[EventKey("character_died")]
public class CharacterDiedEvent : ModEvent
{
+ /// Template id of the character that died.
public string CharacterId { get; init; }
+ /// Display name at time of death (resolved locale string).
public string CharacterName { get; init; }
+ /// Free-form id of the reporting battle system.
public string Source { get; init; }
}
}
diff --git a/src/DeadgroundBehaviour.cs b/src/DeadgroundBehaviour.cs
index 8d977e1..5605b94 100644
--- a/src/DeadgroundBehaviour.cs
+++ b/src/DeadgroundBehaviour.cs
@@ -4,7 +4,7 @@ using Cthangover.Core.Mods;
using Cthangover.Core.Mods.Resolvers;
using Cthangover.Core.Scenes;
using Cthangover.Core.Settings;
-using Cthangover.Core.UI.Base.Lists;
+using Cthangover.Mods.Interface.Base.Lists;
using Cthangover.Core.UI.Theming;
using Cthangover.Core.Utils;
using Godot;
@@ -41,7 +41,7 @@ namespace Cthangover.TurnBattle
GameLogger.Log("BATTLE", $"Deadground.Show() ENTER: Visible={Visible}, GodotVisible={base.Visible}", LogLevel.Debug);
- var audioService = GetNode("/root/AudioService");
+ var audioService = GetNodeOrNull("/root/AudioService") as IAudioService;
audioService?.PlaySound("battle/deadground", SoundType.UI);
var bg = GetNodeOrNull("TransitionBg");
diff --git a/src/GDWingroundListener.cs b/src/GDWingroundListener.cs
index 0762e96..b168cdb 100644
--- a/src/GDWingroundListener.cs
+++ b/src/GDWingroundListener.cs
@@ -19,6 +19,7 @@ namespace Cthangover.Core.Battle
{
private readonly GodotObject _instance;
+ /// Registry key reported by the script's get_winground_listener_id(); deduplicates on rescan.
public string Id { get; }
///
@@ -31,6 +32,7 @@ namespace Cthangover.Core.Battle
_instance = instance;
}
+ /// Calls the script's on_winground_before_exp(ctx) if implemented; no-op otherwise.
public void OnBeforeExpAward(WingroundContext ctx)
{
if (!_instance.HasMethod("on_winground_before_exp"))
@@ -41,6 +43,7 @@ namespace Cthangover.Core.Battle
BattleGDScriptBridge.SyncWingroundBack(gdCtx, ctx);
}
+ /// Calls the script's on_winground_after_loot(ctx) if implemented; no-op otherwise.
public void OnAfterLootGenerated(WingroundContext ctx)
{
if (!_instance.HasMethod("on_winground_after_loot"))
@@ -51,6 +54,7 @@ namespace Cthangover.Core.Battle
BattleGDScriptBridge.SyncWingroundBack(gdCtx, ctx);
}
+ /// Calls the script's on_winground_before_show(ctx) if implemented; no-op otherwise.
public void OnBeforeShow(WingroundContext ctx)
{
if (!_instance.HasMethod("on_winground_before_show"))
diff --git a/src/IBattleSpeedControl.cs b/src/IBattleSpeedControl.cs
index 9b7a916..9fc3851 100644
--- a/src/IBattleSpeedControl.cs
+++ b/src/IBattleSpeedControl.cs
@@ -9,7 +9,7 @@ namespace Cthangover.Core.Battle
public interface IBattleSpeedControl
{
///
- /// Restores Engine.TimeScale to 1x without saving the preference.
+ /// Restores Godot.Engine.TimeScale to 1x without saving the preference.
/// Called when the battle ends so menus and the lobby run at normal speed
/// regardless of the player's in-battle speed choice.
///
diff --git a/src/ShowDamageBehaviour.cs b/src/ShowDamageBehaviour.cs
index ad65ce5..d739706 100644
--- a/src/ShowDamageBehaviour.cs
+++ b/src/ShowDamageBehaviour.cs
@@ -34,7 +34,7 @@ namespace Cthangover.TurnBattle
MouseFilter = MouseFilterEnum.Ignore;
_label = new Label();
- _label.SetAnchorsPreset(LayoutPreset.Center);
+ _label.SetAnchorsAndOffsetsPreset(LayoutPreset.Center);
_label.HorizontalAlignment = HorizontalAlignment.Center;
_label.VerticalAlignment = VerticalAlignment.Center;
_label.AddThemeFontSizeOverride("font_size", 28);
diff --git a/src/StatusEffectActionFactory.cs b/src/StatusEffectActionFactory.cs
index 39ab5c4..6fb665b 100644
--- a/src/StatusEffectActionFactory.cs
+++ b/src/StatusEffectActionFactory.cs
@@ -21,6 +21,7 @@ namespace Cthangover.Core.Factories.Impls
#region Singleton
private static readonly Lazy instance = new(() => new StatusEffectActionFactory());
+ /// Lazy singleton; construction performs the AppDomain scan.
public static StatusEffectActionFactory Instance => instance.Value;
public StatusEffectActionFactory()
@@ -28,12 +29,16 @@ namespace Cthangover.Core.Factories.Impls
actions = new Dictionary();
foreach (var action in Reflections.FindAndCreate())
{
- actions.Add(action.ID, action);
+ // First-wins: the AppDomain scan covers every loaded assembly and mod
+ // assemblies are never unloaded, so a hot-reload can discover the
+ // same implementation twice — keep the first instance (ItemActionFactory precedent).
+ if (!string.IsNullOrEmpty(action.ID) && !actions.ContainsKey(action.ID)) actions[action.ID] = action;
}
}
#endregion
+ /// Resolves the behaviour set registered under the effect's Actions id; unknown ids are logged and return null.
public IStatusActions Get(string id)
{
if (actions.TryGetValue(id, out var result))
diff --git a/src/StatusEffectInfoFactory.cs b/src/StatusEffectInfoFactory.cs
index cbc59ad..a7ee615 100644
--- a/src/StatusEffectInfoFactory.cs
+++ b/src/StatusEffectInfoFactory.cs
@@ -15,6 +15,7 @@ namespace Cthangover.Core.Factories.Impls
public class StatusEffectInfoFactory : FileFactory
{
private static readonly Lazy lazy = new(() => new StatusEffectInfoFactory());
+ /// Lazy singleton facade over the AppServices-owned factory.
public static StatusEffectInfoFactory Instance => lazy.Value;
}
}
diff --git a/src/WingroundBehaviour.cs b/src/WingroundBehaviour.cs
index 0b6594b..f07ef96 100644
--- a/src/WingroundBehaviour.cs
+++ b/src/WingroundBehaviour.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Linq;
using Cthangover.Core.Audio;
@@ -9,7 +9,7 @@ using Cthangover.Core.Items;
using Cthangover.Core.Mods;
using Cthangover.Core.Scenes;
using Cthangover.Core.Settings;
-using Cthangover.Core.UI.Base.Lists;
+using Cthangover.Mods.Interface.Base.Lists;
using Cthangover.Core.UI.Theming;
using Cthangover.Core.Characters;
using Cthangover.Core.Mods.Resolvers;
@@ -71,7 +71,7 @@ namespace Cthangover.TurnBattle
GameLogger.Log("BATTLE", $"Winground.Show() ENTER: Visible={Visible}, GodotVisible={base.Visible}, txtInfo={(txtInfo != null ? "found" : "NULL")}", LogLevel.Debug);
- var audioService = GetNode("/root/AudioService");
+ var audioService = GetNodeOrNull("/root/AudioService") as IAudioService;
audioService?.PlaySound("battle/winground", SoundType.UI);
var bg = GetNodeOrNull("TransitionBg");