forked from Clones/Controlify
vibration conflict support - multiple vibrations can play at once
This commit is contained in:
@ -175,7 +175,7 @@ public class Controlify implements ControlifyApi {
|
|||||||
controller.updateState();
|
controller.updateState();
|
||||||
else {
|
else {
|
||||||
controller.clearState();
|
controller.clearState();
|
||||||
controller.rumbleManager().stopCurrentEffect();
|
controller.rumbleManager().clearEffects();
|
||||||
}
|
}
|
||||||
controller.rumbleManager().tick();
|
controller.rumbleManager().tick();
|
||||||
}
|
}
|
||||||
|
@ -53,18 +53,13 @@ public class MultiPlayerGameModeMixin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void startRumble(BlockState state) {
|
private void startRumble(BlockState state) {
|
||||||
ContinuousRumbleEffect effect = new ContinuousRumbleEffect(tick ->
|
var effect = ContinuousRumbleEffect.builder()
|
||||||
new RumbleState(
|
.byTick(tick -> new RumbleState(
|
||||||
0.02f + Easings.easeInQuad(Math.min(1, state.getBlock().defaultDestroyTime() / 20f)) * 0.25f,
|
0.02f + Easings.easeInQuad(Math.min(1, state.getBlock().defaultDestroyTime() / 20f)) * 0.25f,
|
||||||
0.01f
|
0.01f
|
||||||
)
|
))
|
||||||
){
|
.minTime(1)
|
||||||
@Override
|
.build();
|
||||||
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;
|
blockBreakRumble = effect;
|
||||||
ControlifyApi.get().currentController().rumbleManager().play(RumbleSource.BLOCK_DESTROY, effect);
|
ControlifyApi.get().currentController().rumbleManager().play(RumbleSource.BLOCK_DESTROY, effect);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package dev.isxander.controlify.mixins.feature.rumble.sounds;
|
package dev.isxander.controlify.mixins.feature.rumble.levelevents;
|
||||||
|
|
||||||
import dev.isxander.controlify.api.ControlifyApi;
|
import dev.isxander.controlify.api.ControlifyApi;
|
||||||
import dev.isxander.controlify.rumble.BasicRumbleEffect;
|
import dev.isxander.controlify.rumble.BasicRumbleEffect;
|
||||||
@ -40,7 +40,7 @@ public class LevelRendererMixin {
|
|||||||
float easeOutQuad = Easings.easeOutQuad(t);
|
float easeOutQuad = Easings.easeOutQuad(t);
|
||||||
return new RumbleState(1 - easeOutQuad, 1 - easeOutQuad);
|
return new RumbleState(1 - easeOutQuad, 1 - easeOutQuad);
|
||||||
}, 63)
|
}, 63)
|
||||||
)
|
).prioritised(10)
|
||||||
);
|
);
|
||||||
case LevelEvent.SOUND_WITHER_BOSS_SPAWN -> rumble(
|
case LevelEvent.SOUND_WITHER_BOSS_SPAWN -> rumble(
|
||||||
RumbleSource.GLOBAL_EVENT,
|
RumbleSource.GLOBAL_EVENT,
|
||||||
@ -51,7 +51,7 @@ public class LevelRendererMixin {
|
|||||||
float easeOutQuad = 1 - (1 - t) * (1 - t);
|
float easeOutQuad = 1 - (1 - t) * (1 - t);
|
||||||
return new RumbleState(0f, 1 - easeOutQuad);
|
return new RumbleState(0f, 1 - easeOutQuad);
|
||||||
}, 56)
|
}, 56)
|
||||||
)
|
).prioritised(10)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -8,6 +8,7 @@ import dev.isxander.controlify.rumble.RumbleState;
|
|||||||
import net.minecraft.client.player.LocalPlayer;
|
import net.minecraft.client.player.LocalPlayer;
|
||||||
import net.minecraft.world.InteractionHand;
|
import net.minecraft.world.InteractionHand;
|
||||||
import net.minecraft.world.item.BowItem;
|
import net.minecraft.world.item.BowItem;
|
||||||
|
import net.minecraft.world.item.CrossbowItem;
|
||||||
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.Unique;
|
||||||
@ -20,22 +21,37 @@ public abstract class LocalPlayerMixin extends LivingEntityMixin {
|
|||||||
@Override
|
@Override
|
||||||
protected void onStartUsingItem(InteractionHand hand, CallbackInfo ci, ItemStack stack) {
|
protected void onStartUsingItem(InteractionHand hand, CallbackInfo ci, ItemStack stack) {
|
||||||
switch (stack.getUseAnimation()) {
|
switch (stack.getUseAnimation()) {
|
||||||
case BOW, CROSSBOW, SPEAR ->
|
case BOW -> startRumble(ContinuousRumbleEffect.builder()
|
||||||
startRumble(new ContinuousRumbleEffect(tick ->
|
.byTick(tick -> new RumbleState(
|
||||||
new RumbleState(tick % 7 <= 3 && tick > BowItem.MAX_DRAW_DURATION ? 0.1f : 0f, BowItem.getPowerForTime(tick))
|
tick % 7 <= 3 && tick > BowItem.MAX_DRAW_DURATION ? 0.1f : 0f,
|
||||||
));
|
BowItem.getPowerForTime(tick)
|
||||||
case BLOCK, SPYGLASS ->
|
))
|
||||||
startRumble(new ContinuousRumbleEffect(tick ->
|
.build());
|
||||||
new RumbleState(0f, tick % 4 / 4f * 0.12f + 0.05f)
|
case CROSSBOW -> {
|
||||||
));
|
int chargeDuration = CrossbowItem.getChargeDuration(stack);
|
||||||
case EAT, DRINK ->
|
startRumble(ContinuousRumbleEffect.builder()
|
||||||
startRumble(new ContinuousRumbleEffect(tick ->
|
.byTick(tick -> new RumbleState(
|
||||||
new RumbleState(0.05f, 0.1f)
|
0f,
|
||||||
));
|
(float) tick / chargeDuration
|
||||||
case TOOT_HORN ->
|
))
|
||||||
startRumble(new ContinuousRumbleEffect(tick ->
|
.timeout(chargeDuration)
|
||||||
new RumbleState(Math.min(1f, tick / 10f), 0.25f)
|
.build());
|
||||||
));
|
}
|
||||||
|
case BLOCK, SPYGLASS -> startRumble(ContinuousRumbleEffect.builder()
|
||||||
|
.byTick(tick -> new RumbleState(
|
||||||
|
0f,
|
||||||
|
tick % 4 / 4f * 0.12f + 0.05f
|
||||||
|
))
|
||||||
|
.build());
|
||||||
|
case EAT, DRINK -> startRumble(ContinuousRumbleEffect.builder()
|
||||||
|
.constant(0.05f, 0.1f)
|
||||||
|
.build());
|
||||||
|
case TOOT_HORN -> startRumble(ContinuousRumbleEffect.builder()
|
||||||
|
.byTick(tick -> new RumbleState(
|
||||||
|
Math.min(1f, tick / 10f),
|
||||||
|
0.25f
|
||||||
|
))
|
||||||
|
.build());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,14 +17,25 @@ public final class BasicRumbleEffect implements RumbleEffect {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public RumbleState nextState() {
|
public void tick() {
|
||||||
tick++;
|
tick++;
|
||||||
if (tick >= keyframes.length)
|
if (tick >= keyframes.length)
|
||||||
finished = true;
|
finished = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RumbleState currentState() {
|
||||||
|
if (tick == 0)
|
||||||
|
throw new IllegalStateException("Effect hasn't ticked yet.");
|
||||||
|
|
||||||
return keyframes[tick - 1];
|
return keyframes[tick - 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int age() {
|
||||||
|
return tick;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isFinished() {
|
public boolean isFinished() {
|
||||||
return finished;
|
return finished;
|
||||||
@ -137,9 +148,4 @@ public final class BasicRumbleEffect implements RumbleEffect {
|
|||||||
}
|
}
|
||||||
return effect;
|
return effect;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ContinuousRumbleEffect continuous() {
|
|
||||||
int lastIndex = this.states().length - 1;
|
|
||||||
return new ContinuousRumbleEffect(index -> this.states()[index % lastIndex], this.priority());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,25 +1,34 @@
|
|||||||
package dev.isxander.controlify.rumble;
|
package dev.isxander.controlify.rumble;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.Validate;
|
||||||
|
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
public class ContinuousRumbleEffect implements RumbleEffect {
|
public class ContinuousRumbleEffect implements RumbleEffect {
|
||||||
private final Function<Integer, RumbleState> stateFunction;
|
private final Function<Integer, RumbleState> stateFunction;
|
||||||
private final int priority;
|
private final int priority;
|
||||||
|
private final int timeout;
|
||||||
|
private final int minTime;
|
||||||
private int tick;
|
private int tick;
|
||||||
private boolean stopped;
|
private boolean stopped;
|
||||||
|
|
||||||
public ContinuousRumbleEffect(Function<Integer, RumbleState> stateFunction) {
|
public ContinuousRumbleEffect(Function<Integer, RumbleState> stateFunction, int priority, int timeout, int minTime) {
|
||||||
this(stateFunction, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ContinuousRumbleEffect(Function<Integer, RumbleState> stateFunction, int priority) {
|
|
||||||
this.stateFunction = stateFunction;
|
this.stateFunction = stateFunction;
|
||||||
this.priority = priority;
|
this.priority = priority;
|
||||||
|
this.timeout = timeout;
|
||||||
|
this.minTime = minTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public RumbleState nextState() {
|
public void tick() {
|
||||||
tick++;
|
tick++;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RumbleState currentState() {
|
||||||
|
if (tick == 0)
|
||||||
|
throw new IllegalStateException("Effect hasn't ticked yet.");
|
||||||
|
|
||||||
return stateFunction.apply(tick - 1);
|
return stateFunction.apply(tick - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -27,17 +36,72 @@ public class ContinuousRumbleEffect implements RumbleEffect {
|
|||||||
stopped = true;
|
stopped = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int currentTick() {
|
@Override
|
||||||
|
public int age() {
|
||||||
return tick;
|
return tick;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isFinished() {
|
public boolean isFinished() {
|
||||||
return stopped;
|
return (stopped || (timeout > 0 && tick >= timeout)) && tick >= minTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int priority() {
|
public int priority() {
|
||||||
return this.priority;
|
return this.priority;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Builder builder() {
|
||||||
|
return new Builder();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
private Function<Integer, RumbleState> stateFunction;
|
||||||
|
private int priority;
|
||||||
|
private int timeout = -1;
|
||||||
|
private int minTime;
|
||||||
|
|
||||||
|
private Builder() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder byTick(Function<Integer, RumbleState> stateFunction) {
|
||||||
|
this.stateFunction = stateFunction;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder constant(RumbleState state) {
|
||||||
|
this.stateFunction = tick -> state;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder constant(float strong, float weak) {
|
||||||
|
return this.constant(new RumbleState(strong, weak));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder timeout(int timeoutTicks) {
|
||||||
|
Validate.isTrue(timeoutTicks >= 0, "the timeout cannot be negative!");
|
||||||
|
|
||||||
|
this.timeout = timeoutTicks;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder minTime(int minTimeTicks) {
|
||||||
|
Validate.isTrue(minTimeTicks >= 0, "the minimum time cannot be negative!");
|
||||||
|
|
||||||
|
this.minTime = minTimeTicks;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder priority(int priority) {
|
||||||
|
this.priority = priority;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ContinuousRumbleEffect build() {
|
||||||
|
Validate.notNull(stateFunction, "stateFunction cannot be null!");
|
||||||
|
Validate.isTrue(minTime <= timeout || timeout == -1, "the minimum time cannot be greater than the timeout!");
|
||||||
|
|
||||||
|
return new ContinuousRumbleEffect(stateFunction, priority, timeout, minTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,18 @@
|
|||||||
package dev.isxander.controlify.rumble;
|
package dev.isxander.controlify.rumble;
|
||||||
|
|
||||||
public interface RumbleEffect {
|
public interface RumbleEffect extends Comparable<RumbleEffect> {
|
||||||
RumbleState nextState();
|
void tick();
|
||||||
|
RumbleState currentState();
|
||||||
|
|
||||||
boolean isFinished();
|
boolean isFinished();
|
||||||
|
|
||||||
int priority();
|
int priority();
|
||||||
|
int age();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
default int compareTo(RumbleEffect o) {
|
||||||
|
int priorityCompare = Integer.compare(o.priority(), this.priority());
|
||||||
|
if (priorityCompare != 0) return priorityCompare;
|
||||||
|
return Integer.compare(this.age(), o.age());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,18 @@
|
|||||||
package dev.isxander.controlify.rumble;
|
package dev.isxander.controlify.rumble;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.PriorityQueue;
|
||||||
|
import java.util.Queue;
|
||||||
|
|
||||||
public class RumbleManager {
|
public class RumbleManager {
|
||||||
private final RumbleCapable controller;
|
private final RumbleCapable controller;
|
||||||
private RumbleEffectInstance playingEffect;
|
private final Queue<RumbleEffectInstance> effectQueue;
|
||||||
|
|
||||||
public RumbleManager(RumbleCapable controller) {
|
public RumbleManager(RumbleCapable controller) {
|
||||||
this.controller = controller;
|
this.controller = controller;
|
||||||
|
this.effectQueue = new PriorityQueue<>(Comparator.comparing(RumbleEffectInstance::effect));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Deprecated
|
@Deprecated
|
||||||
@ -17,34 +24,49 @@ public class RumbleManager {
|
|||||||
if (!controller.canRumble())
|
if (!controller.canRumble())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
playingEffect = new RumbleEffectInstance(source, effect);
|
effectQueue.add(new RumbleEffectInstance(source, effect));
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isPlaying() {
|
|
||||||
return playingEffect != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void stopCurrentEffect() {
|
|
||||||
if (playingEffect == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
controller.setRumble(0f, 0f, RumbleSource.MASTER);
|
|
||||||
playingEffect = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void tick() {
|
public void tick() {
|
||||||
if (playingEffect == null)
|
RumbleEffectInstance effect;
|
||||||
return;
|
do {
|
||||||
|
effect = effectQueue.peek();
|
||||||
|
|
||||||
if (playingEffect.effect().isFinished()) {
|
// if we have no effects, break out of loop and get the null check
|
||||||
stopCurrentEffect();
|
if (effect == null)
|
||||||
|
break;
|
||||||
|
|
||||||
|
// if the effect is finished, remove and set null, so we loop again
|
||||||
|
if (effect.effect().isFinished()) {
|
||||||
|
effectQueue.remove(effect);
|
||||||
|
effect = null;
|
||||||
|
}
|
||||||
|
} while (effect == null);
|
||||||
|
|
||||||
|
if (effect == null) {
|
||||||
|
controller.setRumble(0f, 0f, RumbleSource.MASTER);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
RumbleState state = playingEffect.effect().nextState();
|
effectQueue.removeIf(e -> e.effect().isFinished());
|
||||||
controller.setRumble(state.strong(), state.weak(), playingEffect.source());
|
effectQueue.forEach(e -> e.effect().tick());
|
||||||
|
|
||||||
|
RumbleState state = effect.effect().currentState();
|
||||||
|
controller.setRumble(state.strong(), state.weak(), effect.source());
|
||||||
}
|
}
|
||||||
|
|
||||||
private record RumbleEffectInstance(RumbleSource source, RumbleEffect effect) {
|
public void clearEffects() {
|
||||||
|
effectQueue.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPlaying() {
|
||||||
|
return !effectQueue.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private record RumbleEffectInstance(RumbleSource source, RumbleEffect effect) implements Comparable<RumbleEffectInstance> {
|
||||||
|
@Override
|
||||||
|
public int compareTo(@NotNull RumbleManager.RumbleEffectInstance o) {
|
||||||
|
return effect.compareTo(o.effect);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
"core.GLXMixin",
|
"core.GLXMixin",
|
||||||
"feature.rumble.explosion.LightningBoltMixin",
|
"feature.rumble.explosion.LightningBoltMixin",
|
||||||
"feature.rumble.itembreak.LivingEntityMixin",
|
"feature.rumble.itembreak.LivingEntityMixin",
|
||||||
"feature.rumble.sounds.LevelRendererMixin",
|
"feature.rumble.levelevents.LevelRendererMixin",
|
||||||
"feature.rumble.useitem.LivingEntityMixin"
|
"feature.rumble.useitem.LivingEntityMixin"
|
||||||
],
|
],
|
||||||
"client": [
|
"client": [
|
||||||
|
Reference in New Issue
Block a user