🔥 Damage Affinities¶
Affinity providers define per-tag damage multipliers (immune / resist / weak).
They implement IDamageAffinity and are evaluated by AffinityRule during the PRE-damage pipeline.
Affinities are:
- Victim-side modifiers
- Purely multiplicative
- Data-driven and composable
- Allocation-free and side-effect free (providers should not mutate state)
🧩 Provided Types¶
AffinityProfile (ScriptableObject)¶
Reusable authoring table of damage-tag multipliers.
- Drag‑and‑drop authoring via Inspector
- Each entry may include one or more
DamageTagbits - Overlapping entries stack multiplicatively
- Designed for races, elements, enemy types, or equipment sets
Examples
- Fire = 0.0 → immune
- Poison = 0.5 → resist
- Melee = 1.25 → weak
AffinityProfileProvider (MonoBehaviour)¶
Bridges an AffinityProfile into the runtime damage pipeline.
- Wraps an
AffinityProfileand implementsIDamageAffinity - Multiple providers on the same object are supported and stack multiplicatively
- Best for shared tuning and data-driven balance passes
Use this when you want: - Centralised tuning - Reuse across many prefabs - One profile per enemy archetype / equipment set / faction
SimpleAffinityProvider (MonoBehaviour)¶
Inline, prefab-local affinity table.
- Inspector-based configuration
- Same evaluation semantics as
AffinityProfile(mask entries, multiplicative stacking) - Best for prototypes, one-offs, or local overrides
⚙️ Evaluation Model (what AffinityRule actually does)¶
For each incoming hit (when DamageContext.Tags != None):
- If the hit bypasses affinities (
ctx.BypassRulesincludesRuleBypass.Affinity), the system is skipped. AffinityRuleenumerates each set bit inctx.Tagsand evaluates them one bit at a time.- For each evaluated tag bit, every active
IDamageAffinityprovider on the victim is queried. - Providers return:
null→ no opinion- a finite
float→ contributes multiplicatively - Contributions are guarded:
- non-finite values (NaN/Inf) are ignored
- each contribution is clamped to
minFactor..maxFactor - If no provider has an opinion for any tag bit, affinities are skipped (no change).
- If the accumulated product is ≤ 0, the hit is cancelled (full immunity) and the PRE chain short-circuits.
- Otherwise, the bounded product is multiplied into
ctx.Multiplier.
No allocations, no mutation, no hidden state.
Stacking Example
Affinities stack across all providers and sources:
- RacialProfile: Fire ×0.5
- ShieldRune: Fire ×0.8
- BossDebuff: Fire ×1.2
Final multiplier:
0.5 × 0.8 × 1.2 = 0.48
🧰 Typical Setup¶
- Add
DamageRuleHubto the victim - Add
AffinityRule(Priority:DamageRulePriority.Affinity) - Add one or more affinity providers:
AffinityProfileProviderSimpleAffinityProvider- Configure multipliers for relevant
DamageTagvalues
That’s it — the system is declarative and composable.
⚠️ Design Notes & Boundaries¶
- Affinities are victim-side only
- They are intended to answer: “How effective is this damage type against this target?”
- They are not flat mitigation (use Armor/flat rules for that)
- They do not replace shields or execute logic
- Providers may inspect
DamageContextfor situational logic, but should remain side-effect free
❌ What Not To Do¶
- Do not encode gameplay logic inside
AffinityRule(keep it a pure aggregator) - Do not return
1to mean “no opinion” — returnnull - Do not mix flat mitigation into affinities
- Do not assume affinities apply to healing (they do not)