b451f83174
Captures the pre-Godot-port state of the codebase. This is the rollback anchor for the Godot port (M0 of theriapolis-rpg-implementation-plan-godot-port.md). All Phase 0 through Phase 6.5 work is included; Phase 7 is in flight. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
80 lines
2.5 KiB
C#
80 lines
2.5 KiB
C#
using Theriapolis.Core.Rules.Stats;
|
|
|
|
namespace Theriapolis.Core.Rules.Combat;
|
|
|
|
/// <summary>
|
|
/// Phase 5 M6 player death-save loop. d20 every turn while at 0 HP:
|
|
/// - 1 → 2 failures
|
|
/// - 2..9 → 1 failure
|
|
/// - 10..19 → 1 success
|
|
/// - 20 → revive at 1 HP (zero out failures + successes)
|
|
///
|
|
/// 3 cumulative successes (≥10) → stabilised at 0 HP (cleared on heal).
|
|
/// 3 cumulative failures (<10) → dead. CombatHUDScreen pushes
|
|
/// <see cref="Game.Screens.DefeatedScreen"/> when this fires.
|
|
///
|
|
/// Tracker lives on <see cref="Combatant"/> only for the player; NPC
|
|
/// combatants skip death saves and are removed at 0 HP.
|
|
/// </summary>
|
|
public sealed class DeathSaveTracker
|
|
{
|
|
public int Successes { get; private set; }
|
|
public int Failures { get; private set; }
|
|
public bool Stabilised { get; private set; }
|
|
public bool Dead { get; private set; }
|
|
|
|
/// <summary>Roll a death save and update counters. Returns the outcome.</summary>
|
|
public DeathSaveOutcome Roll(Encounter enc, Combatant target)
|
|
{
|
|
if (Dead || Stabilised) return DeathSaveOutcome.NoOp;
|
|
|
|
int d20 = enc.RollD20();
|
|
DeathSaveOutcome outcome;
|
|
if (d20 == 20)
|
|
{
|
|
// Critical success — revive at 1 HP.
|
|
target.CurrentHp = 1;
|
|
target.Conditions.Remove(Condition.Unconscious);
|
|
Successes = 0;
|
|
Failures = 0;
|
|
outcome = DeathSaveOutcome.CriticalRevive;
|
|
}
|
|
else if (d20 >= 10)
|
|
{
|
|
Successes++;
|
|
outcome = Successes >= 3 ? DeathSaveOutcome.Stabilised : DeathSaveOutcome.Success;
|
|
if (Successes >= 3) Stabilised = true;
|
|
}
|
|
else
|
|
{
|
|
int failsThisRoll = d20 == 1 ? 2 : 1;
|
|
Failures += failsThisRoll;
|
|
outcome = Failures >= 3 ? DeathSaveOutcome.Dead : DeathSaveOutcome.Failure;
|
|
if (Failures >= 3) Dead = true;
|
|
}
|
|
|
|
enc.AppendLog(CombatLogEntry.Kind.Save,
|
|
$"{target.Name} death save: {d20} → {outcome} ({Successes}S/{Failures}F)");
|
|
return outcome;
|
|
}
|
|
|
|
/// <summary>Called when the character is healed above 0 HP — cancels the loop.</summary>
|
|
public void Reset()
|
|
{
|
|
Successes = 0;
|
|
Failures = 0;
|
|
Stabilised = false;
|
|
// Don't reset Dead — once dead, stays dead.
|
|
}
|
|
}
|
|
|
|
public enum DeathSaveOutcome
|
|
{
|
|
NoOp = 0,
|
|
Success = 1,
|
|
Failure = 2,
|
|
Stabilised = 3,
|
|
Dead = 4,
|
|
CriticalRevive = 5,
|
|
}
|