Skip to content

🔥 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 DamageTag bits
  • 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 AffinityProfile and implements IDamageAffinity
  • 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):

  1. If the hit bypasses affinities (ctx.BypassRules includes RuleBypass.Affinity), the system is skipped.
  2. AffinityRule enumerates each set bit in ctx.Tags and evaluates them one bit at a time.
  3. For each evaluated tag bit, every active IDamageAffinity provider on the victim is queried.
  4. Providers return:
  5. nullno opinion
  6. a finite float → contributes multiplicatively
  7. Contributions are guarded:
  8. non-finite values (NaN/Inf) are ignored
  9. each contribution is clamped to minFactor..maxFactor
  10. If no provider has an opinion for any tag bit, affinities are skipped (no change).
  11. If the accumulated product is ≤ 0, the hit is cancelled (full immunity) and the PRE chain short-circuits.
  12. 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

  1. Add DamageRuleHub to the victim
  2. Add AffinityRule (Priority: DamageRulePriority.Affinity)
  3. Add one or more affinity providers:
  4. AffinityProfileProvider
  5. SimpleAffinityProvider
  6. Configure multipliers for relevant DamageTag values

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 DamageContext for 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 1 to mean “no opinion” — return null
  • Do not mix flat mitigation into affinities
  • Do not assume affinities apply to healing (they do not)

🔗 See Also