package eu.abdelbaki.cmmodular; import net.minecraft.core.registries.Registries; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.ai.attributes.Attribute; import net.minecraft.world.entity.ai.attributes.AttributeInstance; import net.minecraft.world.entity.ai.attributes.AttributeModifier; import net.minecraft.world.entity.ai.attributes.Attributes; import net.minecraft.world.entity.ai.attributes.RangedAttribute; import net.minecraft.world.entity.player.Player; import net.neoforged.bus.api.IEventBus; import net.neoforged.neoforge.common.NeoForge; import net.neoforged.neoforge.event.entity.EntityAttributeModificationEvent; import net.neoforged.neoforge.event.entity.living.LivingIncomingDamageEvent; import net.neoforged.neoforge.event.tick.PlayerTickEvent; import net.neoforged.neoforge.registries.DeferredHolder; import net.neoforged.neoforge.registries.DeferredRegister; /** * A sea serpent fang in hand is worth more in water than out of it: harder * hits and a faster swing while the bearer is in it. * *

The mining half of the same idea needs nothing here - vanilla models * underwater digging as an attribute of its own, * {@code minecraft:player.submerged_mining_speed}, and the material simply * adds to it. Damage and attack speed have no such switch, so this follows the * pattern {@link FireImmunity} and {@link WaterBreathing} already set: an * attribute of this mod's own that the material grants like any other, and a * listener here that watches for it. * *

Damage is answered on the blow, which needs no state. Attack speed cannot * be - it is read off the attribute map whenever the game wants to know how * fast the bearer swings - so a modifier goes on and comes off as the bearer * enters and leaves the water. It is transient and keyed by one id, so a * player who never touches a fang never has one and a player who does never * collects two. */ public final class WaterCombat { private static final DeferredRegister ATTRIBUTES = DeferredRegister.create(Registries.ATTRIBUTE, CMModular.MOD_ID); /** No more than half again, however much fang the weapon is built from. */ private static final double MAX = 0.5D; /** The fraction added to damage dealt and to swing speed, in water only. */ public static final DeferredHolder WATER_COMBAT = ATTRIBUTES.register("water_combat", id -> new RangedAttribute( "attribute.cmmodular.water_combat", 0.0D, 0.0D, MAX).setSyncable(true)); /** The one attack speed modifier this puts on, so it can be taken off. */ private static final ResourceLocation SWING_ID = ResourceLocation.fromNamespaceAndPath(CMModular.MOD_ID, "water_combat"); private WaterCombat() { } static void register(IEventBus modBus) { ATTRIBUTES.register(modBus); modBus.addListener(WaterCombat::attachToEntities); NeoForge.EVENT_BUS.addListener(WaterCombat::onIncomingDamage); NeoForge.EVENT_BUS.addListener(WaterCombat::onPlayerTick); } /** Attributes only exist on an entity if they were added to its type. */ private static void attachToEntities(EntityAttributeModificationEvent event) { event.getTypes().forEach(type -> event.add(type, WATER_COMBAT)); } private static void onIncomingDamage(LivingIncomingDamageEvent event) { if (!(event.getSource().getEntity() instanceof LivingEntity attacker) || !attacker.isInWater()) { return; } double bonus = boost(attacker); if (bonus > 0.0D) { event.setAmount((float) (event.getAmount() * (1.0D + bonus))); } } /** * Keeps the swing speed modifier in step with the water and the weapon. * *

Only players, because attack speed is only ever read for a player. * The equipment is only consulted while the bearer is in water, so this * costs nothing on dry land. */ private static void onPlayerTick(PlayerTickEvent.Post event) { Player player = event.getEntity(); AttributeInstance speed = player.getAttribute(Attributes.ATTACK_SPEED); if (speed == null) { return; } double bonus = player.isInWater() ? boost(player) : 0.0D; AttributeModifier applied = speed.getModifier(SWING_ID); if (bonus <= 0.0D) { if (applied != null) { speed.removeModifier(SWING_ID); } return; } if (applied != null && applied.amount() == bonus) { return; } // Weapon swapped for one with more fang in it, or the map was rebuilt // out from under the modifier - either way, put the right one back. speed.removeModifier(SWING_ID); speed.addTransientModifier(new AttributeModifier(SWING_ID, bonus, AttributeModifier.Operation.ADD_MULTIPLIED_BASE)); } /** What the held modules are worth, clamped to what the attribute allows. */ private static double boost(LivingEntity entity) { return Math.min(EquipmentAttributes.value(entity, WATER_COMBAT), MAX); } }