new battle gui

This commit is contained in:
cthangover committed 2026-09-28 10:48:04 +03:00
1 parent b5ad139d69
commit 8fbac46378
59 files changed
+2897 -37

No files matched your search

+58
View File
@@ -0,0 +1,58 @@
using Cthangover.Core.Characters;
namespace Cthangover.Core.Cards.StatusEffect
{
/// <summary>
/// Behaviour hooks for a status effect type. Each hook receives the
/// owning character so a single IStatusActions instance can be
/// shared across multiple characters. OnDealDamage / OnTakeDamage
/// use ref int to let effects amplify or reduce damage before it
/// is applied. ModifyAttributes is a passive stat modifier called
/// independently of turn phases. OnFinalAction fires once when the
/// effect expires. All hooks have empty default semantics — effects
/// override only the hooks they care about.
/// </summary>
public interface IStatusActions
{
/// <summary>Factory key matching the <c>Actions</c> field in
/// <see cref="StatusEffectInfo"/>, e.g. <c>"basic/stun"</c>.</summary>
string ID { get; }
/// <summary>Fires when the owning character's turn begins. Use for
/// start-of-turn damage-over-time or regeneration effects.</summary>
void OnTurnStart(Character target);
/// <summary>Fires when the owning character's turn ends. Mirrors
/// <see cref="OnTurnStart"/> for symmetry; useful for effects that
/// activate after the character acts.</summary>
void OnTurnEnd(Character target);
/// <summary>Called before the owning character deals damage to
/// <paramref name="target"/>. Modify <paramref name="damage"/> via
/// <see langword="ref"/> to amplify or reduce outgoing damage —
/// all active effects stack multiplicatively through the queue's
/// iteration.</summary>
void OnDealDamage(Character source, Character target, ref int damage);
/// <summary>Called before the owning character receives damage from
/// <paramref name="source"/>. Modify <paramref name="damage"/> via
/// <see langword="ref"/> to add damage reduction or vulnerability.
/// Fires after <see cref="OnDealDamage"/> in the damage
/// pipeline.</summary>
void OnTakeDamage(Character target, Character source, ref int damage);
/// <summary>Fires immediately when the effect is first placed on a
/// character. Use for one-shot stat boosts, visual feedback, or
/// initialisation logic.</summary>
void OnApply(Character target);
/// <summary>Fires when the effect is manually removed (e.g. via a
/// cleanse card). Not called on natural expiration — use
/// <see cref="OnFinalAction"/> for that path.</summary>
void OnRemove(Character target);
/// <summary>Fires once when the effect expires naturally (timer reaches
/// zero). Use for end-of-effect triggers like explosion damage or
/// stat restoration. Not called on manual removal.</summary>
void OnFinalAction(Character target);
/// <summary>Passive stat modifier invoked independently of turn phases.
/// Receives a reference to the character's attribute container;
/// implementors can add flat or percentage bonuses that other
/// effects may also contribute to.</summary>
void ModifyAttributes(CharacterAttributes attributes);
}
}
+49
View File
@@ -0,0 +1,49 @@
using Godot;
namespace Cthangover.Core.Cards.StatusEffect
{
/// <summary>
/// Status effect data contract. Separates identity (ID, Name, Icon)
/// from mutable state (Duration, RemainingTurns) and behaviour
/// (Actions — an IStatusActions hook set). Copy() is required so the
/// StatusEffectQueue can clone effects when characters are duplicated.
/// </summary>
public interface IStatusEffect
{
/// <summary>Unique mod identifier, e.g. <c>"basic/stun"</c>. Used by factories
/// and the queue to look up and remove effects by key.</summary>
string ID { get; }
/// <summary>Display name shown in the battle UI.</summary>
string Name { get; }
/// <summary>Flavour text explaining the effect to the player.</summary>
string Description { get; }
/// <summary>High-level category (<c>Buff</c>, <c>Debuff</c>, <c>Stun</c>) used
/// for UI tinting and quick type checks like
/// <see cref="StatusEffectQueue.HasStun"/>.</summary>
StatusEffectType EffectType { get; }
/// <summary>Base duration in turns. Set during effect creation; may be
/// overridden by the caller via <see cref="StatusEffectQueue.Add"/>.
/// A value of 0 or negative means the effect never expires
/// naturally.</summary>
int Duration { get; set; }
/// <summary>Turns remaining before the effect expires. Decremented each
/// turn boundary by the queue. Independent of <c>Duration</c> so
/// that the original value is preserved for display or re-apply
/// logic.</summary>
int RemainingTurns { get; set; }
/// <summary>Icon texture loaded from the <c>"characters"</c> mod group,
/// displayed beside the effect name in the HUD.</summary>
Texture2D Icon { get; }
/// <summary>Behaviour hooks that fire during turn phases and on damage
/// events. Resolved by <c>StatusEffectActionFactory</c> from the
/// <see cref="StatusEffectInfo.Actions"/> string ID.</summary>
IStatusActions Actions { get; }
/// <summary>Creates a shallow clone. <c>Actions</c> is a shared reference
/// (stateless singleton) so it is not deep-copied. Used by
/// <see cref="StatusEffectQueue.Copy"/> when duplicating a
/// character.</summary>
IStatusEffect Copy();
}
}
@@ -0,0 +1,49 @@
using Cthangover.Core.Characters;
namespace Cthangover.Core.Cards.StatusEffect.Impls
{
/// <summary>
/// Stun behaviour set — intentionally all no-ops. The stun condition
/// is checked separately via StatusEffectQueue.HasStun() rather than
/// through action hooks, so the Actions implementation is empty.
/// Exists as a concrete type so the factory can resolve "basic/stun"
/// to a valid instance.
/// </summary>
public class StunStatusEffect : IStatusActions
{
/// <inheritdoc cref="IStatusActions.ID"/>
public string ID => "basic/stun";
/// <inheritdoc cref="IStatusActions.OnTurnStart"/>
public void OnTurnStart(Character target)
{ }
/// <inheritdoc cref="IStatusActions.OnTurnEnd"/>
public void OnTurnEnd(Character target)
{ }
/// <inheritdoc cref="IStatusActions.OnDealDamage"/>
public void OnDealDamage(Character source, Character target, ref int damage)
{ }
/// <inheritdoc cref="IStatusActions.OnTakeDamage"/>
public void OnTakeDamage(Character target, Character source, ref int damage)
{ }
/// <inheritdoc cref="IStatusActions.OnApply"/>
public void OnApply(Character target)
{ }
/// <inheritdoc cref="IStatusActions.OnRemove"/>
public void OnRemove(Character target)
{ }
/// <inheritdoc cref="IStatusActions.OnFinalAction"/>
public void OnFinalAction(Character target)
{ }
/// <inheritdoc cref="IStatusActions.ModifyAttributes"/>
public void ModifyAttributes(CharacterAttributes attributes)
{ }
}
}
+45
View File
@@ -0,0 +1,45 @@
using System.Text.Json.Serialization;
using Cthangover.Core.Factories;
namespace Cthangover.Core.Cards.StatusEffect
{
/// <summary>
/// JSON-serialisable status effect definition loaded from mod files.
/// The Actions field is an ID string resolved by StatusEffectActionFactory
/// into an IStatusActions instance at runtime. Type uses
/// JsonStringEnumConverter for human-readable config files.
/// Implements IIdentifiable so it works with PrefabFactory caching.
/// </summary>
[System.Serializable]
public class StatusEffectInfo : IIdentifiable
{
/// <summary>Unique effect identifier used as the factory key.
/// Serialised as <c>"Id"</c> in JSON for consistency with the
/// modding schema.</summary>
[JsonPropertyName("Id")]
public string ID { get; set; }
/// <summary>Display name for the effect, shown in tooltips and the
/// character status panel.</summary>
public string Name { get; set; }
/// <summary>Human-readable description explaining what the effect
/// does.</summary>
public string Description { get; set; }
/// <summary>Category of the effect. Serialised as a string
/// (<c>"Buff"</c>, <c>"Debuff"</c>, <c>"Stun"</c>) via
/// <c>JsonStringEnumConverter</c> for readable mod configs.</summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public StatusEffectType Type { get; set; }
/// <summary>Base duration in turns before the effect naturally
/// expires.</summary>
public int Duration { get; set; }
/// <summary>Factory ID string for the behaviour set, resolved by
/// <c>StatusEffectActionFactory</c> into an
/// <see cref="IStatusActions"/> instance at runtime.</summary>
public string Actions { get; set; }
/// <summary>Filename of the icon texture within the
/// <c>"characters"</c> mod group, loaded by
/// <see cref="StatusEffectItem"/> during construction.</summary>
public string Icon { get; set; }
}
}
+78
View File
@@ -0,0 +1,78 @@
using Cthangover.Core.Factories.Impls;
using Cthangover.Core.Utils;
using Godot;
namespace Cthangover.Core.Cards.StatusEffect
{
/// <summary>
/// Runtime instance of a status effect. Constructed from
/// StatusEffectInfo with the Actions behaviour resolved via
/// StatusEffectActionFactory. Icon is loaded from the "characters"
/// mod group via TextureUtils — effects share the same icon pool
/// as characters. Copy() is a shallow clone of primitives and
/// references (Actions is a shared singleton, not cloned) suitable
/// for StatusEffectQueue duplication.
/// </summary>
public class StatusEffectItem : IStatusEffect
{
/// <inheritdoc cref="IStatusEffect.ID"/>
public string ID { get; private set; }
/// <inheritdoc cref="IStatusEffect.Name"/>
public string Name { get; private set; }
/// <inheritdoc cref="IStatusEffect.Description"/>
public string Description { get; private set; }
/// <inheritdoc cref="IStatusEffect.Duration"/>
public int Duration { get; set; }
/// <inheritdoc cref="IStatusEffect.RemainingTurns"/>
public int RemainingTurns { get; set; }
/// <inheritdoc cref="IStatusEffect.EffectType"/>
public StatusEffectType EffectType { get; private set; }
/// <inheritdoc cref="IStatusEffect.Icon"/>
public Texture2D Icon { get; private set; }
/// <inheritdoc cref="IStatusEffect.Actions"/>
public IStatusActions Actions { get; private set; }
/// <summary>Constructs a runtime effect from its JSON-serialisable
/// definition. Resolves the icon via
/// <c>TextureUtils.LoadFromModGroup</c> and the action set via
/// <c>StatusEffectActionFactory</c>.</summary>
/// <param name="info">Deserialised effect data from a mod
/// config.</param>
public StatusEffectItem(StatusEffectInfo info)
{
ID = info.ID;
Name = info.Name;
Description = info.Description;
Duration = info.Duration;
EffectType = info.Type;
Icon = TextureUtils.LoadFromModGroup("characters", info.Icon);
Actions = StatusEffectActionFactory.Instance.Get(info.Actions);
}
/// <summary>Parameterless constructor for cases where properties are
/// set manually (e.g. by <see cref="Copy"/> or deserialisation
/// paths).</summary>
public StatusEffectItem() { }
/// <summary>Shallow clone that copies primitives and reuses the
/// shared <c>Actions</c> reference. Used when the queue
/// duplicates effects during character cloning.</summary>
/// <returns>A new <see cref="StatusEffectItem"/> with identical
/// property values.</returns>
public IStatusEffect Copy()
{
return new StatusEffectItem
{
ID = ID,
Name = Name,
Description = Description,
Duration = Duration,
RemainingTurns = RemainingTurns,
EffectType = EffectType,
Icon = Icon,
Actions = Actions,
};
}
}
}
+223
View File
@@ -0,0 +1,223 @@
using System.Collections.Generic;
using System.Linq;
using Cthangover.Core.Characters;
using Cthangover.Core.Utils;
using Godot;
namespace Cthangover.Core.Cards.StatusEffect
{
/// <summary>
/// Per-character status-effect pipeline. Manages a list of active
/// effects, advancing their timers on turn boundaries and removing
/// expired ones after firing OnFinalAction. Add resolves the effect
/// info via the factory and calls OnApply immediately. OnTurnStart
/// iterates backwards to safely remove expired effects during
/// traversal. OnDealDamage / OnTakeDamage pipe through every active
/// effect via ref parameters, enabling multiplicative stacking.
/// ActiveStatusEffects returns a defensive copy so external code
/// cannot mutate the internal list. Copy creates a deep clone for
/// character duplication, re-firing OnApply on the new owner.
/// </summary>
public class StatusEffectQueue
{
private readonly List<IStatusEffect> activeStatusEffectData;
private readonly Character owner;
/// <summary>Defensive copy of the internal effect list. Returns
/// a new <c>List</c> each access so external code cannot mutate
/// the queue's backing store. Safe for enumeration.</summary>
public List<IStatusEffect> ActiveStatusEffects => activeStatusEffectData.ToList();
/// <summary>Initialises an empty queue bound to <paramref name="owner"/>.
/// The owner reference is used by all action-hook calls to pass
/// the affected character.</summary>
/// <param name="owner">The character who carries these
/// effects.</param>
public StatusEffectQueue(Character owner)
{
this.activeStatusEffectData = new List<IStatusEffect>();
this.owner = owner;
}
/// <summary>Checks whether any active effect has
/// <see cref="StatusEffectType.Stun"/>. Called by the battle
/// engine to determine if the owner should skip their turn.</summary>
/// <returns><see langword="true"/> if at least one Stun-type effect
/// is active.</returns>
public bool HasStun()
{
foreach (var statusEffect in activeStatusEffectData)
if (statusEffect.EffectType == StatusEffectType.Stun)
return true;
return false;
}
/// <summary>Looks up <paramref name="statusEffectId"/> in the factory,
/// constructs a <see cref="StatusEffectItem"/>, optionally
/// overrides its duration, fires <c>OnApply</c>, and appends it
/// to the active list.</summary>
/// <param name="statusEffectId">Factory key matching a
/// <see cref="StatusEffectInfo"/> entry.</param>
/// <param name="duration">If positive, overwrites the default
/// duration from the effect definition.</param>
/// <returns><see langword="false"/> if the factory lookup fails
/// (key not found); otherwise <see langword="true"/>.</returns>
public bool Add(string statusEffectId, int duration = -1)
{
var statusEffectInfo = Factories.Impls.StatusEffectInfoFactory.Instance.Get(statusEffectId);
if (statusEffectInfo == null)
{
GameLogger.Log("STATUS", $"statusEffect with ID '{statusEffectId}' not found!", LogLevel.Error);
return false;
}
var statusEffect = new StatusEffectItem(statusEffectInfo);
if (duration > 0)
{
statusEffect.Duration = duration;
statusEffect.RemainingTurns = duration;
}
statusEffect.Actions?.OnApply(owner);
activeStatusEffectData.Add(statusEffect);
return true;
}
/// <summary>Removes the first effect matching
/// <paramref name="statusEffectId"/> from the active list.
/// Fires <c>OnRemove</c> so the effect can clean up before
/// disappearing (e.g. revert stat modifications).</summary>
/// <param name="statusEffectId">Factory key of the effect to
/// remove.</param>
/// <returns><see langword="false"/> if no matching effect was
/// found; otherwise <see langword="true"/>.</returns>
public bool Remove(string statusEffectId)
{
var statusEffect = activeStatusEffectData.FirstOrDefault(b => b.ID == statusEffectId);
if (statusEffect == null)
return false;
statusEffect.Actions?.OnRemove(owner);
activeStatusEffectData.Remove(statusEffect);
return true;
}
/// <summary>Advances all active effects at the start of the owner's
/// turn. Fires <c>OnTurnStart</c> on each effect, decrements
/// <see cref="IStatusEffect.RemainingTurns"/> for timed effects,
/// and removes expired ones after firing
/// <c>OnFinalAction</c>. Iterates backwards so removals do not
/// corrupt the index.</summary>
public void OnTurnStart()
{
for (int i = activeStatusEffectData.Count - 1; i >= 0; i--)
{
var statusEffect = activeStatusEffectData[i];
statusEffect.Actions?.OnTurnStart(owner);
if (statusEffect.RemainingTurns > 0 && statusEffect.Duration > 0)
{
statusEffect.RemainingTurns--;
if (statusEffect.RemainingTurns <= 0)
{
statusEffect.Actions?.OnFinalAction(owner);
activeStatusEffectData.RemoveAt(i);
}
}
}
}
/// <summary>Fires end-of-turn hooks on all active effects. Operates
/// on a defensive copy so that <c>OnTurnEnd</c> callbacks that
/// add or remove effects do not invalidate the enumerator.
/// Decrements remaining turns and removes expired effects via
/// <see cref="Remove"/> (which also fires
/// <c>OnRemove</c>).</summary>
public void OnTurnEnd()
{
foreach (var statusEffect in ActiveStatusEffects)
{
statusEffect.Actions?.OnTurnEnd(owner);
statusEffect.RemainingTurns--;
if (statusEffect.RemainingTurns <= 0)
{
statusEffect.Actions?.OnFinalAction(owner);
Remove(statusEffect.ID);
}
}
}
/// <summary>Pipes outgoing damage through every active effect's
/// <c>OnDealDamage</c> hook, allowing each to modify
/// <paramref name="damage"/> via <see langword="ref"/>.
/// Effects iterate in insertion order; multiplicative stacking
/// means order-agnostic results for flat modifiers but
/// order-sensitive for percentage modifiers.</summary>
/// <param name="target">The character receiving the damage.</param>
/// <param name="damage">The damage value, modifiable by effects
/// through the <see langword="ref"/> parameter.</param>
public void OnDealDamage(Character target, ref int damage)
{
foreach (var statusEffect in activeStatusEffectData)
{
statusEffect.Actions?.OnDealDamage(owner, target, ref damage);
}
}
/// <summary>Pipes incoming damage through every active effect's
/// <c>OnTakeDamage</c> hook. Called after
/// <see cref="OnDealDamage"/> in the damage pipeline, so
/// defensive modifiers stack on top of the attacker's
/// amplified value.</summary>
/// <param name="source">The character dealing the damage.</param>
/// <param name="damage">The damage value, modifiable by effects
/// through the <see langword="ref"/> parameter.</param>
public void OnTakeDamage(Character source, ref int damage)
{
foreach (var statusEffect in activeStatusEffectData)
{
statusEffect.Actions?.OnTakeDamage(owner, source, ref damage);
}
}
/// <summary>Removes all active effects, firing <c>OnRemove</c> on
/// each before clearing the list. Used when a character dies or
/// an encounter ends so effects do not leak between battles.</summary>
public void ClearAll()
{
foreach (var statusEffect in activeStatusEffectData)
{
statusEffect.Actions?.OnRemove(owner);
}
activeStatusEffectData.Clear();
}
/// <summary>Deep-clones the entire queue for a new character owner.
/// Each effect is copied via <see cref="IStatusEffect.Copy"/>,
/// and <c>OnApply</c> is re-fired on <paramref name="newOwner"/>
/// so initialisation hooks run in the target's context. Used
/// when a character is duplicated mid-encounter (e.g. a summon
/// or mirror effect).</summary>
/// <param name="newOwner">The character who will own the
/// clone.</param>
/// <returns>A new <see cref="StatusEffectQueue"/> bound to
/// <paramref name="newOwner"/> with copies of all active
/// effects.</returns>
public StatusEffectQueue Copy(Character newOwner)
{
var newSystem = new StatusEffectQueue(newOwner);
foreach (var statusEffect in activeStatusEffectData)
{
var copiedstatusEffect = statusEffect.Copy();
copiedstatusEffect.Actions?.OnApply(newOwner);
newSystem.activeStatusEffectData.Add(copiedstatusEffect);
}
return newSystem;
}
}
}
+40
View File
@@ -0,0 +1,40 @@
using System.Runtime.CompilerServices;
using Cthangover.Core.Characters;
namespace Cthangover.Core.Cards.StatusEffect
{
/// <summary>
/// Side-table that owns the <see cref="StatusEffectQueue"/> of every character
/// participating in battle. The queue used to live on <see cref="Character"/>
/// itself, but the core character model must stay free of battle subsystems —
/// so the battle mod keeps queues keyed by character instance instead.
/// <para/>
/// Uses <see cref="ConditionalWeakTable{TKey, TValue}"/>: queues die together
/// with their character, which matters because each battle constructs fresh
/// copies via <see cref="Character.Copy"/> and discards them afterwards. All
/// access happens on the main thread during battle, so no extra locking is
/// needed beyond what the weak table provides.
/// </summary>
public static class StatusEffectService
{
private static readonly ConditionalWeakTable<Character, StatusEffectQueue> queues = new();
/// <summary>
/// Returns the queue of <paramref name="character"/>, creating an empty one
/// on first access. Returns <c>null</c> for a null character so call sites
/// can keep their existing null-conditional patterns.
/// </summary>
public static StatusEffectQueue Of(Character character)
{
if (character == null)
return null;
return queues.GetValue(character, CreateQueue);
}
private static StatusEffectQueue CreateQueue(Character owner)
{
return new StatusEffectQueue(owner);
}
}
}
+26
View File
@@ -0,0 +1,26 @@
namespace Cthangover.Core.Cards.StatusEffect
{
/// <summary>
/// High-level classification for status effects. Buff/Debuff are
/// informational (UI colouring, stacking rules); Stun is singled
/// out because the queue has a dedicated HasStun() check that the
/// battle engine queries to skip a stunned character's turn.
/// </summary>
public enum StatusEffectType
{
/// <summary>Positive effect that benefits the bearer (e.g. stat boost,
/// regeneration). Rendered with a friendly tint in the UI.</summary>
Buff,
/// <summary>Negative effect that harms the bearer (e.g. damage over
/// time, stat reduction). Rendered with a hostile tint in the
/// UI.</summary>
Debuff,
/// <summary>Crowd-control effect that causes the bearer to skip
/// their turn. Checked by
/// <see cref="StatusEffectQueue.HasStun"/>; the action hooks
/// are deliberately no-ops — the skip is enforced by the battle
/// engine reading this type flag, not by the
/// <see cref="IStatusActions"/> callbacks.</summary>
Stun,
}
}