diff --git a/src/main/java/dev/isxander/controlify/config/gui/YACLHelper.java b/src/main/java/dev/isxander/controlify/config/gui/YACLHelper.java index aad75b8..8dddf9d 100644 --- a/src/main/java/dev/isxander/controlify/config/gui/YACLHelper.java +++ b/src/main/java/dev/isxander/controlify/config/gui/YACLHelper.java @@ -15,7 +15,7 @@ import dev.isxander.controlify.controller.joystick.SingleJoystickController; import dev.isxander.controlify.controller.joystick.JoystickState; import dev.isxander.controlify.gui.screen.ControllerDeadzoneCalibrationScreen; import dev.isxander.controlify.reacharound.ReachAroundMode; -import dev.isxander.controlify.rumble.RumbleEffect; +import dev.isxander.controlify.rumble.BasicRumbleEffect; import dev.isxander.controlify.rumble.RumbleState; import dev.isxander.yacl.api.*; import dev.isxander.yacl.gui.controllers.ActionController; @@ -294,13 +294,16 @@ public class YACLHelper { .controller(ActionController::new) .action((screen, btn) -> { controller.rumbleManager().play( - RumbleEffect.byTime(t -> new RumbleState(0f, t), 20) - .join(RumbleEffect.byTime(t -> new RumbleState(0f, 1 - t), 20)) + BasicRumbleEffect.byTime(t -> new RumbleState(0f, t), 20) + .join(BasicRumbleEffect.byTime(t -> new RumbleState(0f, 1 - t), 20)) .repeat(3) - .join(RumbleEffect.constant(1f, 0f, 5).join(RumbleEffect.constant(0f, 1f, 5)).repeat(10)) + .join(BasicRumbleEffect.constant(1f, 0f, 5) + .join(BasicRumbleEffect.constant(0f, 1f, 5)) + .repeat(10) + ) ); }) - .build());; + .build()); category.group(advancedGroup.build()); diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/rumble/blockbreak/MultiPlayerGameModeMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/rumble/blockbreak/MultiPlayerGameModeMixin.java new file mode 100644 index 0000000..9e17150 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/mixins/feature/rumble/blockbreak/MultiPlayerGameModeMixin.java @@ -0,0 +1,77 @@ +package dev.isxander.controlify.mixins.feature.rumble.blockbreak; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import dev.isxander.controlify.api.ControlifyApi; +import dev.isxander.controlify.rumble.ContinuousRumbleEffect; +import dev.isxander.controlify.rumble.RumbleState; +import net.minecraft.client.multiplayer.MultiPlayerGameMode; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.network.protocol.Packet; +import net.minecraft.world.level.block.state.BlockState; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(MultiPlayerGameMode.class) +public class MultiPlayerGameModeMixin { + @Unique private ContinuousRumbleEffect blockBreakRumble = null; + + @Inject(method = "method_41930", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/multiplayer/ClientLevel;destroyBlockProgress(ILnet/minecraft/core/BlockPos;I)V")) + private void onStartBreakingBlock(BlockState state, BlockPos pos, Direction direction, int i, CallbackInfoReturnable> cir) { + startRumble(state); + } + + @Inject(method = "method_41930", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/multiplayer/MultiPlayerGameMode;destroyBlock(Lnet/minecraft/core/BlockPos;)Z")) + private void onInstabreakBlockSurvival(BlockState state, BlockPos pos, Direction direction, int i, CallbackInfoReturnable cir) { + startRumble(state); + // won't stop until 1 tick + stopRumble(); + } + + @Inject(method = "stopDestroyBlock", at = @At("RETURN")) + private void onStopBreakingBlock(CallbackInfo ci) { + stopRumble(); + } + + @Inject(method = "continueDestroyBlock", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/multiplayer/MultiPlayerGameMode;startPrediction(Lnet/minecraft/client/multiplayer/ClientLevel;Lnet/minecraft/client/multiplayer/prediction/PredictiveAction;)V", ordinal = 1, shift = At.Shift.BEFORE)) + private void onFinishBreakingBlock(BlockPos pos, Direction direction, CallbackInfoReturnable cir) { + stopRumble(); + } + + @ModifyExpressionValue(method = "continueDestroyBlock", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/block/state/BlockState;isAir()Z")) + private boolean onAbortBreakingBlock(boolean original) { + if (original) + stopRumble(); + + return original; + } + + private void startRumble(BlockState state) { + ContinuousRumbleEffect effect = new ContinuousRumbleEffect(tick -> + new RumbleState( + 0.02f + Math.min(1, state.getBlock().defaultDestroyTime() / 20f) * 0.25f, + 0.15f + ) + ){ + @Override + public boolean isFinished() { + // insta-break blocks will stop the same tick it starts, so it must not stop until 1 tick has played + return super.isFinished() && currentTick() > 0; + } + }; + + blockBreakRumble = effect; + ControlifyApi.get().currentController().rumbleManager().play(effect); + } + + private void stopRumble() { + if (blockBreakRumble != null) { + blockBreakRumble.stop(); + blockBreakRumble = null; + } + } +} diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/rumble/damage/LocalPlayerMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/rumble/damage/LocalPlayerMixin.java index 4f58daf..0e42f5f 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/rumble/damage/LocalPlayerMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/rumble/damage/LocalPlayerMixin.java @@ -1,31 +1,44 @@ package dev.isxander.controlify.mixins.feature.rumble.damage; +import com.mojang.authlib.GameProfile; import dev.isxander.controlify.api.ControlifyApi; -import dev.isxander.controlify.rumble.RumbleEffect; +import dev.isxander.controlify.rumble.BasicRumbleEffect; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.player.AbstractClientPlayer; import net.minecraft.client.player.LocalPlayer; +import net.minecraft.util.Mth; import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @Mixin(LocalPlayer.class) -public class LocalPlayerMixin { - @Inject(method = "hurtTo", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/player/LocalPlayer;setHealth(F)V", ordinal = 1)) - private void onClientHurt(float health, CallbackInfo ci) { - // LivingEntity#hurt is server-side only, so we do it here - doRumble(); +public abstract class LocalPlayerMixin extends AbstractClientPlayer { + @Unique private float lastHealth = getHealth(); + @Unique private boolean skipTick = true; + + public LocalPlayerMixin(ClientLevel world, GameProfile profile) { + super(world, profile); } - @Inject(method = "hurtTo", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/player/LocalPlayer;setHealth(F)V", ordinal = 0)) - private void onClientHealthUpdate(float health, CallbackInfo ci) { - // for some reason fall damage calls hurtTo after the health has been updated at some point - // this is called when hurtTo is set to the same health as the player already has - doRumble(); - } + @Inject(method = "tick", at = @At("HEAD")) + private void checkHealthTick(CallbackInfo ci) { + var damageTaken = Math.max(0, lastHealth - getHealth()); + lastHealth = getHealth(); - private void doRumble() { - ControlifyApi.get().currentController().rumbleManager().play( - RumbleEffect.constant(0.5f, 0f, 5) - ); + if (damageTaken > 0 && !skipTick) { + float minMagnitude = 0.4f; + float smallestDamage = 2; // the damage that results in minMagnitude + float maxDamage = 15; // the damage that results in magnitude 1.0f + + float magnitude = (Mth.clamp(damageTaken, smallestDamage, maxDamage) - smallestDamage) / (maxDamage - smallestDamage) * (1 - minMagnitude) + minMagnitude; + System.out.println(magnitude); + ControlifyApi.get().currentController().rumbleManager().play( + BasicRumbleEffect.constant(magnitude, 0f, magnitude >= 0.75f ? 8 : 5) + ); + } + // skip first tick from spawn + skipTick = false; } } diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/rumble/itembreak/LivingEntityMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/rumble/itembreak/LivingEntityMixin.java new file mode 100644 index 0000000..e4625bc --- /dev/null +++ b/src/main/java/dev/isxander/controlify/mixins/feature/rumble/itembreak/LivingEntityMixin.java @@ -0,0 +1,16 @@ +package dev.isxander.controlify.mixins.feature.rumble.itembreak; + +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.item.ItemStack; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(LivingEntity.class) +public class LivingEntityMixin { + @Inject(method = "breakItem", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/LivingEntity;spawnItemParticles(Lnet/minecraft/world/item/ItemStack;I)V")) + protected void onBreakItemParticles(ItemStack stack, CallbackInfo ci) { + + } +} diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/rumble/itembreak/LocalPlayerMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/rumble/itembreak/LocalPlayerMixin.java new file mode 100644 index 0000000..611ef58 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/mixins/feature/rumble/itembreak/LocalPlayerMixin.java @@ -0,0 +1,20 @@ +package dev.isxander.controlify.mixins.feature.rumble.itembreak; + +import dev.isxander.controlify.api.ControlifyApi; +import dev.isxander.controlify.rumble.BasicRumbleEffect; +import dev.isxander.controlify.rumble.RumbleEffect; +import dev.isxander.controlify.rumble.RumbleState; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.world.item.ItemStack; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(LocalPlayer.class) +public class LocalPlayerMixin extends LivingEntityMixin { + @Override + protected void onBreakItemParticles(ItemStack stack, CallbackInfo ci) { + ControlifyApi.get().currentController().rumbleManager().play( + BasicRumbleEffect.byTick(tick -> new RumbleState(tick <= 4 ? 1f : 0f, 1f), 10) + ); + } +} diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/rumble/sounds/LevelRendererMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/rumble/sounds/LevelRendererMixin.java index 1778868..999cde5 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/rumble/sounds/LevelRendererMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/rumble/sounds/LevelRendererMixin.java @@ -1,6 +1,7 @@ package dev.isxander.controlify.mixins.feature.rumble.sounds; import dev.isxander.controlify.api.ControlifyApi; +import dev.isxander.controlify.rumble.BasicRumbleEffect; import dev.isxander.controlify.rumble.RumbleEffect; import dev.isxander.controlify.rumble.RumbleState; import net.minecraft.client.renderer.LevelRenderer; @@ -17,9 +18,9 @@ public class LevelRendererMixin { private void onLevelEvent(int eventId, BlockPos pos, int data, CallbackInfo ci) { switch (eventId) { case LevelEvent.SOUND_ANVIL_USED -> rumble( - RumbleEffect.join( - RumbleEffect.constant(1f, 0.5f, 2), - RumbleEffect.empty(5) + BasicRumbleEffect.join( + BasicRumbleEffect.constant(1f, 0.5f, 2), + BasicRumbleEffect.empty(5) ).repeat(3) ); } @@ -29,9 +30,9 @@ public class LevelRendererMixin { private void onGlobalLevelEvent(int eventId, BlockPos pos, int data, CallbackInfo ci) { switch (eventId) { case LevelEvent.SOUND_DRAGON_DEATH -> rumble( - RumbleEffect.join( - RumbleEffect.constant(1f, 1f, 194), - RumbleEffect.byTime(t -> { + BasicRumbleEffect.join( + BasicRumbleEffect.constant(1f, 1f, 194), + BasicRumbleEffect.byTime(t -> { float easeOutQuad = 1 - (1 - t) * (1 - t); return new RumbleState(1 - easeOutQuad, 1 - easeOutQuad); }, 63) diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/rumble/useitem/LivingEntityMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/rumble/useitem/LivingEntityMixin.java index c9f5216..4f12cb1 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/rumble/useitem/LivingEntityMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/rumble/useitem/LivingEntityMixin.java @@ -1,9 +1,7 @@ package dev.isxander.controlify.mixins.feature.rumble.useitem; -import dev.isxander.controlify.api.ControlifyApi; -import dev.isxander.controlify.rumble.RumbleEffect; -import net.minecraft.client.player.LocalPlayer; -import net.minecraft.util.Mth; +import com.llamalad7.mixinextras.sugar.Local; +import net.minecraft.world.InteractionHand; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.item.ItemStack; import org.spongepowered.asm.mixin.Mixin; @@ -16,6 +14,16 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; public abstract class LivingEntityMixin { @Shadow public abstract int getUseItemRemainingTicks(); + @Inject(method = "startUsingItem", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/item/ItemStack;getUseDuration()I")) + protected void onStartUsingItem(InteractionHand hand, CallbackInfo ci, @Local ItemStack stack) { + + } + + @Inject(method = "stopUsingItem", at = @At("HEAD")) + protected void onStopUsingItem(CallbackInfo ci) { + + } + @Inject(method = "updateUsingItem", at = @At("HEAD")) protected void onUpdateUsingItem(ItemStack stack, CallbackInfo ci) { diff --git a/src/main/java/dev/isxander/controlify/mixins/feature/rumble/useitem/LocalPlayerMixin.java b/src/main/java/dev/isxander/controlify/mixins/feature/rumble/useitem/LocalPlayerMixin.java index bcedef7..e98a206 100644 --- a/src/main/java/dev/isxander/controlify/mixins/feature/rumble/useitem/LocalPlayerMixin.java +++ b/src/main/java/dev/isxander/controlify/mixins/feature/rumble/useitem/LocalPlayerMixin.java @@ -1,29 +1,55 @@ package dev.isxander.controlify.mixins.feature.rumble.useitem; import dev.isxander.controlify.api.ControlifyApi; -import dev.isxander.controlify.rumble.RumbleEffect; +import dev.isxander.controlify.rumble.BasicRumbleEffect; +import dev.isxander.controlify.rumble.ContinuousRumbleEffect; +import dev.isxander.controlify.rumble.RumbleState; import net.minecraft.client.player.LocalPlayer; -import net.minecraft.util.Mth; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.item.BowItem; import net.minecraft.world.item.ItemStack; import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @Mixin(LocalPlayer.class) public abstract class LocalPlayerMixin extends LivingEntityMixin { + @Unique private ContinuousRumbleEffect useItemRumble; + @Override - protected void onUpdateUsingItem(ItemStack stack, CallbackInfo ci) { + protected void onStartUsingItem(InteractionHand hand, CallbackInfo ci, ItemStack stack) { switch (stack.getUseAnimation()) { - case BOW, CROSSBOW, SPEAR -> { - var magnitude = Mth.clamp((stack.getUseDuration() - getUseItemRemainingTicks()) / 20f, 0f, 1f) * 0.5f; - playRumble(RumbleEffect.constant(magnitude * 0.3f, magnitude, 1)); - } - case BLOCK, SPYGLASS -> playRumble(RumbleEffect.constant(0f, 0.1f, 1)); - case EAT, DRINK -> playRumble(RumbleEffect.constant(0.05f, 0.1f, 1)); - case TOOT_HORN -> playRumble(RumbleEffect.constant(1f, 0.25f, 1)); + case BOW, CROSSBOW, SPEAR -> + startRumble(new ContinuousRumbleEffect(tick -> + new RumbleState(tick % 7 <= 3 && tick > BowItem.MAX_DRAW_DURATION ? 0.1f : 0f, BowItem.getPowerForTime(tick)) + )); + case BLOCK, SPYGLASS -> + startRumble(new ContinuousRumbleEffect(tick -> + new RumbleState(0f, tick % 4 / 4f * 0.12f + 0.05f) + )); + case EAT, DRINK -> startRumble(BasicRumbleEffect.constant(0.05f, 0.1f, 1).continuous()); + case TOOT_HORN -> + startRumble(new ContinuousRumbleEffect(tick -> + new RumbleState(Math.min(1f, tick / 10f), 0.25f) + )); } } - private void playRumble(RumbleEffect effect) { + @Override + protected void onUpdateUsingItem(ItemStack stack, CallbackInfo ci) { + + } + + @Override + protected void onStopUsingItem(CallbackInfo ci) { + if (useItemRumble != null) { + useItemRumble.stop(); + useItemRumble = null; + } + } + + private void startRumble(ContinuousRumbleEffect effect) { ControlifyApi.get().currentController().rumbleManager().play(effect); + useItemRumble = effect; } } diff --git a/src/main/java/dev/isxander/controlify/rumble/BasicRumbleEffect.java b/src/main/java/dev/isxander/controlify/rumble/BasicRumbleEffect.java new file mode 100644 index 0000000..710aae6 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/rumble/BasicRumbleEffect.java @@ -0,0 +1,145 @@ +package dev.isxander.controlify.rumble; + +import org.apache.commons.lang3.Validate; + +import java.util.Arrays; +import java.util.Objects; +import java.util.function.Function; + +public final class BasicRumbleEffect implements RumbleEffect { + private final RumbleState[] keyframes; + private int tick = 0; + private boolean finished; + private int priority = 0; + + public BasicRumbleEffect(RumbleState[] keyframes) { + this.keyframes = keyframes; + } + + @Override + public RumbleState nextState() { + tick++; + if (tick >= keyframes.length) + finished = true; + + return keyframes[tick - 1]; + } + + @Override + public boolean isFinished() { + return finished; + } + + @Override + public int priority() { + return priority; + } + + public BasicRumbleEffect prioritised(int priority) { + this.priority = priority; + return this; + } + + public RumbleState[] states() { + return keyframes; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (obj == null || obj.getClass() != this.getClass()) return false; + var that = (BasicRumbleEffect) obj; + return Arrays.equals(this.states(), that.states()) + && this.priority() == that.priority(); + } + + @Override + public int hashCode() { + return Objects.hash(Arrays.hashCode(this.states()), this.priority()); + } + + @Override + public String toString() { + return "RumbleEffect[" + + "states=" + Arrays.toString(this.states()) + ',' + + "priority=" + this.priority() + ']'; + } + + /** + * Creates a rumble effect where the state is determined by the tick. + * + * @param stateFunction the function that takes a tick and returns the state for that tick. + * @param durationTicks how many ticks the effect should last for. + */ + public static BasicRumbleEffect byTick(Function stateFunction, int durationTicks) { + RumbleState[] states = new RumbleState[durationTicks]; + for (int i = 0; i < durationTicks; i++) { + states[i] = stateFunction.apply(i); + } + return new BasicRumbleEffect(states); + } + + /** + * Creates a rumble effect from a function that takes a time value from 0, start, to 1, end, and returns that tick. + * + * @param stateFunction the function that takes the time value and returns the state for that tick. + * @param durationTicks how many ticks the effect should last for. + */ + public static BasicRumbleEffect byTime(Function stateFunction, int durationTicks) { + return BasicRumbleEffect.byTick(tick -> stateFunction.apply((float) tick / (float) durationTicks), durationTicks); + } + + /** + * Creates a rumble effect that has a constant state. + * + * @param strong the strong motor magnitude. + * @param weak the weak motor magnitude + * @param durationTicks how many ticks the effect should last for. + */ + public static BasicRumbleEffect constant(float strong, float weak, int durationTicks) { + return BasicRumbleEffect.byTick(tick -> new RumbleState(strong, weak), durationTicks); + } + + public static BasicRumbleEffect empty(int durationTicks) { + return BasicRumbleEffect.byTick(tick -> new RumbleState(0f, 0f), durationTicks); + } + + public static BasicRumbleEffect join(BasicRumbleEffect... effects) { + int totalTicks = 0; + for (BasicRumbleEffect effect : effects) { + totalTicks += effect.states().length; + } + + RumbleState[] states = new RumbleState[totalTicks]; + int currentTick = 0; + for (BasicRumbleEffect effect : effects) { + for (RumbleState state : effect.states()) { + states[currentTick] = state; + currentTick++; + } + } + + return new BasicRumbleEffect(states); + } + + public BasicRumbleEffect join(BasicRumbleEffect other) { + return BasicRumbleEffect.join(this, other); + } + + public BasicRumbleEffect repeat(int count) { + Validate.isTrue(count > 0, "count must be greater than 0"); + + if (count == 1) return this; + + BasicRumbleEffect effect = this; + for (int i = 0; i < count - 1; i++) { + effect = BasicRumbleEffect.join(effect, this); + } + return effect; + } + + public ContinuousRumbleEffect continuous() { + int lastIndex = this.states().length - 1; + return new ContinuousRumbleEffect(index -> this.states()[index % lastIndex], this.priority()); + } +} diff --git a/src/main/java/dev/isxander/controlify/rumble/ContinuousRumbleEffect.java b/src/main/java/dev/isxander/controlify/rumble/ContinuousRumbleEffect.java new file mode 100644 index 0000000..55149ad --- /dev/null +++ b/src/main/java/dev/isxander/controlify/rumble/ContinuousRumbleEffect.java @@ -0,0 +1,43 @@ +package dev.isxander.controlify.rumble; + +import java.util.function.Function; + +public class ContinuousRumbleEffect implements RumbleEffect { + private final Function stateFunction; + private final int priority; + private int tick; + private boolean stopped; + + public ContinuousRumbleEffect(Function stateFunction) { + this(stateFunction, 0); + } + + public ContinuousRumbleEffect(Function stateFunction, int priority) { + this.stateFunction = stateFunction; + this.priority = priority; + } + + @Override + public RumbleState nextState() { + tick++; + return stateFunction.apply(tick - 1); + } + + public void stop() { + stopped = true; + } + + public int currentTick() { + return tick; + } + + @Override + public boolean isFinished() { + return stopped; + } + + @Override + public int priority() { + return this.priority; + } +} diff --git a/src/main/java/dev/isxander/controlify/rumble/RumbleEffect.java b/src/main/java/dev/isxander/controlify/rumble/RumbleEffect.java index be97ef5..d7ca8d0 100644 --- a/src/main/java/dev/isxander/controlify/rumble/RumbleEffect.java +++ b/src/main/java/dev/isxander/controlify/rumble/RumbleEffect.java @@ -1,77 +1,9 @@ package dev.isxander.controlify.rumble; -import org.apache.commons.lang3.Validate; +public interface RumbleEffect { + RumbleState nextState(); -import java.util.function.Function; + boolean isFinished(); -public record RumbleEffect(RumbleState[] states) { - /** - * Creates a rumble effect where the state is determined by the tick. - * @param stateFunction the function that takes a tick and returns the state for that tick. - * @param durationTicks how many ticks the effect should last for. - */ - public static RumbleEffect byTick(Function stateFunction, int durationTicks) { - RumbleState[] states = new RumbleState[durationTicks]; - for (int i = 0; i < durationTicks; i++) { - states[i] = stateFunction.apply(i); - } - return new RumbleEffect(states); - } - - /** - * Creates a rumble effect from a function that takes a time value from 0, start, to 1, end, and returns that tick. - * @param stateFunction the function that takes the time value and returns the state for that tick. - * @param durationTicks how many ticks the effect should last for. - */ - public static RumbleEffect byTime(Function stateFunction, int durationTicks) { - return RumbleEffect.byTick(tick -> stateFunction.apply((float) tick / (float) durationTicks), durationTicks); - } - - /** - * Creates a rumble effect that has a constant state. - * @param strong the strong motor magnitude. - * @param weak the weak motor magnitude - * @param durationTicks how many ticks the effect should last for. - */ - public static RumbleEffect constant(float strong, float weak, int durationTicks) { - return RumbleEffect.byTick(tick -> new RumbleState(strong, weak), durationTicks); - } - - public static RumbleEffect empty(int durationTicks) { - return RumbleEffect.byTick(tick -> new RumbleState(0f, 0f), durationTicks); - } - - public static RumbleEffect join(RumbleEffect... effects) { - int totalTicks = 0; - for (RumbleEffect effect : effects) { - totalTicks += effect.states().length; - } - - RumbleState[] states = new RumbleState[totalTicks]; - int currentTick = 0; - for (RumbleEffect effect : effects) { - for (RumbleState state : effect.states()) { - states[currentTick] = state; - currentTick++; - } - } - - return new RumbleEffect(states); - } - - public RumbleEffect join(RumbleEffect other) { - return RumbleEffect.join(this, other); - } - - public RumbleEffect repeat(int count) { - Validate.isTrue(count > 0, "count must be greater than 0"); - - if (count == 1) return this; - - RumbleEffect effect = this; - for (int i = 0; i < count - 1; i++) { - effect = RumbleEffect.join(effect, this); - } - return effect; - } + int priority(); } diff --git a/src/main/java/dev/isxander/controlify/rumble/RumbleManager.java b/src/main/java/dev/isxander/controlify/rumble/RumbleManager.java index 90f84f2..7611002 100644 --- a/src/main/java/dev/isxander/controlify/rumble/RumbleManager.java +++ b/src/main/java/dev/isxander/controlify/rumble/RumbleManager.java @@ -3,7 +3,6 @@ package dev.isxander.controlify.rumble; public class RumbleManager { private final RumbleCapable controller; private RumbleEffect playingEffect; - private int currentPlayingTick; public RumbleManager(RumbleCapable controller) { this.controller = controller; @@ -14,7 +13,6 @@ public class RumbleManager { return; playingEffect = effect; - currentPlayingTick = 0; } public boolean isPlaying() { @@ -27,20 +25,18 @@ public class RumbleManager { controller.setRumble(0f, 0f); playingEffect = null; - currentPlayingTick = 0; } public void tick() { if (playingEffect == null) return; - if (currentPlayingTick >= playingEffect.states().length) { + if (playingEffect.isFinished()) { stopCurrentEffect(); return; } - RumbleState state = playingEffect.states()[currentPlayingTick]; + RumbleState state = playingEffect.nextState(); controller.setRumble(state.strong(), state.weak()); - currentPlayingTick++; } } diff --git a/src/main/java/dev/isxander/controlify/utils/Easings.java b/src/main/java/dev/isxander/controlify/utils/Easings.java new file mode 100644 index 0000000..7570be5 --- /dev/null +++ b/src/main/java/dev/isxander/controlify/utils/Easings.java @@ -0,0 +1,11 @@ +package dev.isxander.controlify.utils; + +public class Easings { + public static float easeInQuad(float t) { + return t * t; + } + + public static float easeOutQuad(float t) { + return 1 - (1 - t) * (1 - t); + } +} diff --git a/src/main/resources/controlify.mixins.json b/src/main/resources/controlify.mixins.json index fcc8961..63847cf 100644 --- a/src/main/resources/controlify.mixins.json +++ b/src/main/resources/controlify.mixins.json @@ -9,6 +9,7 @@ "compat.sodium.SliderControlElementMixin", "compat.sodium.TickBoxControlElementMixin", "core.GLXMixin", + "feature.rumble.itembreak.LivingEntityMixin", "feature.rumble.sounds.LevelRendererMixin", "feature.rumble.useitem.LivingEntityMixin" ], @@ -35,7 +36,9 @@ "feature.guide.screen.AbstractWidgetMixin", "feature.guide.screen.TabNavigationBarMixin", "feature.reacharound.GameRendererMixin", + "feature.rumble.blockbreak.MultiPlayerGameModeMixin", "feature.rumble.damage.LocalPlayerMixin", + "feature.rumble.itembreak.LocalPlayerMixin", "feature.rumble.useitem.LocalPlayerMixin", "feature.screenop.MinecraftMixin", "feature.screenop.ScreenMixin",