Commit pending working-tree changes (local edits, line endings)

This commit is contained in:
cthangover committed 2026-10-21 12:32:00 +02:00
1 parent a9ed1b09c0
commit f4dc0c0b04
5 files changed
+215 -215

No files matched your search

+37 -37
View File
@@ -1,37 +1,37 @@
# Godot 4.x # Godot 4.x
.godot/ .godot/
.idea/ .idea/
sdk/ sdk/
bin/ bin/
obj/ obj/
#ai #ai
.omo/ .omo/
.opencode/ .opencode/
gen/ gen/
obj/ obj/
bin/ bin/
*.tres~ *.tres~
*.tscn~ *.tscn~
# GDScript LSP # GDScript LSP
.godot/ .godot/
# Export # Export
*.exe *.exe
*.dmg *.dmg
*.apk *.apk
*.aab *.aab
*.pck *.pck
*.zip *.zip
*.log *.log
# OS # OS
.DS_Store .DS_Store
Thumbs.db Thumbs.db
# Editor # Editor
*.import *.import
*.godot.uid *.godot.uid
+48 -48
View File
@@ -1,48 +1,48 @@
using Cthangover.Core.Settings; using Cthangover.Core.Settings;
namespace Cthangover.Core.Relationship namespace Cthangover.Core.Relationship
{ {
/// <summary> /// <summary>
/// Per-recruit behaviour plugin discovered via reflection from mod /// Per-recruit behaviour plugin discovered via reflection from mod
/// assemblies. Each implementation is registered into /// assemblies. Each implementation is registered into
/// <see cref="RecruitBehaviourRegistry"/> by /// <see cref="RecruitBehaviourRegistry"/> by
/// <see cref="Mods.ModAssemblyLoader.RegisterAssembly"/> and then /// <see cref="Mods.ModAssemblyLoader.RegisterAssembly"/> and then
/// invoked for <b>every</b> active recruit — not just those from the /// invoked for <b>every</b> active recruit — not just those from the
/// owning mod — during tick, configure, and removal lifecycle events. /// owning mod — during tick, configure, and removal lifecycle events.
/// ///
/// This cross-recruit dispatch pattern is intentional: a behaviour /// This cross-recruit dispatch pattern is intentional: a behaviour
/// might want to introspect all recruits (e.g. a "rivalry" system /// might want to introspect all recruits (e.g. a "rivalry" system
/// that checks for conflicting personalities), so the registry passes /// that checks for conflicting personalities), so the registry passes
/// the full recruit list to every behaviour rather than filtering by /// the full recruit list to every behaviour rather than filtering by
/// ownership. /// ownership.
/// </summary> /// </summary>
public interface IRecruitBehaviour public interface IRecruitBehaviour
{ {
/// <summary>Unique identifier for lookup via <c>RecruitBehaviourRegistry.Get</c>.</summary> /// <summary>Unique identifier for lookup via <c>RecruitBehaviourRegistry.Get</c>.</summary>
string Id { get; } string Id { get; }
/// <summary> /// <summary>
/// Called once when a recruit is added to the party /// Called once when a recruit is added to the party
/// (via <c>RecruitingData.Add</c>). Use for initial stat /// (via <c>RecruitingData.Add</c>). Use for initial stat
/// setup, buff application, or spawning companion nodes. /// setup, buff application, or spawning companion nodes.
/// </summary> /// </summary>
void ConfigureRecruit(Recruit recruit, RuntimeData runtime); void ConfigureRecruit(Recruit recruit, RuntimeData runtime);
/// <summary> /// <summary>
/// Called every game tick for each active recruit. The /// Called every game tick for each active recruit. The
/// <paramref name="currentTick"/> value is the global /// <paramref name="currentTick"/> value is the global
/// <c>RuntimeData.Time.Tick</c> counter — use it for /// <c>RuntimeData.Time.Tick</c> counter — use it for
/// cooldown-based or periodic logic rather than maintaining /// cooldown-based or periodic logic rather than maintaining
/// a private timer. /// a private timer.
/// </summary> /// </summary>
void OnTick(Recruit recruit, RuntimeData runtime, long currentTick); void OnTick(Recruit recruit, RuntimeData runtime, long currentTick);
/// <summary> /// <summary>
/// Called when a recruit is removed from the party /// Called when a recruit is removed from the party
/// (via <c>RecruitingData.Remove</c>). Clean up any /// (via <c>RecruitingData.Remove</c>). Clean up any
/// persisted state, buffs, or companion nodes. /// persisted state, buffs, or companion nodes.
/// </summary> /// </summary>
void OnRemove(Recruit recruit, RuntimeData runtime); void OnRemove(Recruit recruit, RuntimeData runtime);
} }
} }
+33 -33
View File
@@ -1,33 +1,33 @@
using Cthangover.Core.Characters; using Cthangover.Core.Characters;
using Cthangover.Core.Settings; using Cthangover.Core.Settings;
namespace Cthangover.Core.Relationship namespace Cthangover.Core.Relationship
{ {
/// <summary> /// <summary>
/// Gating predicate for the recruit system. Implementations are /// Gating predicate for the recruit system. Implementations are
/// auto-discovered from mod assemblies by /// auto-discovered from mod assemblies by
/// <see cref="RecruitBehaviourRegistry.RegisterAssembly"/> and /// <see cref="RecruitBehaviourRegistry.RegisterAssembly"/> and
/// evaluated conjunctively — <b>all</b> conditions must return /// evaluated conjunctively — <b>all</b> conditions must return
/// <c>true</c> for recruitment to succeed. /// <c>true</c> for recruitment to succeed.
/// ///
/// Conditions receive the full <see cref="Character"/> and /// Conditions receive the full <see cref="Character"/> and
/// <see cref="RuntimeData"/> so they can inspect character stats, /// <see cref="RuntimeData"/> so they can inspect character stats,
/// inventory, relationship flags, quest state, or any other /// inventory, relationship flags, quest state, or any other
/// runtime-accessible data when deciding whether a given enemy is /// runtime-accessible data when deciding whether a given enemy is
/// recruitable. /// recruitable.
/// </summary> /// </summary>
public interface IRecruitCondition public interface IRecruitCondition
{ {
/// <summary>Unique identifier for debugging and override lookups.</summary> /// <summary>Unique identifier for debugging and override lookups.</summary>
string Id { get; } string Id { get; }
/// <summary> /// <summary>
/// Returns <c>true</c> if the given <paramref name="enemy"/> /// Returns <c>true</c> if the given <paramref name="enemy"/>
/// can be recruited under current game state. Called from /// can be recruited under current game state. Called from
/// <c>RecruitBehaviourRegistry.CanRecruit</c> as part of a /// <c>RecruitBehaviourRegistry.CanRecruit</c> as part of a
/// conjunctive gate — a single <c>false</c> blocks recruitment. /// conjunctive gate — a single <c>false</c> blocks recruitment.
/// </summary> /// </summary>
bool CanRecruit(Character enemy, RuntimeData runtime); bool CanRecruit(Character enemy, RuntimeData runtime);
} }
} }
+35 -35
View File
@@ -1,35 +1,35 @@
using System; using System;
using Cthangover.Core.Utils; using Cthangover.Core.Utils;
namespace Cthangover.Core.Relationship namespace Cthangover.Core.Relationship
{ {
/// <summary> /// <summary>
/// Runtime model for a recruited party member. Stores the recruit's /// Runtime model for a recruited party member. Stores the recruit's
/// unique ID, the character template it was created from, and a /// unique ID, the character template it was created from, and a
/// modifiable <see cref="Utility.PropertyData"/> bag that behaviours /// modifiable <see cref="Utility.PropertyData"/> bag that behaviours
/// read and mutate during tick/configure events. /// read and mutate during tick/configure events.
/// ///
/// Marked <c>[Serializable]</c> so the recruiting roster survives /// Marked <c>[Serializable]</c> so the recruiting roster survives
/// save/load — <c>PropertyData</c> serialises its nested values /// save/load — <c>PropertyData</c> serialises its nested values
/// automatically. The <c>PROP_HEALTH</c> constant is the conventional /// automatically. The <c>PROP_HEALTH</c> constant is the conventional
/// key for health tracking; behaviours that need custom state /// key for health tracking; behaviours that need custom state
/// (e.g. morale, loyalty timers) add their own keys to the /// (e.g. morale, loyalty timers) add their own keys to the
/// <c>Properties</c> bag at configure time. /// <c>Properties</c> bag at configure time.
/// </summary> /// </summary>
[Serializable] [Serializable]
public class Recruit public class Recruit
{ {
/// <summary>Conventional key for the recruit's health value in the <c>Properties</c> bag.</summary> /// <summary>Conventional key for the recruit's health value in the <c>Properties</c> bag.</summary>
public const string PROP_HEALTH = "Health"; public const string PROP_HEALTH = "Health";
/// <summary>Unique instance ID (auto-generated via <c>RecruitingData.GenID</c>).</summary> /// <summary>Unique instance ID (auto-generated via <c>RecruitingData.GenID</c>).</summary>
public string ID { get; set; } public string ID { get; set; }
/// <summary>Character template ID this recruit was spawned from.</summary> /// <summary>Character template ID this recruit was spawned from.</summary>
public string CharacterID { get; set; } public string CharacterID { get; set; }
/// <summary>Mutable property bag — behaviours read/write custom state here.</summary> /// <summary>Mutable property bag — behaviours read/write custom state here.</summary>
public PropertyData Properties { get; set; } = new(); public PropertyData Properties { get; set; } = new();
} }
} }
+62 -62
View File
@@ -1,62 +1,62 @@
using Cthangover.Core.Scenes; using Cthangover.Core.Scenes;
using Cthangover.Core.UI.Event; using Cthangover.Core.UI.Event;
namespace Cthangover.Core.Relationship namespace Cthangover.Core.Relationship
{ {
/// <summary> /// <summary>
/// Lazy bridge between the scene timer system and the recruit /// Lazy bridge between the scene timer system and the recruit
/// behaviour tick loop. Implements <see cref="IOnTimeEvent"/> /// behaviour tick loop. Implements <see cref="IOnTimeEvent"/>
/// so it can be registered with <see cref="SceneEventController"/> /// so it can be registered with <see cref="SceneEventController"/>
/// via <c>AddTimerTickEventListener</c>. /// via <c>AddTimerTickEventListener</c>.
/// ///
/// Registration is lazy (<see cref="EnsureRegistered"/> is a no-op /// Registration is lazy (<see cref="EnsureRegistered"/> is a no-op
/// after the first call) because the recruit system should not /// after the first call) because the recruit system should not
/// force the timer subsystem to activate if no behaviours are /// force the timer subsystem to activate if no behaviours are
/// actually loaded. The first behaviour lifecycle method in /// actually loaded. The first behaviour lifecycle method in
/// <see cref="RecruitBehaviourRegistry"/> calls /// <see cref="RecruitBehaviourRegistry"/> calls
/// <c>EnsureRegistered</c>, which finds the scene's /// <c>EnsureRegistered</c>, which finds the scene's
/// <c>EventController</c> node and subscribes. /// <c>EventController</c> node and subscribes.
/// ///
/// <c>Priority = 0</c> so recruit ticks fire at a neutral point in /// <c>Priority = 0</c> so recruit ticks fire at a neutral point in
/// the timer event chain — before or after other listeners as /// the timer event chain — before or after other listeners as
/// determined by their relative priorities. /// determined by their relative priorities.
/// </summary> /// </summary>
public class RecruitTickController : IOnTimeEvent public class RecruitTickController : IOnTimeEvent
{ {
/// <summary>Neutral priority — doesn't preempt or follow any specific listener.</summary> /// <summary>Neutral priority — doesn't preempt or follow any specific listener.</summary>
public int Priority => 0; public int Priority => 0;
private bool _registered; private bool _registered;
/// <summary> /// <summary>
/// Finds the scene's <c>EventController</c> node and subscribes /// Finds the scene's <c>EventController</c> node and subscribes
/// this controller as a timer-tick listener. Idempotent — /// this controller as a timer-tick listener. Idempotent —
/// subsequent calls do nothing. /// subsequent calls do nothing.
/// </summary> /// </summary>
public void EnsureRegistered() public void EnsureRegistered()
{ {
if (_registered) if (_registered)
return; return;
var eventController = SceneContextNode.FindNode<SceneEventController>("EventController"); var eventController = SceneContextNode.FindNode<SceneEventController>("EventController");
if (eventController == null) if (eventController == null)
return; return;
eventController.AddTimerTickEventListener(this); eventController.AddTimerTickEventListener(this);
_registered = true; _registered = true;
} }
/// <summary> /// <summary>
/// Called by <see cref="SceneEventController"/> on every /// Called by <see cref="SceneEventController"/> on every
/// timer tick. Delegates to /// timer tick. Delegates to
/// <see cref="RecruitBehaviourRegistry.OnTick"/>. /// <see cref="RecruitBehaviourRegistry.OnTick"/>.
/// Ticks also fire during the launcher boot window, so the registry /// Ticks also fire during the launcher boot window, so the registry
/// side must not touch <c>GameData.Instance.Runtime</c> or mod data /// side must not touch <c>GameData.Instance.Runtime</c> or mod data
/// until <c>Boot.ModsReady</c> (see the boot-window contract on /// until <c>Boot.ModsReady</c> (see the boot-window contract on
/// <c>IOnTimeEvent</c>; core-side reference: TimeTickController). /// <c>IOnTimeEvent</c>; core-side reference: TimeTickController).
/// </summary> /// </summary>
public void OnTimerTick() public void OnTimerTick()
{ {
RecruitBehaviourRegistry.Instance.OnTick(); RecruitBehaviourRegistry.Instance.OnTick();
} }
} }
} }