Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import fr.hugman.mubble.client.model.MubbleModelLayers;
import fr.hugman.mubble.client.network.MubbleClientPayloadReceivers;
import fr.hugman.mubble.client.renderer.MubbleRenderers;
import fr.hugman.mubble.client.sound.AirMoveSounds;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
Expand All @@ -19,5 +21,7 @@ public void onInitializeClient() {
MubbleRenderers.registerLayers();
MubbleKeyBindings.registerEvents();
MubbleClientPayloadReceivers.register();

ClientTickEvents.END_CLIENT_TICK.register(AirMoveSounds::tick);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package fr.hugman.mubble.client.mixin;

import fr.hugman.mubble.world.entity.JumpKeyHolder;
import net.minecraft.client.player.LocalPlayer;
import org.spongepowered.asm.mixin.Mixin;

/**
* Where a client reads its own jump key.
* <p>
* The server is handed the key of every player through their input packets, but a client only ever has one
* to read: the one under the keyboard in front of it. That is enough, since a client only ever simulates the
* mid-air moves of the player it controls.
*/
@Mixin(LocalPlayer.class)
public class LocalPlayerMixin implements JumpKeyHolder {
@Override
public boolean isJumpKeyHeld() {
return ((LocalPlayer) (Object) this).input.keyPresses.jump();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package fr.hugman.mubble.client.sound;

import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.minecraft.client.resources.sounds.AbstractTickableSoundInstance;
import net.minecraft.client.resources.sounds.SoundInstance;
import net.minecraft.core.Holder;
import net.minecraft.sounds.SoundEvent;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.entity.player.Player;

/**
* The loop a mid-air move is heard as, for as long as it lasts.
* <p>
* It follows the player rather than the move: what a flutter turning into a float should sound like is one
* loop giving way to another, so the instance stops as soon as the sound the player is owed is no longer
* its own, and {@link AirMoveSounds} starts whichever one has taken over.
*/
@Environment(EnvType.CLIENT)
public class AirMoveSoundInstance extends AbstractTickableSoundInstance {
private final Player player;
private final Holder<SoundEvent> event;

public AirMoveSoundInstance(Player player, Holder<SoundEvent> event) {
super(event.value(), SoundSource.PLAYERS, SoundInstance.createUnseededRandom());
this.player = player;
this.event = event;
this.looping = true;
this.delay = 0;
this.volume = 0.2F;
}

/** The loop this instance is playing, so that the handler can tell it apart from the one now owed. */
public Holder<SoundEvent> event() {
return this.event;
}

@Override
public boolean canPlaySound() {
return !this.player.isSilent();
}

@Override
public boolean canStartSilent() {
return true;
}

@Override
public void tick() {
var wanted = AirMoveSounds.soundFor(this.player);
if (this.player.isRemoved() || wanted.isEmpty() || !wanted.get().equals(this.event)) {
this.stop();
return;
}
this.x = (float) this.player.getX();
this.y = (float) this.player.getY();
this.z = (float) this.player.getZ();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package fr.hugman.mubble.client.sound;

import fr.hugman.mubble.world.power_up.ability.FloatAbility;
import fr.hugman.mubble.world.power_up.ability.FlutterAbility;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.minecraft.client.Minecraft;
import net.minecraft.core.Holder;
import net.minecraft.sounds.SoundEvent;
import net.minecraft.world.entity.player.Player;

import java.util.Map;
import java.util.Optional;
import java.util.WeakHashMap;

/**
* Keeps one loop going per player on a mid-air move in sight.
* <p>
* A sound instance stops itself once the move it belongs to is over, but nothing would stop a second one
* from being started on the very next tick, so the ones already playing are held onto here. The map is weak
* on purpose: a player that walked out of range, or left the game, takes their entry with them.
*/
@Environment(EnvType.CLIENT)
public final class AirMoveSounds {
private static final Map<Player, AirMoveSoundInstance> PLAYING = new WeakHashMap<>();

private AirMoveSounds() {
}

/**
* @return the loop {@code player} is owed right now, the climb taking precedence over the descent for
* the tick or two in which a client believes they are on both
*/
public static Optional<Holder<SoundEvent>> soundFor(Player player) {
if (player.isFluttering()) {
return player.getFlutterAbility().flatMap(FlutterAbility::sound);
}
if (player.isFloating()) {
return player.getFloatAbility().flatMap(FloatAbility::sound);
}
return Optional.empty();
}

public static void tick(Minecraft client) {
if (client.level == null) {
PLAYING.clear();
return;
}
for (Player player : client.level.players()) {
var wanted = soundFor(player);
if (wanted.isEmpty()) {
PLAYING.remove(player);
continue;
}
var playing = PLAYING.get(player);
if (playing != null && !playing.isStopped() && wanted.get().equals(playing.event())) {
continue;
}
var instance = new AirMoveSoundInstance(player, wanted.get());
PLAYING.put(player, instance);
client.getSoundManager().play(instance);
}
}
}
3 changes: 2 additions & 1 deletion mubble-core/src/client/resources/mubble.client.mixins.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"ClientPacketListenerMixin",
"HudMixin",
"ItemInHandRendererMixin",
"LivingEntityRendererMixin"
"LivingEntityRendererMixin",
"LocalPlayerMixin"
],
"injectors": {
"defaultRequire": 1
Expand Down
Loading
Loading