Skip to content

Health System — System Guarantees Matrix

This document defines the explicit behavioural guarantees of the RevFramework Health System.

It mirrors the structure used in other systems (e.g., Currency) and is intentionally strict.

This is the contract

If something is not listed here, it is not guaranteed.


1. Core Health

Aspect Guarantee
State ownership Per-GameObject Current and Max values owned by the health host
Units External HP state is integer-based
Internal math Internal rule math may use floats before final rounding
Rounding mode Half-to-even (banker's), not half-up — 2.5 lands as 2, 3.5 as 4
Threading Main-thread usage
Mutation surface Public APIs only
HP bounds Always clamped [0..Max]
Death handling Idempotent
Cross-component atomicity ❌ Not guaranteed

2. Damage Pipeline

Aspect Guarantee
Guard order Authority → State Guards (Dead → Invincible → DamageLock)
PRE rule order Ascending Priority
Rule bypass Specific rule families may be skipped via RuleBypass
DamageAttempted Fires once per evaluated attempt (applied or rejected)
Damage evaluation (RawAmount * Multiplier) + FlatDelta
Extreme amounts ✔ Saturate at int.MaxValue rather than overflowing — int.MaxValue damage lands as a kill, on every platform
Shield position After PRE rules, before HP mutation
Shield bypass Respects BypassShields
POST observers Run once for attempts that reach POST evaluation (implementation-dependent for early rejections)
Death trigger Only after HP ≤ 0
Cross-attempt atomicity ❌ Not guaranteed

3. Heal Pipeline

Aspect Guarantee
Guard order Authority → Dead → Amount ≤ 0
PRE heal rules Ascending Priority
Healing modifiers Multiplicative (IHealingModifier)
Zero-apply heals Treated as non-applied (no events guaranteed)
Extreme amounts ✔ Saturate at int.MaxValue rather than overflowing, both in the scale step and when added to current health
HP mutation Clamped to Max
Overheal spill Optional spill into OverhealShield when enabled
POST observers Run once per successful heal
Lifecycle bypass Revive / SetCurrent bypass the rule pipeline
Cross-op atomicity ❌ Not guaranteed

4. Shields

Aspect Guarantee
Execution slot After PRE damage rules
Mutation Shields never mutate HP directly; they only modify incoming damage
Full absorb HP unchanged
Partial absorb Remainder applied to HP
Preview seam Accurate preview requires IShieldPreview
Shield selection Defined by ShieldChain when present; otherwise first enabled shield
ShieldPool absorption ❌ Not a shield

5. Rules

Aspect Guarantee
PRE rules May mutate context or cancel
POST rules Observe finalized outcome
Rule ordering Deterministic by Priority
Disabled rules Skipped — both stages, PRE and POST
Exception safety Exceptions caught; pipeline continues
Direct HP mutation ❌ Not supported through rule contracts
Cross-rule atomicity ❌ Not guaranteed

A rule component that is unticked in the Inspector, or whose GameObject is inactive, does not run. Re-ticking it takes effect on the very next evaluation — no Refresh() needed, because the check is made when rules run rather than when they are discovered.

Changed in 1.2.0

Disabled PRE rules used to run anyway: rules were discovered with GetComponents, which includes disabled components, and nothing consulted enabled. The POST stage already honoured the tickbox, so the two halves of one pipeline disagreed. If a project relied on a disabled-but-attached PRE rule still applying, that rule now needs to be enabled or removed.


6. Authority

Aspect Guarantee
Opt-in gating requireAuthority
Denied mutation Deterministic block
Error surface AuthorityDenied event + LastAuthorityError, plus one console warning per component (suppressible with logAuthorityDenials)
Throwing authority ⚠ The exception propagates out of the mutation, before any write, event, LastAuthorityError or warning — so neither surface above reports it. Not converted to a denial; Currency is the only system that converts. Inside the before-death seam a throw is caught as handler isolation and death proceeds
Read-only access Allowed
Disabled or destroyed authority ✔ Stops answering on the next mutation; resolution reopens. True of a custom IHealthAuthority as well as of the shipped binder
Swapped binder ✔ Takes effect on the next mutation
Automatic discovery ✔ Any IHealthAuthority, nearest first — object, parents, scene. Changed in 1.3.0; it was the HealthAuthorityBinder component type only
Custom IHealthAuthority ✔ Found in the scene like the shipped binder, and subject to the same active-and-enabled test — at resolution and on every later mutation
SetAuthorityResolver ✔ Still takes precedence over anything resolved from the scene
Replication ❌ Not provided
Ownership model ❌ Not defined

Discovery matches the interface — it did not before 1.3.0

Every resolution step now looks for IHealthAuthority, which is what the other four discovering systems have always done. Until 1.3.0 every step looked for the concrete HealthAuthorityBinder, so a custom implementation left in the scene was never found and the public interface had no reachable caller at all.

Nearest wins, and it wins whether or not another component resolved first. The two hierarchy steps — the object being gated, then its parents — run ahead of the scene cache and their answer is never written into it, so an actor carrying its own IHealthAuthority is governed by that one and nothing else. Only the scene-wide steps cache, and the entry they write answers for every HealthSystem that has no authority of its own.

Until 1.3.0 the cache was consulted first and hierarchy hits were cached, so "nearest wins" held only until some other component in the scene resolved: after that an actor's own denying authority was never asked, and an actor's own granting one leaked to every actor that had none.

SetAuthorityResolver still wins over anything resolved from the scene, and remains the way to install a rule the scene cannot express. See the Authority section of the Health Overview.


7. Lifecycle

Aspect Guarantee
Death pipeline IBeforeDeathHandler → IHealthDeathHandler → UnityEvents → C# event
Cancellation Only via IBeforeDeathHandler
Revive Clears dead state
Revive atomicity ✔ Single health mutation operation
Multi-handler atomicity ❌ Not guaranteed

8. Regeneration

Aspect Guarantee
Delay after damage
Smooth/tick modes
Ceiling enforcement ✔ (when enabled)
Authority respect
Frame-rate independence ✔ Both modes carry the remainder across frames
Heals per frame Smooth: up to Max Smooth Heals Per Frame. Tick: one
Catch-up after a stall ⚠ Bounded at one interval in tick mode
Multi-source coordination ❌ Not guaranteed
MaxHealthModifierStack entries persisted ❌ No — see below

Frame-rate independence, precisely. Both modes accumulate: smooth carries a fractional heal, tick carries the overshoot past the interval. So the long-run rate is the configured rate at any frame rate, and two clients running at 144 Hz and 30 Hz agree.

Tick mode heals at most once per frame, which is what makes it discrete. A frame longer than a whole interval therefore cannot be paid off inside it, and the carried debt is capped at one interval rather than banked — a stall costs you the ticks it swallowed instead of discharging them as a burst when the frame rate recovers. With the default one-second interval this is unreachable; with the 0.01 s minimum it is every frame. Smooth mode has no such cap and heals up to Max Smooth Heals Per Frame.

Corrected in 1.3.0

This row read "✔ (accumulator-based)" and tick mode was not accumulator-based: it reset its timer to zero after each tick, discarding the overshoot, so the effective interval was interval + E[overshoot] — a function of frame time. Measured at the defaults over 600 s: 595 heals at 120 fps against 590 at 60 fps, where 600 was due. The remainder is now carried, and HealthRegenerationFrameRateTests compares the tick count at two frame rates.

MaxHealthModifierStack is not a save participant. Neither its entries nor its captured baseline is written to a save; the Health section persists HealthSystem and nothing else. A load therefore restores the effective max the stack had produced and knows nothing about the stack.

This is the ordinary shape for a modifier stack — what buffs an actor is normally rebuilt by whatever applies it — but it has a consequence worth stating: Reapply() after a load recomputes from the Awake baseline, so a game that does not re-push its modifiers on load will see the first reapply overwrite the restored max. Re-push your modifiers before anything triggers a reapply, or call CaptureBaselineFromCurrent() after the load.


9. Invincibility

Aspect Guarantee
Damage block ✔ While active
Tick ownership HealthSystem drives Tick
Disable edge Disabling while active emits end event once
Scaled/unscaled Supported
Retroactive block ❌ No

Persistence

Aspect Guarantee
Snapshot type Absolute state
Restore behaviour Silent
Event emission ❌ None
Shield persistence ❌ Not automatic
Cross-owner atomicity ❌ Not guaranteed

11. Preview

Aspect Guarantee
No health / shield / death mutation
No events
No death
Rule state left alone ✔ for every shipped rule — see below
Sees rules the hit will see
Invincibility considered ❌ Ignored
Authority checks ❌ Ignored
Damage lock considered ❌ Ignored
Shield accuracy Only if IShieldPreview implemented
Deterministic ordering ✔ (structural ordering only)

Preview runs the live rule components, which is what makes it agree with the hit it is predicting. Each is told through DamageContext.IsPreview / HealContext.IsPreview that nothing will be committed; CritRule and AntiHealRule do not roll when they see it, and no other shipped rule holds state to advance.

A custom rule that ignores IsPreview still mutates

This guarantee covers the rules RevFramework ships. It cannot cover a rule of your own that advances a counter, cooldown or RNG without checking the flag — preview calls straight into it, at whatever rate your UI repaints.


12. Events

Aspect Guarantee
Death event Raised once per death
DamageAttempted Fires per attempt
HealthChanged After HP mutation
Event ordering Deterministic within a single health mutation pipeline
Event emission Occurs after evaluation; mutation-dependent events fire only on successful state changes
Listener isolation Exceptions caught
Cross-system ordering ❌ Not guaranteed

13. Public API

Aspect Guarantee
Stable contracts ✔ (DamageContext, HealContext, etc.)
Internal namespaces ❌ Not supported
Composition safety
Subclass safety ❌ Not supported

Non-Guarantees

The Health system does not guarantee

  • ❌ Multi-operation atomicity across rules + shields + handlers
  • ❌ Multiplayer replication
  • ❌ Rollback determinism
  • ❌ Cross-scene authority safety
  • ❌ Cross-owner atomic mutation
  • ❌ Network prediction
  • ❌ Automatic shield/effect persistence
  • ❌ Deterministic RNG unless explicitly seeded
  • ❌ Shield stacking behaviour without ShieldChain

Summary

Health guarantees

  • Deterministic ordering
  • Explicit extension seams
  • Idempotent death handling
  • Event isolation
  • Clear responsibility boundaries

Health does not attempt to be:

  • A netcode framework
  • A transaction engine
  • A persistence manager
  • A prediction system
  • A gameplay brain

If it’s not listed above, it is not guaranteed.