1
0
forked from Clones/Controlify

more rumble effects

- new continuous rumble
- block break rumble
- item break rumble
- fix damage rumble
This commit is contained in:
isXander
2023-04-04 23:38:56 +01:00
parent 19f2d00a3b
commit 4940b04f09
14 changed files with 414 additions and 120 deletions

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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) {
}
}

View File

@ -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)
);
}
}

View File

@ -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)

View File

@ -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) {

View File

@ -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;
}
}