diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..3030c1a
--- /dev/null
+++ b/.gitattributes
@@ -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
diff --git a/.gitignore b/.gitignore
index bb88804..c301ca8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,37 +1,37 @@
-# Godot 4.x
-.godot/
-.idea/
-sdk/
-bin/
-obj/
-
-#ai
-.omo/
-.opencode/
-
-gen/
-obj/
-bin/
-
-*.tres~
-*.tscn~
-
-# GDScript LSP
-.godot/
-
-# Export
-*.exe
-*.dmg
-*.apk
-*.aab
-*.pck
-*.zip
-*.log
-
-# OS
-.DS_Store
-Thumbs.db
-
-# Editor
-*.import
-*.godot.uid
+# Godot 4.x
+.godot/
+.idea/
+sdk/
+bin/
+obj/
+
+#ai
+.omo/
+.opencode/
+
+gen/
+obj/
+bin/
+
+*.tres~
+*.tscn~
+
+# GDScript LSP
+.godot/
+
+# Export
+*.exe
+*.dmg
+*.apk
+*.aab
+*.pck
+*.zip
+*.log
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Editor
+*.import
+*.godot.uid
diff --git a/characters/actions/physics_attack.json b/characters/actions/physics_attack.json
index 536c98a..5476359 100644
--- a/characters/actions/physics_attack.json
+++ b/characters/actions/physics_attack.json
@@ -1,17 +1,17 @@
-{
- "Kind": "action",
- "Items": [
- {
- "Id": "physics/attack",
- "Name": "characters/action/physics_attack/name",
- "Description": "characters/action/physics_attack/description",
- "Image": "characters/actions/physics_attack",
- "Type": "ToEnemy",
- "Values": [
- "RequiredPoint=1",
- "Attack=1.0",
- "AttackType=Physics"
- ]
- }
- ]
+{
+ "Kind": "action",
+ "Items": [
+ {
+ "Id": "physics/attack",
+ "Name": "characters/action/physics_attack/name",
+ "Description": "characters/action/physics_attack/description",
+ "Image": "characters/actions/physics_attack",
+ "Type": "ToEnemy",
+ "Values": [
+ "RequiredPoint=1",
+ "Attack=1.0",
+ "AttackType=Physics"
+ ]
+ }
+ ]
}
diff --git a/characters/actions/physics_defence.json b/characters/actions/physics_defence.json
index 52163ea..1c59f98 100644
--- a/characters/actions/physics_defence.json
+++ b/characters/actions/physics_defence.json
@@ -1,16 +1,16 @@
-{
- "Kind": "action",
- "Items": [
- {
- "Id": "physics/defence",
- "Name": "characters/action/defence/name",
- "Description": "characters/action/defence/description",
- "Image": "characters/actions/physics_defence",
- "Type": "ToAlias",
- "Values": [
- "RequiredPoint=1",
- "Defence=4"
- ]
- }
- ]
+{
+ "Kind": "action",
+ "Items": [
+ {
+ "Id": "physics/defence",
+ "Name": "characters/action/defence/name",
+ "Description": "characters/action/defence/description",
+ "Image": "characters/actions/physics_defence",
+ "Type": "ToAlias",
+ "Values": [
+ "RequiredPoint=1",
+ "Defence=4"
+ ]
+ }
+ ]
}
diff --git a/characters/actions/physics_stun.json b/characters/actions/physics_stun.json
index 0cc290f..a7fc388 100644
--- a/characters/actions/physics_stun.json
+++ b/characters/actions/physics_stun.json
@@ -1,17 +1,17 @@
-{
- "Kind": "action",
- "Items": [
- {
- "Id": "physics/stun",
- "Name": "characters/action/stun/name",
- "Description": "characters/action/stun/description",
- "Image": "characters/actions/physics_stun",
- "Type": "ToEnemy",
- "Values": [
- "RequiredPoint=2",
- "AttackType=Physics",
- "Turn=3"
- ]
- }
- ]
+{
+ "Kind": "action",
+ "Items": [
+ {
+ "Id": "physics/stun",
+ "Name": "characters/action/stun/name",
+ "Description": "characters/action/stun/description",
+ "Image": "characters/actions/physics_stun",
+ "Type": "ToEnemy",
+ "Values": [
+ "RequiredPoint=2",
+ "AttackType=Physics",
+ "Turn=3"
+ ]
+ }
+ ]
}
diff --git a/src/Characters/ActionCharacterActiveExtension.cs b/src/Characters/ActionCharacterActiveExtension.cs
index 42a832e..d35e5e1 100644
--- a/src/Characters/ActionCharacterActiveExtension.cs
+++ b/src/Characters/ActionCharacterActiveExtension.cs
@@ -1,63 +1,63 @@
-namespace Cthangover.Core.Characters
-{
- ///
- /// Cooldown helpers for cards.
- /// Active cards are one-shot abilities usable outside battle (from the
- /// character panel). The last-use timestamp is stored in
- /// under
- /// 'LastUsed' - the cooldown duration
- /// (seconds) under 'ActiveCooldown'
- /// Absence of LastUsed means the card has never been activated.
- ///
- public static class ActionCharacterActiveExtension
- {
- ///
- /// Returns true when the card is
- /// and its cooldown has expired (or it was never used).
- ///
- public static bool IsReadyToUse(this ActionCharacter action, long currentTime)
- {
- if (action.Type != ActionCharacterType.Active)
- return false;
-
- var lastUsed = action.Properties.GetLong("LastUsed", 0);
- var cooldown = action.GetInt("ActiveCooldown", 0);
-
- if (lastUsed == 0)
- return true;
- if (cooldown <= 0)
- return false;
-
- return currentTime - lastUsed >= cooldown;
- }
-
- ///
- /// Seconds remaining until the cooldown expires. Returns 0 when
- /// ready, never used, or not an Active card.
- ///
- public static long GetRemainingCooldown(this ActionCharacter action, long currentTime)
- {
- if (action.Type != ActionCharacterType.Active)
- return 0;
-
- var lastUsed = action.Properties.GetLong("LastUsed", 0);
- var cooldown = action.GetInt("ActiveCooldown", 0);
-
- if (lastUsed == 0 || cooldown <= 0)
- return 0;
-
- var elapsed = currentTime - lastUsed;
- return elapsed >= cooldown ? 0 : cooldown - elapsed;
- }
-
- ///
- /// Records the activation timestamp. Has no effect on non-Active cards.
- ///
- public static void MarkUsed(this ActionCharacter action, long timestamp)
- {
- if (action.Type != ActionCharacterType.Active)
- return;
- action.Properties.SetLong("LastUsed", timestamp);
- }
- }
-}
+namespace Cthangover.Core.Characters
+{
+ ///
+ /// Cooldown helpers for cards.
+ /// Active cards are one-shot abilities usable outside battle (from the
+ /// character panel). The last-use timestamp is stored in
+ /// under
+ /// 'LastUsed' - the cooldown duration
+ /// (seconds) under 'ActiveCooldown'
+ /// Absence of LastUsed means the card has never been activated.
+ ///
+ public static class ActionCharacterActiveExtension
+ {
+ ///
+ /// Returns true when the card is
+ /// and its cooldown has expired (or it was never used).
+ ///
+ public static bool IsReadyToUse(this ActionCharacter action, long currentTime)
+ {
+ if (action.Type != ActionCharacterType.Active)
+ return false;
+
+ var lastUsed = action.Properties.GetLong("LastUsed", 0);
+ var cooldown = action.GetInt("ActiveCooldown", 0);
+
+ if (lastUsed == 0)
+ return true;
+ if (cooldown <= 0)
+ return false;
+
+ return currentTime - lastUsed >= cooldown;
+ }
+
+ ///
+ /// Seconds remaining until the cooldown expires. Returns 0 when
+ /// ready, never used, or not an Active card.
+ ///
+ public static long GetRemainingCooldown(this ActionCharacter action, long currentTime)
+ {
+ if (action.Type != ActionCharacterType.Active)
+ return 0;
+
+ var lastUsed = action.Properties.GetLong("LastUsed", 0);
+ var cooldown = action.GetInt("ActiveCooldown", 0);
+
+ if (lastUsed == 0 || cooldown <= 0)
+ return 0;
+
+ var elapsed = currentTime - lastUsed;
+ return elapsed >= cooldown ? 0 : cooldown - elapsed;
+ }
+
+ ///
+ /// Records the activation timestamp. Has no effect on non-Active cards.
+ ///
+ public static void MarkUsed(this ActionCharacter action, long timestamp)
+ {
+ if (action.Type != ActionCharacterType.Active)
+ return;
+ action.Properties.SetLong("LastUsed", timestamp);
+ }
+ }
+}
diff --git a/src/Characters/ActionCharacterInfo.cs b/src/Characters/ActionCharacterInfo.cs
index a2a7e56..874e018 100644
--- a/src/Characters/ActionCharacterInfo.cs
+++ b/src/Characters/ActionCharacterInfo.cs
@@ -1,56 +1,56 @@
-using System;
-using System.Collections.Generic;
-using System.Text.Json.Serialization;
-using Cthangover.Core.Factories;
-
-namespace Cthangover.Core.Characters
-{
- ///
- /// JSON-serializable descriptor for an ActionCharacter. The Values list is a
- /// flat string array carrying the PropertyData pairs — serialized as a list
- /// rather than a dictionary to maintain JSON readability and avoid key
- /// escaping issues. Type uses a string enum converter so the JSON reads
- /// "ToSelf"/"ToAlias"/"ToEnemy" instead of integers. The Image field is a
- /// resource path string, resolved to Texture2D by the factory at load time.
- ///
- [Serializable]
- [EntityKind("action")]
- public class ActionCharacterInfo : IIdentifiable
- {
- ///
- /// Unique action identifier, serialized as "Id" in JSON.
- ///
- [JsonPropertyName("Id")]
- public string ID { get; set; }
- ///
- /// Display name for the battle action menu.
- ///
- public string Name { get; set; }
- ///
- /// Description text for the battle action tooltip.
- ///
- public string Description { get; set; }
- ///
- /// Targeting category, serialized as string (e.g. "ToSelf",
- /// "ToAlias", "ToEnemy") via
- /// for human-readable JSON.
- ///
- [JsonConverter(typeof(JsonStringEnumConverter))]
- public ActionCharacterType Type { get; set; }
- ///
- /// Resource path string for the action icon. Resolved to
- /// Texture2D by at load
- /// time.
- ///
- public string Image { get; set; }
- ///
- /// Flat list of alternating key-value pairs that populate the
- /// bag at load time.
- /// Index 0 = first key, index 1 = first value, index 2 = second
- /// key, etc. Stored as a list rather than a dictionary to
- /// simplify JSON authoring and avoid key-escaping issues.
- ///
- public List Values { get; set; }
- }
-
-}
+using System;
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+using Cthangover.Core.Factories;
+
+namespace Cthangover.Core.Characters
+{
+ ///
+ /// JSON-serializable descriptor for an ActionCharacter. The Values list is a
+ /// flat string array carrying the PropertyData pairs — serialized as a list
+ /// rather than a dictionary to maintain JSON readability and avoid key
+ /// escaping issues. Type uses a string enum converter so the JSON reads
+ /// "ToSelf"/"ToAlias"/"ToEnemy" instead of integers. The Image field is a
+ /// resource path string, resolved to Texture2D by the factory at load time.
+ ///
+ [Serializable]
+ [EntityKind("action")]
+ public class ActionCharacterInfo : IIdentifiable
+ {
+ ///
+ /// Unique action identifier, serialized as "Id" in JSON.
+ ///
+ [JsonPropertyName("Id")]
+ public string ID { get; set; }
+ ///
+ /// Display name for the battle action menu.
+ ///
+ public string Name { get; set; }
+ ///
+ /// Description text for the battle action tooltip.
+ ///
+ public string Description { get; set; }
+ ///
+ /// Targeting category, serialized as string (e.g. "ToSelf",
+ /// "ToAlias", "ToEnemy") via
+ /// for human-readable JSON.
+ ///
+ [JsonConverter(typeof(JsonStringEnumConverter))]
+ public ActionCharacterType Type { get; set; }
+ ///
+ /// Resource path string for the action icon. Resolved to
+ /// Texture2D by at load
+ /// time.
+ ///
+ public string Image { get; set; }
+ ///
+ /// Flat list of alternating key-value pairs that populate the
+ /// bag at load time.
+ /// Index 0 = first key, index 1 = first value, index 2 = second
+ /// key, etc. Stored as a list rather than a dictionary to
+ /// simplify JSON authoring and avoid key-escaping issues.
+ ///
+ public List Values { get; set; }
+ }
+
+}
diff --git a/src/Characters/CharacterInfo.cs b/src/Characters/CharacterInfo.cs
index 811cab8..f12b077 100644
--- a/src/Characters/CharacterInfo.cs
+++ b/src/Characters/CharacterInfo.cs
@@ -1,93 +1,93 @@
-using System;
-using System.Collections.Generic;
-using System.Text.Json.Serialization;
-using Cthangover.Core.Factories;
-
-namespace Cthangover.Core.Characters
-{
- ///
- /// JSON-serializable character descriptor loaded from data files.
- /// Only the four core battle stats (Health, Defence, Attack, Point)
- /// are hardcoded — they map directly to fields
- /// in . All other properties
- /// (any mod-defined extras) are captured by
- /// and flowed into at
- /// factory time, so mods can add arbitrary stat keys without
- /// changing the DTO schema.
- ///
- [Serializable]
- [EntityKind("character")]
- public class CharacterInfo : IIdentifiable
- {
- ///
- /// Unique character ID, serialized as "Id" in JSON. Serves as the
- /// factory lookup key and save-data identifier.
- ///
- [JsonPropertyName("Id")]
- public string ID { get; set; }
- ///
- /// AI behaviour class name for this character in battle. Resolved
- /// externally — null means no AI override.
- ///
- public string Behaviour { get; set; }
- ///
- /// Starting level for this character when recruited.
- ///
- public int Level { get; set; }
- ///
- /// Resource path string for the full-body portrait. Resolved to a
- /// Texture2D by at load time.
- ///
- public string Image { get; set; }
- ///
- /// Display name, localized via TranslationServer when shown to the
- /// player.
- ///
- public string Name { get; set; }
- ///
- /// Starting health points (used as .BaseValue).
- ///
- public int Health { get; set; }
- ///
- /// Starting defence value.
- ///
- public int Defence { get; set; }
- ///
- /// Starting action points (resource pool for abilities).
- ///
- public int Point { get; set; }
- ///
- /// Starting attack power.
- ///
- public int Attack { get; set; }
- ///
- /// Starting experience points.
- ///
- public int Exp { get; set; }
- ///
- /// Recruitment chance percentage (0–100) after defeating this
- /// character in battle.
- ///
- public int RecruitmentChance { get; set; }
- ///
- /// Action ID strings, each referencing an
- /// entry in the action data file.
- /// Stored as a JSON array for clean mod-patching with
- /// $add/$remove/$set suffix operations.
- ///
- public List Actions { get; set; }
- ///
- /// Loot drop table for this character when defeated as an enemy.
- ///
- public List Loot { get; set; }
- ///
- /// Captures any JSON keys not matched by the hardcoded properties
- /// above — including former DTO fields like any mod-defined extras.
- /// These are imported into
- /// by .
- ///
- [JsonExtensionData]
- public Dictionary ExtraProperties { get; set; }
- }
-
-}
+using System;
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+using Cthangover.Core.Factories;
+
+namespace Cthangover.Core.Characters
+{
+ ///
+ /// JSON-serializable character descriptor loaded from data files.
+ /// Only the four core battle stats (Health, Defence, Attack, Point)
+ /// are hardcoded — they map directly to fields
+ /// in . All other properties
+ /// (any mod-defined extras) are captured by
+ /// and flowed into at
+ /// factory time, so mods can add arbitrary stat keys without
+ /// changing the DTO schema.
+ ///
+ [Serializable]
+ [EntityKind("character")]
+ public class CharacterInfo : IIdentifiable
+ {
+ ///
+ /// Unique character ID, serialized as "Id" in JSON. Serves as the
+ /// factory lookup key and save-data identifier.
+ ///
+ [JsonPropertyName("Id")]
+ public string ID { get; set; }
+ ///
+ /// AI behaviour class name for this character in battle. Resolved
+ /// externally — null means no AI override.
+ ///
+ public string Behaviour { get; set; }
+ ///
+ /// Starting level for this character when recruited.
+ ///
+ public int Level { get; set; }
+ ///
+ /// Resource path string for the full-body portrait. Resolved to a
+ /// Texture2D by at load time.
+ ///
+ public string Image { get; set; }
+ ///
+ /// Display name, localized via TranslationServer when shown to the
+ /// player.
+ ///
+ public string Name { get; set; }
+ ///
+ /// Starting health points (used as .BaseValue).
+ ///
+ public int Health { get; set; }
+ ///
+ /// Starting defence value.
+ ///
+ public int Defence { get; set; }
+ ///
+ /// Starting action points (resource pool for abilities).
+ ///
+ public int Point { get; set; }
+ ///
+ /// Starting attack power.
+ ///
+ public int Attack { get; set; }
+ ///
+ /// Starting experience points.
+ ///
+ public int Exp { get; set; }
+ ///
+ /// Recruitment chance percentage (0–100) after defeating this
+ /// character in battle.
+ ///
+ public int RecruitmentChance { get; set; }
+ ///
+ /// Action ID strings, each referencing an
+ /// entry in the action data file.
+ /// Stored as a JSON array for clean mod-patching with
+ /// $add/$remove/$set suffix operations.
+ ///
+ public List Actions { get; set; }
+ ///
+ /// Loot drop table for this character when defeated as an enemy.
+ ///
+ public List Loot { get; set; }
+ ///
+ /// Captures any JSON keys not matched by the hardcoded properties
+ /// above — including former DTO fields like any mod-defined extras.
+ /// These are imported into
+ /// by .
+ ///
+ [JsonExtensionData]
+ public Dictionary ExtraProperties { get; set; }
+ }
+
+}
diff --git a/src/Characters/CharacterPassiveExtension.cs b/src/Characters/CharacterPassiveExtension.cs
index 6713294..955436d 100644
--- a/src/Characters/CharacterPassiveExtension.cs
+++ b/src/Characters/CharacterPassiveExtension.cs
@@ -1,25 +1,25 @@
-using System.Linq;
-
-namespace Cthangover.Core.Characters
-{
- ///
- /// Extension for querying passive effects on a .
- /// Passive actions are entries with
- /// whose
- /// declare effect flags as boolean keys (e.g. "IgnoreArmor"). The key names
- /// are chosen by the mod author — the core imposes no registry or constants.
- ///
- public static class CharacterPassiveExtension
- {
- ///
- /// Returns true if the character has at least one Passive action
- /// whose contains
- /// with a true boolean value.
- ///
- public static bool HasPassiveEffect(this Character character, string key)
- {
- return character?.Actions?.Any(a =>
- a.Type == ActionCharacterType.Passive && a.GetBool(key)) ?? false;
- }
- }
-}
+using System.Linq;
+
+namespace Cthangover.Core.Characters
+{
+ ///
+ /// Extension for querying passive effects on a .
+ /// Passive actions are entries with
+ /// whose
+ /// declare effect flags as boolean keys (e.g. "IgnoreArmor"). The key names
+ /// are chosen by the mod author — the core imposes no registry or constants.
+ ///
+ public static class CharacterPassiveExtension
+ {
+ ///
+ /// Returns true if the character has at least one Passive action
+ /// whose contains
+ /// with a true boolean value.
+ ///
+ public static bool HasPassiveEffect(this Character character, string key)
+ {
+ return character?.Actions?.Any(a =>
+ a.Type == ActionCharacterType.Passive && a.GetBool(key)) ?? false;
+ }
+ }
+}
diff --git a/src/Factories/AvatarFactory.cs b/src/Factories/AvatarFactory.cs
index f434431..8cfb390 100644
--- a/src/Factories/AvatarFactory.cs
+++ b/src/Factories/AvatarFactory.cs
@@ -1,30 +1,30 @@
-using Cthangover.Core.Factories;
-using Cthangover.Core.Utils;
-using Godot;
-
-namespace Cthangover.Core.Factories.Impls
-{
- ///
- /// Thin Texture2DFactory for character portrait avatars. The
- /// separate factory (as opposed to reusing UITextureFactory)
- /// isolates avatar images into their own mod group with a dedicated
- /// cache budget, so frequently-swapped dialog portraits don't evict
- /// other UI textures under memory pressure.
- ///
- /// The factory lives in the characters engine mod; its singleton is also
- /// published to the kernel through by
- /// CharactersModInitializer, so core-side consumers resolve
- /// avatars without referencing this assembly.
- ///
- public class AvatarFactory : Texture2DFactory, IAvatarProvider
- {
- internal AvatarFactory() : base("avatars", 64) { }
-
- /// Process-wide singleton for this mod assembly; the public API for avatar lookups.
- public static AvatarFactory Instance { get; } = new AvatarFactory();
-
- /// Mod asset group scanned by this factory — files live under mods/*/avatars.
- public override string GroupName => "avatars";
-
- }
-}
+using Cthangover.Core.Factories;
+using Cthangover.Core.Utils;
+using Godot;
+
+namespace Cthangover.Core.Factories.Impls
+{
+ ///
+ /// Thin Texture2DFactory for character portrait avatars. The
+ /// separate factory (as opposed to reusing UITextureFactory)
+ /// isolates avatar images into their own mod group with a dedicated
+ /// cache budget, so frequently-swapped dialog portraits don't evict
+ /// other UI textures under memory pressure.
+ ///
+ /// The factory lives in the characters engine mod; its singleton is also
+ /// published to the kernel through by
+ /// CharactersModInitializer, so core-side consumers resolve
+ /// avatars without referencing this assembly.
+ ///
+ public class AvatarFactory : Texture2DFactory, IAvatarProvider
+ {
+ internal AvatarFactory() : base("avatars", 64) { }
+
+ /// Process-wide singleton for this mod assembly; the public API for avatar lookups.
+ public static AvatarFactory Instance { get; } = new AvatarFactory();
+
+ /// Mod asset group scanned by this factory — files live under mods/*/avatars.
+ public override string GroupName => "avatars";
+
+ }
+}