forked from Clones/Controlify
more rumble effects
- new continuous rumble - block break rumble - item break rumble - fix damage rumble
This commit is contained in:
@ -15,7 +15,7 @@ import dev.isxander.controlify.controller.joystick.SingleJoystickController;
|
|||||||
import dev.isxander.controlify.controller.joystick.JoystickState;
|
import dev.isxander.controlify.controller.joystick.JoystickState;
|
||||||
import dev.isxander.controlify.gui.screen.ControllerDeadzoneCalibrationScreen;
|
import dev.isxander.controlify.gui.screen.ControllerDeadzoneCalibrationScreen;
|
||||||
import dev.isxander.controlify.reacharound.ReachAroundMode;
|
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.controlify.rumble.RumbleState;
|
||||||
import dev.isxander.yacl.api.*;
|
import dev.isxander.yacl.api.*;
|
||||||
import dev.isxander.yacl.gui.controllers.ActionController;
|
import dev.isxander.yacl.gui.controllers.ActionController;
|
||||||
@ -294,13 +294,16 @@ public class YACLHelper {
|
|||||||
.controller(ActionController::new)
|
.controller(ActionController::new)
|
||||||
.action((screen, btn) -> {
|
.action((screen, btn) -> {
|
||||||
controller.rumbleManager().play(
|
controller.rumbleManager().play(
|
||||||
RumbleEffect.byTime(t -> new RumbleState(0f, t), 20)
|
BasicRumbleEffect.byTime(t -> new RumbleState(0f, t), 20)
|
||||||
.join(RumbleEffect.byTime(t -> new RumbleState(0f, 1 - t), 20))
|
.join(BasicRumbleEffect.byTime(t -> new RumbleState(0f, 1 - t), 20))
|
||||||
.repeat(3)
|
.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());
|
category.group(advancedGroup.build());
|
||||||
|
|
||||||
|
@ -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<Packet<?>> 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<Packet> 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<Boolean> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,31 +1,44 @@
|
|||||||
package dev.isxander.controlify.mixins.feature.rumble.damage;
|
package dev.isxander.controlify.mixins.feature.rumble.damage;
|
||||||
|
|
||||||
|
import com.mojang.authlib.GameProfile;
|
||||||
import dev.isxander.controlify.api.ControlifyApi;
|
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.client.player.LocalPlayer;
|
||||||
|
import net.minecraft.util.Mth;
|
||||||
import org.spongepowered.asm.mixin.Mixin;
|
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.At;
|
||||||
import org.spongepowered.asm.mixin.injection.Inject;
|
import org.spongepowered.asm.mixin.injection.Inject;
|
||||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||||
|
|
||||||
@Mixin(LocalPlayer.class)
|
@Mixin(LocalPlayer.class)
|
||||||
public class LocalPlayerMixin {
|
public abstract class LocalPlayerMixin extends AbstractClientPlayer {
|
||||||
@Inject(method = "hurtTo", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/player/LocalPlayer;setHealth(F)V", ordinal = 1))
|
@Unique private float lastHealth = getHealth();
|
||||||
private void onClientHurt(float health, CallbackInfo ci) {
|
@Unique private boolean skipTick = true;
|
||||||
// LivingEntity#hurt is server-side only, so we do it here
|
|
||||||
doRumble();
|
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))
|
@Inject(method = "tick", at = @At("HEAD"))
|
||||||
private void onClientHealthUpdate(float health, CallbackInfo ci) {
|
private void checkHealthTick(CallbackInfo ci) {
|
||||||
// for some reason fall damage calls hurtTo after the health has been updated at some point
|
var damageTaken = Math.max(0, lastHealth - getHealth());
|
||||||
// this is called when hurtTo is set to the same health as the player already has
|
lastHealth = getHealth();
|
||||||
doRumble();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void doRumble() {
|
if (damageTaken > 0 && !skipTick) {
|
||||||
ControlifyApi.get().currentController().rumbleManager().play(
|
float minMagnitude = 0.4f;
|
||||||
RumbleEffect.constant(0.5f, 0f, 5)
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
package dev.isxander.controlify.mixins.feature.rumble.sounds;
|
package dev.isxander.controlify.mixins.feature.rumble.sounds;
|
||||||
|
|
||||||
import dev.isxander.controlify.api.ControlifyApi;
|
import dev.isxander.controlify.api.ControlifyApi;
|
||||||
|
import dev.isxander.controlify.rumble.BasicRumbleEffect;
|
||||||
import dev.isxander.controlify.rumble.RumbleEffect;
|
import dev.isxander.controlify.rumble.RumbleEffect;
|
||||||
import dev.isxander.controlify.rumble.RumbleState;
|
import dev.isxander.controlify.rumble.RumbleState;
|
||||||
import net.minecraft.client.renderer.LevelRenderer;
|
import net.minecraft.client.renderer.LevelRenderer;
|
||||||
@ -17,9 +18,9 @@ public class LevelRendererMixin {
|
|||||||
private void onLevelEvent(int eventId, BlockPos pos, int data, CallbackInfo ci) {
|
private void onLevelEvent(int eventId, BlockPos pos, int data, CallbackInfo ci) {
|
||||||
switch (eventId) {
|
switch (eventId) {
|
||||||
case LevelEvent.SOUND_ANVIL_USED -> rumble(
|
case LevelEvent.SOUND_ANVIL_USED -> rumble(
|
||||||
RumbleEffect.join(
|
BasicRumbleEffect.join(
|
||||||
RumbleEffect.constant(1f, 0.5f, 2),
|
BasicRumbleEffect.constant(1f, 0.5f, 2),
|
||||||
RumbleEffect.empty(5)
|
BasicRumbleEffect.empty(5)
|
||||||
).repeat(3)
|
).repeat(3)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -29,9 +30,9 @@ public class LevelRendererMixin {
|
|||||||
private void onGlobalLevelEvent(int eventId, BlockPos pos, int data, CallbackInfo ci) {
|
private void onGlobalLevelEvent(int eventId, BlockPos pos, int data, CallbackInfo ci) {
|
||||||
switch (eventId) {
|
switch (eventId) {
|
||||||
case LevelEvent.SOUND_DRAGON_DEATH -> rumble(
|
case LevelEvent.SOUND_DRAGON_DEATH -> rumble(
|
||||||
RumbleEffect.join(
|
BasicRumbleEffect.join(
|
||||||
RumbleEffect.constant(1f, 1f, 194),
|
BasicRumbleEffect.constant(1f, 1f, 194),
|
||||||
RumbleEffect.byTime(t -> {
|
BasicRumbleEffect.byTime(t -> {
|
||||||
float easeOutQuad = 1 - (1 - t) * (1 - t);
|
float easeOutQuad = 1 - (1 - t) * (1 - t);
|
||||||
return new RumbleState(1 - easeOutQuad, 1 - easeOutQuad);
|
return new RumbleState(1 - easeOutQuad, 1 - easeOutQuad);
|
||||||
}, 63)
|
}, 63)
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
package dev.isxander.controlify.mixins.feature.rumble.useitem;
|
package dev.isxander.controlify.mixins.feature.rumble.useitem;
|
||||||
|
|
||||||
import dev.isxander.controlify.api.ControlifyApi;
|
import com.llamalad7.mixinextras.sugar.Local;
|
||||||
import dev.isxander.controlify.rumble.RumbleEffect;
|
import net.minecraft.world.InteractionHand;
|
||||||
import net.minecraft.client.player.LocalPlayer;
|
|
||||||
import net.minecraft.util.Mth;
|
|
||||||
import net.minecraft.world.entity.LivingEntity;
|
import net.minecraft.world.entity.LivingEntity;
|
||||||
import net.minecraft.world.item.ItemStack;
|
import net.minecraft.world.item.ItemStack;
|
||||||
import org.spongepowered.asm.mixin.Mixin;
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
@ -16,6 +14,16 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
|||||||
public abstract class LivingEntityMixin {
|
public abstract class LivingEntityMixin {
|
||||||
@Shadow public abstract int getUseItemRemainingTicks();
|
@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"))
|
@Inject(method = "updateUsingItem", at = @At("HEAD"))
|
||||||
protected void onUpdateUsingItem(ItemStack stack, CallbackInfo ci) {
|
protected void onUpdateUsingItem(ItemStack stack, CallbackInfo ci) {
|
||||||
|
|
||||||
|
@ -1,29 +1,55 @@
|
|||||||
package dev.isxander.controlify.mixins.feature.rumble.useitem;
|
package dev.isxander.controlify.mixins.feature.rumble.useitem;
|
||||||
|
|
||||||
import dev.isxander.controlify.api.ControlifyApi;
|
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.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 net.minecraft.world.item.ItemStack;
|
||||||
import org.spongepowered.asm.mixin.Mixin;
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.Unique;
|
||||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||||
|
|
||||||
@Mixin(LocalPlayer.class)
|
@Mixin(LocalPlayer.class)
|
||||||
public abstract class LocalPlayerMixin extends LivingEntityMixin {
|
public abstract class LocalPlayerMixin extends LivingEntityMixin {
|
||||||
|
@Unique private ContinuousRumbleEffect useItemRumble;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onUpdateUsingItem(ItemStack stack, CallbackInfo ci) {
|
protected void onStartUsingItem(InteractionHand hand, CallbackInfo ci, ItemStack stack) {
|
||||||
switch (stack.getUseAnimation()) {
|
switch (stack.getUseAnimation()) {
|
||||||
case BOW, CROSSBOW, SPEAR -> {
|
case BOW, CROSSBOW, SPEAR ->
|
||||||
var magnitude = Mth.clamp((stack.getUseDuration() - getUseItemRemainingTicks()) / 20f, 0f, 1f) * 0.5f;
|
startRumble(new ContinuousRumbleEffect(tick ->
|
||||||
playRumble(RumbleEffect.constant(magnitude * 0.3f, magnitude, 1));
|
new RumbleState(tick % 7 <= 3 && tick > BowItem.MAX_DRAW_DURATION ? 0.1f : 0f, BowItem.getPowerForTime(tick))
|
||||||
}
|
));
|
||||||
case BLOCK, SPYGLASS -> playRumble(RumbleEffect.constant(0f, 0.1f, 1));
|
case BLOCK, SPYGLASS ->
|
||||||
case EAT, DRINK -> playRumble(RumbleEffect.constant(0.05f, 0.1f, 1));
|
startRumble(new ContinuousRumbleEffect(tick ->
|
||||||
case TOOT_HORN -> playRumble(RumbleEffect.constant(1f, 0.25f, 1));
|
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);
|
ControlifyApi.get().currentController().rumbleManager().play(effect);
|
||||||
|
useItemRumble = effect;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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<Integer, RumbleState> 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<Float, RumbleState> 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());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
package dev.isxander.controlify.rumble;
|
||||||
|
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
public class ContinuousRumbleEffect implements RumbleEffect {
|
||||||
|
private final Function<Integer, RumbleState> stateFunction;
|
||||||
|
private final int priority;
|
||||||
|
private int tick;
|
||||||
|
private boolean stopped;
|
||||||
|
|
||||||
|
public ContinuousRumbleEffect(Function<Integer, RumbleState> stateFunction) {
|
||||||
|
this(stateFunction, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ContinuousRumbleEffect(Function<Integer, RumbleState> 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;
|
||||||
|
}
|
||||||
|
}
|
@ -1,77 +1,9 @@
|
|||||||
package dev.isxander.controlify.rumble;
|
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) {
|
int 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 RumbleEffect byTick(Function<Integer, RumbleState> 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<Float, RumbleState> 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,6 @@ package dev.isxander.controlify.rumble;
|
|||||||
public class RumbleManager {
|
public class RumbleManager {
|
||||||
private final RumbleCapable controller;
|
private final RumbleCapable controller;
|
||||||
private RumbleEffect playingEffect;
|
private RumbleEffect playingEffect;
|
||||||
private int currentPlayingTick;
|
|
||||||
|
|
||||||
public RumbleManager(RumbleCapable controller) {
|
public RumbleManager(RumbleCapable controller) {
|
||||||
this.controller = controller;
|
this.controller = controller;
|
||||||
@ -14,7 +13,6 @@ public class RumbleManager {
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
playingEffect = effect;
|
playingEffect = effect;
|
||||||
currentPlayingTick = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isPlaying() {
|
public boolean isPlaying() {
|
||||||
@ -27,20 +25,18 @@ public class RumbleManager {
|
|||||||
|
|
||||||
controller.setRumble(0f, 0f);
|
controller.setRumble(0f, 0f);
|
||||||
playingEffect = null;
|
playingEffect = null;
|
||||||
currentPlayingTick = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void tick() {
|
public void tick() {
|
||||||
if (playingEffect == null)
|
if (playingEffect == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (currentPlayingTick >= playingEffect.states().length) {
|
if (playingEffect.isFinished()) {
|
||||||
stopCurrentEffect();
|
stopCurrentEffect();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
RumbleState state = playingEffect.states()[currentPlayingTick];
|
RumbleState state = playingEffect.nextState();
|
||||||
controller.setRumble(state.strong(), state.weak());
|
controller.setRumble(state.strong(), state.weak());
|
||||||
currentPlayingTick++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
11
src/main/java/dev/isxander/controlify/utils/Easings.java
Normal file
11
src/main/java/dev/isxander/controlify/utils/Easings.java
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -9,6 +9,7 @@
|
|||||||
"compat.sodium.SliderControlElementMixin",
|
"compat.sodium.SliderControlElementMixin",
|
||||||
"compat.sodium.TickBoxControlElementMixin",
|
"compat.sodium.TickBoxControlElementMixin",
|
||||||
"core.GLXMixin",
|
"core.GLXMixin",
|
||||||
|
"feature.rumble.itembreak.LivingEntityMixin",
|
||||||
"feature.rumble.sounds.LevelRendererMixin",
|
"feature.rumble.sounds.LevelRendererMixin",
|
||||||
"feature.rumble.useitem.LivingEntityMixin"
|
"feature.rumble.useitem.LivingEntityMixin"
|
||||||
],
|
],
|
||||||
@ -35,7 +36,9 @@
|
|||||||
"feature.guide.screen.AbstractWidgetMixin",
|
"feature.guide.screen.AbstractWidgetMixin",
|
||||||
"feature.guide.screen.TabNavigationBarMixin",
|
"feature.guide.screen.TabNavigationBarMixin",
|
||||||
"feature.reacharound.GameRendererMixin",
|
"feature.reacharound.GameRendererMixin",
|
||||||
|
"feature.rumble.blockbreak.MultiPlayerGameModeMixin",
|
||||||
"feature.rumble.damage.LocalPlayerMixin",
|
"feature.rumble.damage.LocalPlayerMixin",
|
||||||
|
"feature.rumble.itembreak.LocalPlayerMixin",
|
||||||
"feature.rumble.useitem.LocalPlayerMixin",
|
"feature.rumble.useitem.LocalPlayerMixin",
|
||||||
"feature.screenop.MinecraftMixin",
|
"feature.screenop.MinecraftMixin",
|
||||||
"feature.screenop.ScreenMixin",
|
"feature.screenop.ScreenMixin",
|
||||||
|
Reference in New Issue
Block a user