-
Notifications
You must be signed in to change notification settings - Fork 0
Combat
Package: dev.goshi.omnimixin.api.event.combat
Everything to do with health going down (PlayDamage, PlayKnockback,
PlayArmor) and health going up or being lost permanently (PlayHealth),
plus status effects (PlayStatusEffect).
Three phases sharing one [DamageContext](https://claude.ai/chat/a3c01413-be46-46f0-a149-92ab00193506#damagecontext):
PRE_CALCULATE → MODIFY_AMOUNT → (vanilla applies the damage) → POST_APPLY
// Immunity to fire damage for anything wearing my custom fire cloak.
PlayDamage.PRE_CALCULATE.register(ctx -> {
if (ctx.source().isIn(DamageTypeTags.IS_FIRE) && isWearingFireCloak(ctx.victim())) {
ctx.cancel().cancel();
}
});
// A "last stand" perk: never let a hit drop a player below 1 HP.
PlayDamage.MODIFY_AMOUNT.register(ctx -> {
if (ctx.victim() instanceof PlayerEntity player && hasLastStandPerk(player)) {
float wouldSurvive = player.getHealth() - ctx.finalDamage().get();
if (wouldSurvive < 1.0f) {
ctx.finalDamage().set(player.getHealth() - 1.0f);
}
}
});
// Log every hit that actually landed, for a combat log feature.
PlayDamage.POST_APPLY.register((ctx, applied) -> {
if (applied) {
combatLog.record(ctx.victim(), ctx.source(), ctx.finalDamage().get());
}
});
DamageContext(LivingEntity victim, DamageSource source, float originalDamage, MutableFloat finalDamage, Cancellable cancel)
originalDamage never changes across all three phases — it's always the
true starting number, so you can always compare "what was it originally"
against "what is it now" (finalDamage.get()). Cancelling from either
PRE_CALCULATE or MODIFY_AMOUNT blocks the damage entirely and skips the
remaining phases (including POST_APPLY).
PRE_APPLY and MODIFY_FORCE, sharing [KnockbackContext](https://claude.ai/chat/a3c01413-be46-46f0-a149-92ab00193506#knockbackcontext).
// Grant knockback resistance to anything standing in molasses.
PlayKnockback.MODIFY_FORCE.register(ctx -> {
if (isInMolasses(ctx.entity())) {
ctx.forceMultiplier().multiply(0.2f);
}
});
// Fully immune to knockback while channeling a spell.
PlayKnockback.PRE_APPLY.register(ctx -> {
if (isChanneling(ctx.entity())) {
ctx.cancel().cancel();
}
});
KnockbackContext(LivingEntity entity, double originalStrength, double x, double z, MutableFloat forceMultiplier, Cancellable cancel)
x/z describe the horizontal direction the knockback is being applied
in — useful if you want to redirect rather than just scale it (you'd need to
apply your own velocity change separately; this event only exposes a
strength multiplier, not a direction override).
MODIFY_PROTECTION adjusts how much damage armor blocks; ON_DURABILITY_DAMAGE
lets you veto armor losing durability from a specific hit.
// A "vanity armor" toggle: skip durability loss, but keep the protection.
PlayArmor.ON_DURABILITY_DAMAGE.register(ctx -> {
if (isVanityModeEnabled(ctx.victim())) {
ctx.cancel().cancel();
}
});
ArmorContext(LivingEntity victim, DamageSource source, float incomingDamage, float armorAdjustedDamage, MutableFloat modifier)
DurabilityContext(LivingEntity victim, DamageSource source, float amount, Cancellable cancel)
APPLY fires when a potion/beacon/other effect is about to be added to an
entity; EXPIRE fires when one is about to be removed (naturally running
out, milk bucket, etc.). Both are cancellable.
// Immunity to a specific custom "cursed" effect for anything holy-blessed.
PlayStatusEffect.APPLY.register(ctx -> {
if (ctx.effect().getEffectType() == MyEffects.CURSED && isBlessed(ctx.entity())) {
ctx.cancel().cancel();
}
});
StatusEffectApplyContext(LivingEntity entity, StatusEffectInstance effect, Entity source, Cancellable cancel)
StatusEffectExpireContext(LivingEntity entity, RegistryEntry<StatusEffect> effect, Cancellable cancel)
Note: only application and expiry are covered — a per-tick TICK hook is
not, see Known Limitations.
Covers healing, death, and absorption (yellow hearts) — the "gaining or
holding onto health" side of combat, as opposed to PlayDamage's "losing
it" side.
// Double the effectiveness of all healing.
PlayHealth.HEAL.register(ctx -> ctx.modifier().multiply(2.0f));
// Custom last-words message on death.
PlayHealth.DEATH.register(ctx -> {
if (ctx.entity() instanceof PlayerEntity player) {
broadcastLastWords(player, ctx.source());
}
});
// Absorption hearts decay 50% slower for anything under a "ward" buff.
PlayHealth.ABSORPTION_CHANGE.register(ctx -> {
if (ctx.newAmount() < ctx.oldAmount() && hasWardBuff(ctx.entity())) {
float decayed = ctx.oldAmount() - ctx.newAmount();
ctx.modifier().set(ctx.oldAmount() - decayed * 0.5f);
}
});
HealContext(LivingEntity entity, float originalAmount, MutableFloat modifier, Cancellable cancel)
DeathContext(LivingEntity entity, DamageSource source, Cancellable cancel)
Important: by the time DEATH fires, the entity's health has already hit
zero and vanilla has already committed to the kill. Cancelling here skips
onDeath's own side effects — loot drops, the death message, advancement
and stat updates, the death animation and sound — but the entity does
not come back to life. It stays at zero health without ever finishing
its death sequence. If you actually want to prevent a kill, intervene
earlier in PlayDamage.MODIFY_AMOUNT and cap the damage short of lethal
instead.
AbsorptionChangeContext(LivingEntity entity, float oldAmount, float newAmount, MutableFloat modifier, Cancellable cancel)
Note: there's deliberately no raw hook on LivingEntity.setHealth(float)
itself — see Known Limitations for why.