Status effects expire on their data duration; loot rolls never throw
A status effect added without an explicit duration started with no remaining turns and the countdown skipped it, so it never expired. OnTurnEnd counted down a second time, expired unlimited effects after one turn and removed expired stacks by id; it is now a pure hook. The victory screen rolls loot through LootEntry.Roll, so a loot table with MinCount > MaxCount no longer throws half-way through the battle end, and its success-path diagnostics are no longer logged as errors. Mod unit tests for the status effect lifetime.
This commit is contained in:
1 parent
72b54eb564
commit
68976523f0
5 files changed
+55
-27
No files matched your search
@@ -8,5 +8,6 @@
|
|||||||
"sources": [
|
"sources": [
|
||||||
"src/**/*.cs"
|
"src/**/*.cs"
|
||||||
],
|
],
|
||||||
|
"test_sources": ["tests/*.cs"],
|
||||||
"depends": [ "characters_base>=1", "ui_base>=1", "items_base>=1", "quests_base>=1", "time_base>=1" ]
|
"depends": [ "characters_base>=1", "ui_base>=1", "items_base>=1", "quests_base>=1", "time_base>=1" ]
|
||||||
}
|
}
|
||||||
@@ -45,6 +45,7 @@ namespace Cthangover.Core.Cards.StatusEffect
|
|||||||
Name = info.Name;
|
Name = info.Name;
|
||||||
Description = info.Description;
|
Description = info.Description;
|
||||||
Duration = info.Duration;
|
Duration = info.Duration;
|
||||||
|
RemainingTurns = info.Duration;
|
||||||
EffectType = info.Type;
|
EffectType = info.Type;
|
||||||
Icon = TextureUtils.LoadFromModGroup("characters", info.Icon);
|
Icon = TextureUtils.LoadFromModGroup("characters", info.Icon);
|
||||||
Actions = StatusEffectActionFactory.Instance.Get(info.Actions);
|
Actions = StatusEffectActionFactory.Instance.Get(info.Actions);
|
||||||
|
|||||||
@@ -134,21 +134,13 @@ namespace Cthangover.Core.Cards.StatusEffect
|
|||||||
/// <summary>Fires end-of-turn hooks on all active effects. Operates
|
/// <summary>Fires end-of-turn hooks on all active effects. Operates
|
||||||
/// on a defensive copy so that <c>OnTurnEnd</c> callbacks that
|
/// on a defensive copy so that <c>OnTurnEnd</c> callbacks that
|
||||||
/// add or remove effects do not invalidate the enumerator.
|
/// add or remove effects do not invalidate the enumerator.
|
||||||
/// Decrements remaining turns and removes expired effects via
|
/// Durations count down in <see cref="OnTurnStart"/> only.</summary>
|
||||||
/// <see cref="Remove"/> (which also fires
|
|
||||||
/// <c>OnRemove</c>).</summary>
|
|
||||||
public void OnTurnEnd()
|
public void OnTurnEnd()
|
||||||
{
|
{
|
||||||
|
// Only the hook: the countdown belongs to OnTurnStart, a second one
|
||||||
|
// here halved every duration and expired effects without one.
|
||||||
foreach (var statusEffect in ActiveStatusEffects)
|
foreach (var statusEffect in ActiveStatusEffects)
|
||||||
{
|
|
||||||
statusEffect.Actions?.OnTurnEnd(owner);
|
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
|
/// <summary>Pipes outgoing damage through every active effect's
|
||||||
|
|||||||
@@ -75,17 +75,17 @@ namespace Cthangover.TurnBattle
|
|||||||
audioService?.PlaySound("battle/winground", SoundType.UI);
|
audioService?.PlaySound("battle/winground", SoundType.UI);
|
||||||
|
|
||||||
var bg = GetNodeOrNull<TextureRect>("TransitionBg");
|
var bg = GetNodeOrNull<TextureRect>("TransitionBg");
|
||||||
GameLogger.Log("BATTLE", $"Winground.Show: TransitionBg={(bg != null ? "found" : "NULL")}, texture={(bg?.Texture != null ? "has" : "null")}", LogLevel.Error);
|
GameLogger.Log("BATTLE", $"Winground.Show: TransitionBg={(bg != null ? "found" : "NULL")}, texture={(bg?.Texture != null ? "has" : "null")}", bg != null ? LogLevel.Debug : LogLevel.Error);
|
||||||
|
|
||||||
if (bg != null && bg.Texture == null)
|
if (bg != null && bg.Texture == null)
|
||||||
{
|
{
|
||||||
var tex = Textures.Resolve("winground");
|
var tex = Textures.Resolve("winground");
|
||||||
GameLogger.Log("BATTLE", $"Winground.Show: ResolveTexture('winground') = {(tex != null ? "loaded" : "NULL")}", LogLevel.Error);
|
GameLogger.Log("BATTLE", $"Winground.Show: ResolveTexture('winground') = {(tex != null ? "loaded" : "NULL")}", tex != null ? LogLevel.Debug : LogLevel.Error);
|
||||||
bg.Texture = tex;
|
bg.Texture = tex;
|
||||||
}
|
}
|
||||||
|
|
||||||
data = BattleRequest.Current;
|
data = BattleRequest.Current;
|
||||||
GameLogger.Log("BATTLE_UI", $"Winground.Show: BattleData={(data != null ? "found" : "NULL")}", LogLevel.Error);
|
GameLogger.Log("BATTLE_UI", $"Winground.Show: BattleData={(data != null ? "found" : "NULL")}", data != null ? LogLevel.Debug : LogLevel.Error);
|
||||||
|
|
||||||
var expTracking = BuildExpTracking();
|
var expTracking = BuildExpTracking();
|
||||||
var defeatedEnemies = BuildDefeatedEnemies();
|
var defeatedEnemies = BuildDefeatedEnemies();
|
||||||
@@ -167,7 +167,7 @@ namespace Cthangover.TurnBattle
|
|||||||
private void BuildCharExpDisplay(Dictionary<string, int> expTracking)
|
private void BuildCharExpDisplay(Dictionary<string, int> expTracking)
|
||||||
{
|
{
|
||||||
var container = charExpContainer ?? GetNodeOrNull<Container>("Content/CharExpContainer");
|
var container = charExpContainer ?? GetNodeOrNull<Container>("Content/CharExpContainer");
|
||||||
GameLogger.Log("BATTLE_UI", $"BuildCharExpDisplay: charExpContainer export={(charExpContainer != null ? "found" : "NULL")}, GetNodeOrNull fallback={(container != null ? "found" : "NULL")}", LogLevel.Error);
|
GameLogger.Log("BATTLE_UI", $"BuildCharExpDisplay: charExpContainer export={(charExpContainer != null ? "found" : "NULL")}, GetNodeOrNull fallback={(container != null ? "found" : "NULL")}", LogLevel.Debug);
|
||||||
|
|
||||||
if (container == null)
|
if (container == null)
|
||||||
{
|
{
|
||||||
@@ -180,7 +180,7 @@ namespace Cthangover.TurnBattle
|
|||||||
|
|
||||||
var charData = GameData.Instance.Runtime.CharacterData;
|
var charData = GameData.Instance.Runtime.CharacterData;
|
||||||
|
|
||||||
GameLogger.Log("BATTLE_UI", $"BuildCharExpDisplay: expTracking={(expTracking != null ? $"found ({expTracking.Count} entries)" : "NULL")}, BattleSet count={charData.BattleSet.Count}", LogLevel.Error);
|
GameLogger.Log("BATTLE_UI", $"BuildCharExpDisplay: expTracking={(expTracking != null ? $"found ({expTracking.Count} entries)" : "NULL")}, BattleSet count={charData.BattleSet.Count}", LogLevel.Debug);
|
||||||
|
|
||||||
var battleSetList = charData.BattleSet.ToList();
|
var battleSetList = charData.BattleSet.ToList();
|
||||||
|
|
||||||
@@ -208,7 +208,7 @@ namespace Cthangover.TurnBattle
|
|||||||
reportCard.Setup(cardTemplate?.Image, name, earnedExp);
|
reportCard.Setup(cardTemplate?.Image, name, earnedExp);
|
||||||
container.AddChild(reportCard);
|
container.AddChild(reportCard);
|
||||||
|
|
||||||
GameLogger.Log("BATTLE_UI", $"BuildCharExpDisplay: added reportCard for '{characterType}', id='{info.ID}', name='{name}', earnedExp={earnedExp}, cardTemplate={(cardTemplate != null ? "found" : "NULL")}, icon={(cardTemplate?.Image != null ? "has" : "null")}", LogLevel.Error);
|
GameLogger.Log("BATTLE_UI", $"BuildCharExpDisplay: added reportCard for '{characterType}', id='{info.ID}', name='{name}', earnedExp={earnedExp}, cardTemplate={(cardTemplate != null ? "found" : "NULL")}, icon={(cardTemplate?.Image != null ? "has" : "null")}", LogLevel.Debug);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -247,16 +247,7 @@ namespace Cthangover.TurnBattle
|
|||||||
if (string.IsNullOrWhiteSpace(entry.ItemId))
|
if (string.IsNullOrWhiteSpace(entry.ItemId))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var probability = Math.Clamp(entry.Probability, 0, 100);
|
var count = entry.Roll(random);
|
||||||
if (probability <= 0)
|
|
||||||
continue;
|
|
||||||
if (probability < 100 && random.Next(1, 101) > probability)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var count = entry.MinCount == entry.MaxCount
|
|
||||||
? entry.MinCount
|
|
||||||
: random.Next(entry.MinCount, entry.MaxCount + 1);
|
|
||||||
|
|
||||||
if (count > 0)
|
if (count > 0)
|
||||||
TryAddLootItem(loot, entry.ItemId, count);
|
TryAddLootItem(loot, entry.ItemId, count);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
namespace Cthangover.Mods.TurnBattle.Tests
|
||||||
|
{
|
||||||
|
using Cthangover.Core.Cards.StatusEffect;
|
||||||
|
using Cthangover.Core.Characters;
|
||||||
|
using Cthangover.Core.Mods.Testing;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lifetime of status effects: the countdown runs once per turn start, a
|
||||||
|
/// duration from the effect's data counts like an explicit one, and the
|
||||||
|
/// turn-end hook does not shorten an effect a second time.
|
||||||
|
/// </summary>
|
||||||
|
public class StatusEffectQueueTests
|
||||||
|
{
|
||||||
|
private const string Stun = "effect/physics/stun";
|
||||||
|
|
||||||
|
[ModTest("status/data_duration_expires")]
|
||||||
|
static void DataDurationExpires(ModTestContext ctx)
|
||||||
|
{
|
||||||
|
var queue = StatusEffectService.Of(new Character { ID = "status_test" });
|
||||||
|
|
||||||
|
ModAssert.True(queue.Add(Stun), "stun applied with its data duration");
|
||||||
|
queue.OnTurnStart();
|
||||||
|
queue.OnTurnStart();
|
||||||
|
ModAssert.True(queue.HasStun(), "stun still active after 2 of 3 turns");
|
||||||
|
queue.OnTurnStart();
|
||||||
|
ModAssert.False(queue.HasStun(), "stun expired after its 3 turns");
|
||||||
|
}
|
||||||
|
|
||||||
|
[ModTest("status/turn_end_does_not_shorten")]
|
||||||
|
static void TurnEndDoesNotShorten(ModTestContext ctx)
|
||||||
|
{
|
||||||
|
var queue = StatusEffectService.Of(new Character { ID = "status_test" });
|
||||||
|
|
||||||
|
queue.Add(Stun, 2);
|
||||||
|
queue.OnTurnEnd();
|
||||||
|
queue.OnTurnStart();
|
||||||
|
ModAssert.True(queue.HasStun(), "one turn of two left after a full turn");
|
||||||
|
queue.OnTurnEnd();
|
||||||
|
queue.OnTurnStart();
|
||||||
|
ModAssert.False(queue.HasStun(), "expired after two turns");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user