Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package fr.hugman.mubble.super_mario.client;

import fr.hugman.mubble.super_mario.client.gui.screens.inventory.BumpableScreen;
import fr.hugman.mubble.super_mario.client.keybind.FreezeStruggleHandler;
import fr.hugman.mubble.super_mario.client.model.SuperMarioModelLayers;
import fr.hugman.mubble.super_mario.client.particle.SuperMarioParticleResources;
import fr.hugman.mubble.super_mario.client.renderer.SuperMarioRenderPipelines;
Expand All @@ -9,6 +10,7 @@
import com.google.common.reflect.Reflection;
import fr.hugman.mubble.super_mario.world.inventory.SuperMarioMenuTypes;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.minecraft.client.gui.screens.MenuScreens;
Expand All @@ -25,6 +27,7 @@ public void onInitializeClient() {
SuperMarioRenderers.registerEntities();
SuperMarioRenderers.registerBlockEntities();
SuperMarioParticleResources.register();
ClientTickEvents.END_CLIENT_TICK.register(FreezeStruggleHandler::tick);
}

private static void registerHandledScreens() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package fr.hugman.mubble.super_mario.client.keybind;

import fr.hugman.mubble.super_mario.network.protocol.common.custom.StruggleFreePayload;
import fr.hugman.mubble.super_mario.world.entity.freeze.Freezing;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
import net.minecraft.client.KeyMapping;
import net.minecraft.client.Minecraft;

/**
* Lets a frozen player smash their way out of the ice a little sooner by hammering the movement
* keys.
* <p>
* Only the presses themselves count, never the keys being held down: holding a direction is what a
* player does anyway when they run into the ice ball that froze them.
*/
@Environment(EnvType.CLIENT)
public class FreezeStruggleHandler {
public static void tick(Minecraft client) {
var player = client.player;
var options = client.options;
if (player == null) {
return;
}

int presses = consumeClicks(options.keyUp) + consumeClicks(options.keyDown)
+ consumeClicks(options.keyLeft) + consumeClicks(options.keyRight);
// the keys are consumed either way: a press held over from before the freeze is not a struggle
if (presses == 0 || !Freezing.isFrozen(player)) {
return;
}
for (int i = 0; i < presses; i++) {
ClientPlayNetworking.send(StruggleFreePayload.INSTANCE);
}
}

private static int consumeClicks(KeyMapping key) {
int presses = 0;
while (key.consumeClick()) {
presses++;
}
return presses;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package fr.hugman.mubble.super_mario.client.mixin;

import com.mojang.blaze3d.vertex.PoseStack;
import fr.hugman.mubble.super_mario.client.references.SuperMarioRenderStateDataKeys;
import fr.hugman.mubble.super_mario.client.renderer.entity.state.FreezeRenderData;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.minecraft.client.renderer.SubmitNodeCollector;
import net.minecraft.client.renderer.entity.EntityRenderer;
import net.minecraft.client.renderer.entity.state.EntityRenderState;
import net.minecraft.client.renderer.state.level.CameraRenderState;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.phys.Vec3;
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;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;

/**
* Wraps a frozen entity in the block of ice holding it, and holds its animations still while it is
* in there.
*/
@Mixin(EntityRenderer.class)
@Environment(EnvType.CLIENT)
public class FrozenEntityRendererMixin<T extends Entity, S extends EntityRenderState> {
@Inject(method = "extractRenderState(Lnet/minecraft/world/entity/Entity;Lnet/minecraft/client/renderer/entity/state/EntityRenderState;F)V", at = @At("TAIL"))
private void super_mario$extractFreeze(T entity, S state, float partialTicks, CallbackInfo ci) {
var freeze = FreezeRenderData.of(entity, partialTicks);
// set even when absent: render states are handed down from one entity to the next
state.setData(SuperMarioRenderStateDataKeys.FREEZE, freeze);
if (freeze != null) {
// winding the age back to what it was when the ice took hold stops everything driven by it
state.ageInTicks -= freeze.frozenFor();
}
}

/**
* Shakes a block of ice that is about to give.
* <p>
* The offset is added here rather than around the ice cube below, because this is the one the
* whole entity is drawn from: the ice and whatever is caught inside it shudder as the one thing.
*/
@Inject(method = "getRenderOffset", at = @At("RETURN"), cancellable = true)
private void super_mario$rattleTheIce(S state, CallbackInfoReturnable<Vec3> cir) {
var freeze = state.getData(SuperMarioRenderStateDataKeys.FREEZE);
// most of a freeze is spent perfectly still, and that half is not worth a vector for
if (freeze != null && freeze.rattle() != Vec3.ZERO) {
cir.setReturnValue(cir.getReturnValue().add(freeze.rattle()));
}
}

@Inject(method = "submit", at = @At("TAIL"))
private void super_mario$submitIceCube(S state, PoseStack poseStack, SubmitNodeCollector submitNodeCollector, CameraRenderState camera, CallbackInfo ci) {
var freeze = state.getData(SuperMarioRenderStateDataKeys.FREEZE);
if (freeze == null) {
return;
}
poseStack.pushPose();
// the block model spans a whole block from the corner it is drawn at, hence the centering
poseStack.scale(state.boundingBoxWidth, state.boundingBoxHeight, state.boundingBoxWidth);
poseStack.translate(-0.5F, 0.0F, -0.5F);
submitNodeCollector.submitMovingBlock(poseStack, freeze.iceCube(), 0);
poseStack.popPose();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package fr.hugman.mubble.super_mario.client.mixin;

import com.llamalad7.mixinextras.injector.ModifyReturnValue;
import fr.hugman.mubble.super_mario.client.references.SuperMarioRenderStateDataKeys;
import fr.hugman.mubble.super_mario.client.renderer.SuperMarioRenderTypes;
import fr.hugman.mubble.super_mario.world.entity.freeze.FreezeSnapshot;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.minecraft.client.model.EntityModel;
import net.minecraft.client.renderer.entity.LivingEntityRenderer;
import net.minecraft.client.renderer.entity.state.LivingEntityRenderState;
import net.minecraft.client.renderer.rendertype.RenderType;
import net.minecraft.resources.Identifier;
import net.minecraft.world.entity.LivingEntity;
import org.jspecify.annotations.Nullable;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;

/**
* Gives a frozen entity the colour of the ice it is caught in, and holds the one animation the age
* alone does not drive.
*
* @see FrozenEntityRendererMixin
*/
@Mixin(LivingEntityRenderer.class)
@Environment(EnvType.CLIENT)
public class FrozenLivingEntityRendererMixin<T extends LivingEntity, S extends LivingEntityRenderState, M extends EntityModel<? super S>> {
@Shadow
public Identifier getTextureLocation(final S state) {
return null;
}

/**
* Puts the limbs back where they were the moment the ice took hold.
* <p>
* Vanilla reads them off a walk animation that runs itself down as soon as the entity stops
* moving, so a mob frozen mid-stride would ease into a resting pose over the next half second.
*/
@Inject(method = "extractRenderState(Lnet/minecraft/world/entity/LivingEntity;Lnet/minecraft/client/renderer/entity/state/LivingEntityRenderState;F)V", at = @At("TAIL"))
private void super_mario$holdThePoseWhileFrozen(T entity, S state, float partialTicks, CallbackInfo ci) {
if (state.getData(SuperMarioRenderStateDataKeys.FREEZE) == null) {
return;
}
var snapshot = (FreezeSnapshot) entity;
state.walkAnimationPos = snapshot.frozenWalkPos();
state.walkAnimationSpeed = snapshot.frozenWalkSpeed();
}

/**
* Draws a frozen entity through the ice shader, which remaps it onto the ice block's palette.
* <p>
* Whatever vanilla settled on is kept when it decided not to draw the entity at all, so an
* invisible mob stays invisible in there.
*/
@ModifyReturnValue(method = "getRenderType", at = @At("RETURN"))
private @Nullable RenderType super_mario$iceRenderTypeWhileFrozen(@Nullable RenderType original, S state, boolean isBodyVisible, boolean forceTransparent, boolean appearGlowing) {
if (original == null || state.getData(SuperMarioRenderStateDataKeys.FREEZE) == null) {
return original;
}
return SuperMarioRenderTypes.getFrozenEntity(this.getTextureLocation(state));
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package fr.hugman.mubble.super_mario.client.references;

import fr.hugman.mubble.super_mario.client.renderer.entity.state.FreezeRenderData;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.fabricmc.fabric.api.client.rendering.v1.RenderStateDataKey;
import org.joml.Quaternionfc;

@Environment(EnvType.CLIENT)
public class SuperMarioRenderStateDataKeys {
/** The block of ice around the entity, {@code null} whenever it is not frozen. */
public static final RenderStateDataKey<FreezeRenderData> FREEZE = RenderStateDataKey.create(() -> "Freeze");

/** Set on entities held inside a {@link fr.hugman.mubble.super_mario.world.entity.projectile.Bubble}. */
public static final RenderStateDataKey<BubbleRide> BUBBLE_RIDE = RenderStateDataKey.create(() -> "Bubble ride");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,17 @@ public class SuperMarioRenderPipelines {
.withCull(false)
.build()
);

// Same as GOLDEN_ENTITY_PIPELINE, over the ice block's palette instead
public static final RenderPipeline FROZEN_ENTITY_PIPELINE = RenderPipelines.register(
RenderPipeline.builder(RenderPipelines.ENTITY_SNIPPET)
.withLocation(SuperMario.id("pipeline/frozen_entity"))
.withShaderDefine("ALPHA_CUTOUT", 0.1F)
.withShaderDefine("PER_FACE_LIGHTING")
.withFragmentShader(SuperMario.id("core/frozen_entity"))
.withBindGroupLayout(BindGroupLayouts.SAMPLER1)
.withColorTargetState(new ColorTargetState(BlendFunction.TRANSLUCENT))
.withCull(false)
.build()
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,25 @@ public class SuperMarioRenderTypes {
return RenderType.create("super_mario_golden_entity", state);
});

// Based on RenderTypes#ENTITY_TRANSLUCENT
public static final BiFunction<Identifier, Boolean, RenderType> FROZEN_ENTITY = Util.memoize(
(texture, affectsOutline) -> {
RenderSetup state = RenderSetup.builder(SuperMarioRenderPipelines.FROZEN_ENTITY_PIPELINE)
.withTexture("Sampler0", texture)
.useLightmap()
.useOverlay()
.affectsCrumbling()
.sortOnUpload()
.setOutline(affectsOutline ? RenderSetup.OutlineProperty.AFFECTS_OUTLINE : RenderSetup.OutlineProperty.NONE)
.createRenderSetup();
return RenderType.create("super_mario_frozen_entity", state);
});

public static RenderType getGoldenEntity(Identifier texture) {
return GOLDEN_ENTITY.apply(texture, true);
}

public static RenderType getFrozenEntity(Identifier texture) {
return FROZEN_ENTITY.apply(texture, true);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package fr.hugman.mubble.super_mario.client.renderer.entity.state;

import fr.hugman.mubble.super_mario.world.entity.freeze.Freezing;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.client.renderer.block.MovingBlockRenderState;
import net.minecraft.util.Mth;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.phys.Vec3;
import org.jspecify.annotations.Nullable;

/**
* What the renderers need to know about the block of ice an entity is trapped in.
*
* @param frozenFor how long the entity has been frozen for, in ticks, interpolated within the tick.
* Subtracting it from an age gives the very same value on every single frame, which
* is what holds the animations of a frozen entity still.
* @param rattle how far the block of ice is off its resting place this frame, which is nothing at
* all until it is nearly out of time
* @param iceCube the ice block filling the entity hitbox, ready to be handed to the block renderer
*/
@Environment(EnvType.CLIENT)
public record FreezeRenderData(float frozenFor, Vec3 rattle, MovingBlockRenderState iceCube) {
/** How far the ice throws itself around, in blocks, by the time it is about to give. */
private static final double RATTLE_AMPLITUDE = 0.06D;
/** How fast it does so, in radians per tick. Fast enough to read as a shudder rather than a sway. */
private static final float RATTLE_FREQUENCY = 2.7F;
/** The two axes are run at different rates so that the shudder never settles into a straight line. */
private static final float RATTLE_CROSS_FREQUENCY = 3.9F;

/**
* @return what to render around the entity, or {@code null} when it is not frozen
*/
@Nullable
public static FreezeRenderData of(Entity entity, float partialTicks) {
var freeze = Freezing.getState(entity);
if (freeze == null) {
return null;
}
long gameTime = entity.level().getGameTime();

var iceCube = new MovingBlockRenderState();
var pos = entity.blockPosition();
iceCube.randomSeedPos = pos;
iceCube.blockPos = pos;
iceCube.blockState = Blocks.ICE.defaultBlockState();
if (entity.level() instanceof ClientLevel level) {
iceCube.biome = level.getBiome(pos);
iceCube.cardinalLighting = level.cardinalLighting();
iceCube.lightEngine = level.getLightEngine();
}

float frozenFor = freeze.elapsed(gameTime) + partialTicks;
return new FreezeRenderData(frozenFor, rattleOf(freeze.remaining(gameTime) - partialTicks, frozenFor), iceCube);
}

/**
* Works out how hard the ice is shaking, which is the only warning anyone gets that it is about to
* let go.
*
* @param remaining how much of the freeze is left, in ticks, interpolated within the tick
* @param frozenFor how long the freeze has run for, in ticks, interpolated within the tick. It is
* what the shudder is driven off, so that it keeps going rather than restarting
* every frame.
*/
private static Vec3 rattleOf(float remaining, float frozenFor) {
if (remaining >= Freezing.RATTLE_DURATION) {
return Vec3.ZERO;
}
// it starts as a barely-there tremor and works itself up to the moment the ice gives
double amplitude = RATTLE_AMPLITUDE * (1.0D - Math.max(remaining, 0.0F) / Freezing.RATTLE_DURATION);
return new Vec3(
Mth.sin(frozenFor * RATTLE_FREQUENCY) * amplitude,
0.0D,
Mth.sin(frozenFor * RATTLE_CROSS_FREQUENCY) * amplitude);
}
}
Loading
Loading