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,53 @@
package fr.hugman.mubble.super_mario.client.mixin;

import fr.hugman.mubble.super_mario.client.references.SuperMarioRenderStateDataKeys;
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.util.ARGB;
import net.minecraft.world.entity.LivingEntity;
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;

/**
* 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>> {
/** The pale blue of the ice block, which whatever is seen through it takes on. */
private static final int super_mario$ICE_TINT = 0xFFB9E4FF;

/**
* 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.
* The snapshot the entity took of itself does not move, and neither do the limbs read off it.
*/
@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();
}

@Inject(method = "getModelTint", at = @At("RETURN"), cancellable = true)
private void super_mario$tintWhileFrozen(S state, CallbackInfoReturnable<Integer> cir) {
if (state.getData(SuperMarioRenderStateDataKeys.FREEZE) != null) {
cir.setReturnValue(ARGB.multiply(cir.getReturnValueI(), super_mario$ICE_TINT));
Comment thread
Hugman76 marked this conversation as resolved.
Outdated
}
}
}
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
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
"compatibilityLevel": "JAVA_21",
"client": [
"ClientPacketListenerMixin",
"FrozenEntityRendererMixin",
"FrozenLivingEntityRendererMixin",
"HumanoidMobRendererMixin",
"LivingEntityRendererMixin",
"AvatarRendererMixin"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,18 @@ protected TagAppender<DamageType> builder(TagKey<DamageType> tag) {

@Override
protected void addTags(HolderLookup.Provider wrapperLookup) {
// what an enderman blinks away from, and what projectile protection is worth anything against
this.builder(DamageTypeTags.IS_PROJECTILE)
.add(SuperMarioDamageTypeIds.KOOPA_SHELL);
.add(SuperMarioDamageTypeIds.KOOPA_SHELL)
.add(SuperMarioDamageTypeIds.FIREBALL)
.add(SuperMarioDamageTypeIds.ICEBALL)
.add(SuperMarioDamageTypeIds.GOLD_FIREBALL);

this.builder(SuperMarioDamageTypeTags.MELTS_FREEZE)
// optional only because nothing here generates the vanilla tag for the validator to find
.addOptionalTag(DamageTypeTags.IS_FIRE)
.add(SuperMarioDamageTypeIds.FIREBALL)
.add(SuperMarioDamageTypeIds.GOLD_FIREBALL);

this.builder(SuperMarioDamageTypeTags.INSTANT_KILLS_GOOMBAS)
.add(SuperMarioDamageTypeIds.STOMP)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ public void generateTranslations(HolderLookup.Provider wrapperLookup, Translatio
builder.add("block." + SuperMario.MOD_ID + ".bumpable.drop.one", "Drop one");
builder.add("block." + SuperMario.MOD_ID + ".bumpable.drop.one.description", "The block will drop one item per bump");

builder.add("commands." + SuperMario.MOD_ID + ".freeze.set.frozen", "Froze %s for %s ticks");
builder.add("commands." + SuperMario.MOD_ID + ".freeze.set.thawed", "Thawed %s");
builder.add("commands." + SuperMario.MOD_ID + ".freeze.set.already_frozen", "Nothing changed. That entity is already frozen");
builder.add("commands." + SuperMario.MOD_ID + ".freeze.set.not_frozen", "Nothing changed. That entity is not frozen");
builder.add("commands." + SuperMario.MOD_ID + ".freeze.set.unfreezable", "Nothing changed. That entity cannot be frozen");
builder.add("commands." + SuperMario.MOD_ID + ".freeze.query.frozen", "%s is frozen for %s more ticks");
builder.add("commands." + SuperMario.MOD_ID + ".freeze.query.thawed", "%s is not frozen");

builder.add("power_up." + SuperMario.MOD_ID + ".mini.description.size", "Shrinks you to a third of your size.");
builder.add("power_up." + SuperMario.MOD_ID + ".mini.description.trade_off", "Weaker, but a better jumper.");
builder.add("power_up." + SuperMario.MOD_ID + ".mini.description.water", "Sprint off land to run on water.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ protected void addTags(HolderLookup.Provider wrapperLookup) {
// FIREBALL is qualified because vanilla has one under that name too.
builder(ALL).add(GOOMBA, GREEN_KOOPA_SHELL, RED_KOOPA_SHELL, SuperMarioEntityTypeIds.FIREBALL, ICEBALL, GOLD_FIREBALL, CLOUD_PLATFORM, BUBBLE);

// bosses shrug an ice ball off; every other mob is judged on its bulk alone
builder(FREEZE_IMMUNE).add(ENDER_DRAGON, WITHER);

// Bosses and anything too big to make sense inside a bubble. Players are here on purpose: they fit the
// automatic size and health criteria, but getting stuck inside someone else's bubble is not the point.
builder(BUBBLE_CANNOT_TRAP).add(PLAYER, ENDER_DRAGON, WITHER, WARDEN, ELDER_GUARDIAN, RAVAGER, IRON_GOLEM);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
package fr.hugman.mubble.super_mario;

import com.google.common.reflect.Reflection;
import fr.hugman.mubble.super_mario.commands.SuperMarioCommands;
import fr.hugman.mubble.super_mario.core.component.SuperMarioDataComponents;
import fr.hugman.mubble.super_mario.core.particles.SuperMarioParticleTypes;
import fr.hugman.mubble.super_mario.core.attachment.SuperMarioAttachmentTypes;
import fr.hugman.mubble.super_mario.core.registries.SuperMarioBuiltInRegistries;
import fr.hugman.mubble.super_mario.network.protocol.SuperMarioServerReceivers;
import fr.hugman.mubble.super_mario.network.protocol.common.custom.SuperMarioPayloadTypes;
import fr.hugman.mubble.super_mario.sounds.SuperMarioSounds;
import fr.hugman.mubble.super_mario.world.attribute.SuperMarioEnvironmentAttributes;
import fr.hugman.mubble.super_mario.world.entity.SuperMarioEntityEvents;
import fr.hugman.mubble.super_mario.world.entity.SuperMarioEntityTypes;
import fr.hugman.mubble.super_mario.world.entity.freeze.FreezeEvents;
import fr.hugman.mubble.super_mario.world.inventory.SuperMarioMenuTypes;
import fr.hugman.mubble.super_mario.world.item.SuperMarioCreativeModeTabs;
import fr.hugman.mubble.super_mario.world.item.SuperMarioItems;
Expand Down Expand Up @@ -40,6 +45,7 @@ public void onInitialize() {

Reflection.initialize(SuperMarioParticleTypes.class);
Reflection.initialize(SuperMarioEnvironmentAttributes.class);
Reflection.initialize(SuperMarioAttachmentTypes.class);
SuperMarioEntityTypes.registerAttributes();

SuperMarioCreativeModeTabs.appendItemGroups();
Expand All @@ -50,8 +56,14 @@ public void onInitialize() {

SuperMarioBiomeModifications.register();

SuperMarioPayloadTypes.registerTypes();
SuperMarioServerReceivers.register();

SuperMarioCommands.register();

// Events
SuperMarioEntityEvents.registerListeners();
FreezeEvents.register();
}

public static Identifier id(String path) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package fr.hugman.mubble.super_mario.commands;

import fr.hugman.mubble.super_mario.server.commands.FreezeCommand;
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;

public class SuperMarioCommands {
public static void register() {
CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> FreezeCommand.register(dispatcher));
}
}
Loading
Loading