Implement ultimate selection feature with key binding and UI
Some checks failed
build / build (push) Has been cancelled

This commit is contained in:
2026-01-16 21:39:36 +00:00
parent 5a868d2396
commit 0a55a44064
9 changed files with 193 additions and 20 deletions

View File

@@ -38,6 +38,10 @@ public class UltcraftClient implements ClientModInitializer {
Ultcraft.LOGGER.info("Ultimate activation requested!"); Ultcraft.LOGGER.info("Ultimate activation requested!");
} }
} }
while (UltimateKeybinding.switchUltimateMenuKey.consumeClick()) {
client.setScreen(new com.ottohg.ultcraft.client.UltimateSelectionScreen());
}
}); });
Ultcraft.LOGGER.info("Ultcraft Client initialized successfully!"); Ultcraft.LOGGER.info("Ultcraft Client initialized successfully!");

View File

@@ -7,8 +7,12 @@ package com.ottohg.ultcraft.client;
public class ClientUltimateData { public class ClientUltimateData {
private static float targetCharge = 0.0f; private static float targetCharge = 0.0f;
private static float displayedCharge = 0.0f; private static float displayedCharge = 0.0f;
// Animation state
private static long lastAnimationTime = 0; private static long lastAnimationTime = 0;
private static boolean isAnimatingReset = false; private static boolean isAnimating = false;
private static float animationStartCharge = 0.0f;
private static float animationTargetCharge = 0.0f;
private static final float RESET_DURATION = 500.0f; // 0.5 seconds private static final float RESET_DURATION = 500.0f; // 0.5 seconds
public static float getCharge() { public static float getCharge() {
@@ -16,17 +20,17 @@ public class ClientUltimateData {
} }
public static float getDisplayCharge() { public static float getDisplayCharge() {
if (isAnimatingReset) { if (isAnimating) {
long currentTime = System.currentTimeMillis(); long currentTime = System.currentTimeMillis();
float timeDelta = currentTime - lastAnimationTime; float timeDelta = currentTime - lastAnimationTime;
float progress = timeDelta / RESET_DURATION; float progress = timeDelta / RESET_DURATION;
if (progress >= 1.0f) { if (progress >= 1.0f) {
displayedCharge = targetCharge; displayedCharge = animationTargetCharge;
isAnimatingReset = false; isAnimating = false;
} else { } else {
// Lerp from 100 to 0 // Lerp from start to target
displayedCharge = 100.0f * (1.0f - progress); displayedCharge = animationStartCharge + (animationTargetCharge - animationStartCharge) * progress;
} }
} else { } else {
displayedCharge = targetCharge; displayedCharge = targetCharge;
@@ -37,11 +41,16 @@ public class ClientUltimateData {
public static void setCharge(float charge) { public static void setCharge(float charge) {
float newCharge = Math.min(charge, 100.0f); float newCharge = Math.min(charge, 100.0f);
// Check if we should trigger reset animation (high charge -> low charge) // Check for significant drops that should be animated
// Relaxed condition to catch cases where passive charge might have ticked once // 1. Reset: > 90 -> < 10
if (targetCharge > 90.0f && newCharge < 10.0f) { // 2. Switch penalty: > 15 -> ~15
isAnimatingReset = true; if ((targetCharge > 90.0f && newCharge < 10.0f) ||
(targetCharge > 15.1f && Math.abs(newCharge - 15.0f) < 0.1f)) {
isAnimating = true;
lastAnimationTime = System.currentTimeMillis(); lastAnimationTime = System.currentTimeMillis();
animationStartCharge = displayedCharge; // Start from current visual position (usually 100 or current)
animationTargetCharge = newCharge;
} }
targetCharge = newCharge; targetCharge = newCharge;

View File

@@ -11,6 +11,7 @@ import org.lwjgl.glfw.GLFW;
*/ */
public class UltimateKeybinding { public class UltimateKeybinding {
public static KeyMapping activateUltimateKey; public static KeyMapping activateUltimateKey;
public static KeyMapping switchUltimateMenuKey;
public static final KeyMapping.Category ULTIMATE_CATEGORY = KeyMapping.Category.register(Identifier.fromNamespaceAndPath("ultcraft", "ultimate")); public static final KeyMapping.Category ULTIMATE_CATEGORY = KeyMapping.Category.register(Identifier.fromNamespaceAndPath("ultcraft", "ultimate"));
public static void register() { public static void register() {
@@ -20,5 +21,12 @@ public class UltimateKeybinding {
GLFW.GLFW_KEY_Z, // Default to 'Z' key GLFW.GLFW_KEY_Z, // Default to 'Z' key
ULTIMATE_CATEGORY // Category ULTIMATE_CATEGORY // Category
)); ));
switchUltimateMenuKey = KeyBindingHelper.registerKeyBinding(new KeyMapping(
"key.ultcraft.switch_ultimate_menu",
InputConstants.Type.KEYSYM,
GLFW.GLFW_KEY_H,
ULTIMATE_CATEGORY
));
} }
} }

View File

@@ -0,0 +1,46 @@
package com.ottohg.ultcraft.client;
import com.ottohg.ultcraft.UltimateType;
import com.ottohg.ultcraft.network.UltimateSwitchPacket;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.network.chat.Component;
public class UltimateSelectionScreen extends Screen {
public UltimateSelectionScreen() {
super(Component.literal("Select Ultimate"));
}
@Override
protected void init() {
super.init();
int centerX = this.width / 2;
int centerY = this.height / 2;
this.addRenderableWidget(Button.builder(UltimateType.ARMED_AND_DANGEROUS.getDisplayName(), button -> {
selectUltimate(UltimateType.ARMED_AND_DANGEROUS);
})
.bounds(centerX - 100, centerY - 25, 200, 20).build());
this.addRenderableWidget(Button.builder(UltimateType.LIKE_A_RABBIT.getDisplayName(), button -> {
selectUltimate(UltimateType.LIKE_A_RABBIT);
})
.bounds(centerX - 100, centerY + 5, 200, 20).build());
}
private void selectUltimate(UltimateType type) {
ClientPlayNetworking.send(new UltimateSwitchPacket(type.getId()));
this.onClose();
}
@Override
public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) {
// Draw semi-transparent background manually to avoid overlapping blur issues
guiGraphics.fill(0, 0, this.width, this.height, 0x90000000);
guiGraphics.drawCenteredString(this.font, this.title, this.width / 2, this.height / 2 - 50, 0xFFFFFF);
super.render(guiGraphics, mouseX, mouseY, partialTick);
}
}

View File

@@ -1,6 +1,7 @@
package com.ottohg.ultcraft; package com.ottohg.ultcraft;
import com.ottohg.ultcraft.network.UltimateActivatePacket; import com.ottohg.ultcraft.network.UltimateActivatePacket;
import com.ottohg.ultcraft.network.UltimateSwitchPacket;
import net.fabricmc.api.ModInitializer; import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry; import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry;
@@ -20,10 +21,12 @@ public class Ultcraft implements ModInitializer {
// Register network packets // Register network packets
PayloadTypeRegistry.playC2S().register(UltimateActivatePacket.TYPE, UltimateActivatePacket.CODEC); PayloadTypeRegistry.playC2S().register(UltimateActivatePacket.TYPE, UltimateActivatePacket.CODEC);
PayloadTypeRegistry.playC2S().register(UltimateSwitchPacket.TYPE, UltimateSwitchPacket.CODEC);
PayloadTypeRegistry.playS2C().register(com.ottohg.ultcraft.network.UltimateSyncPacket.TYPE, PayloadTypeRegistry.playS2C().register(com.ottohg.ultcraft.network.UltimateSyncPacket.TYPE,
com.ottohg.ultcraft.network.UltimateSyncPacket.CODEC); com.ottohg.ultcraft.network.UltimateSyncPacket.CODEC);
UltimateActivatePacket.register(); UltimateActivatePacket.register();
ServerPlayNetworking.registerGlobalReceiver(UltimateSwitchPacket.TYPE, UltimateSwitchPacket::handle);
// Register server tick event for passive charging // Register server tick event for passive charging
ServerTickEvents.END_SERVER_TICK.register(server -> { ServerTickEvents.END_SERVER_TICK.register(server -> {

View File

@@ -21,6 +21,7 @@ import java.util.UUID;
public class UltimateData { public class UltimateData {
private static final Map<UUID, Float> ultimateCharges = new HashMap<>(); private static final Map<UUID, Float> ultimateCharges = new HashMap<>();
private static final Map<UUID, Long> lastTickTimes = new HashMap<>(); private static final Map<UUID, Long> lastTickTimes = new HashMap<>();
private static final Map<UUID, UltimateType> selectedUltimates = new HashMap<>();
// Constants // Constants
private static final float PASSIVE_CHARGE_PER_MINUTE = 100.0f / 15.0f; // 100% over 15 minutes private static final float PASSIVE_CHARGE_PER_MINUTE = 100.0f / 15.0f; // 100% over 15 minutes
@@ -77,29 +78,67 @@ public class UltimateData {
return false; return false;
} }
// Apply effects: Strength 2 and Regeneration 2 for 5 seconds UltimateType type = getUltimateType(player.getUUID());
Holder<MobEffect> strength = BuiltInRegistries.MOB_EFFECT.wrapAsHolder(
BuiltInRegistries.MOB_EFFECT.getValue(Identifier.fromNamespaceAndPath("minecraft", "strength"))
);
Holder<MobEffect> regeneration = BuiltInRegistries.MOB_EFFECT.wrapAsHolder(
BuiltInRegistries.MOB_EFFECT.getValue(Identifier.fromNamespaceAndPath("minecraft", "regeneration"))
);
player.addEffect(new MobEffectInstance(strength, 200, 1)); if (type == UltimateType.ARMED_AND_DANGEROUS) {
player.addEffect(new MobEffectInstance(regeneration, 200, 1)); // Apply effects: Strength 2 and Regeneration 2 for 10 seconds
Holder<MobEffect> strength = BuiltInRegistries.MOB_EFFECT.wrapAsHolder(
BuiltInRegistries.MOB_EFFECT.getValue(Identifier.fromNamespaceAndPath("minecraft", "strength"))
);
Holder<MobEffect> regeneration = BuiltInRegistries.MOB_EFFECT.wrapAsHolder(
BuiltInRegistries.MOB_EFFECT.getValue(Identifier.fromNamespaceAndPath("minecraft", "regeneration"))
);
player.addEffect(new MobEffectInstance(strength, 200, 1));
player.addEffect(new MobEffectInstance(regeneration, 200, 1));
} else if (type == UltimateType.LIKE_A_RABBIT) {
// Swiftness 2 and Jump Boost 2 for 15 seconds
Holder<MobEffect> speed = BuiltInRegistries.MOB_EFFECT.wrapAsHolder(
BuiltInRegistries.MOB_EFFECT.getValue(Identifier.fromNamespaceAndPath("minecraft", "speed"))
);
Holder<MobEffect> jumpBoost = BuiltInRegistries.MOB_EFFECT.wrapAsHolder(
BuiltInRegistries.MOB_EFFECT.getValue(Identifier.fromNamespaceAndPath("minecraft", "jump_boost"))
);
player.addEffect(new MobEffectInstance(speed, 300, 1));
player.addEffect(new MobEffectInstance(jumpBoost, 300, 1));
}
// Reset charge // Reset charge
setCharge(player.getUUID(), 0.0f); setCharge(player.getUUID(), 0.0f);
Ultcraft.LOGGER.info("Player {} activated ultimate!", player.getName().getString()); Ultcraft.LOGGER.info("Player {} activated ultimate: {}", player.getName().getString(), type);
return true; return true;
} }
public static void removePlayer(UUID playerId) { public static void removePlayer(UUID playerId) {
ultimateCharges.remove(playerId); ultimateCharges.remove(playerId);
lastTickTimes.remove(playerId); lastTickTimes.remove(playerId);
selectedUltimates.remove(playerId);
} }
public static UltimateType getUltimateType(UUID playerId) {
return selectedUltimates.getOrDefault(playerId, UltimateType.ARMED_AND_DANGEROUS);
}
public static void switchUltimate(ServerPlayer player, String ultimateId) {
UltimateType newType = UltimateType.fromId(ultimateId);
UltimateType currentType = getUltimateType(player.getUUID());
if (newType != currentType) {
selectedUltimates.put(player.getUUID(), newType);
float currentCharge = getCharge(player.getUUID());
if (currentCharge > 15.0f) {
setCharge(player.getUUID(), 15.0f);
// Sync charge immediately
ServerPlayNetworking.send(player, new UltimateSyncPacket(15.0f));
}
// You might want to sync the selected ultimate type to the client here as well,
// but for now, we'll assume the client knows what it selected locally or we rely on charge sync.
// If the UI needs to know the server state (e.g. after relog), we'd need another packet.
}
}
public static void saveToNBT(UUID playerId, CompoundTag tag) { public static void saveToNBT(UUID playerId, CompoundTag tag) {
tag.putFloat("UltimateCharge", getCharge(playerId)); tag.putFloat("UltimateCharge", getCharge(playerId));
} }

View File

@@ -0,0 +1,33 @@
package com.ottohg.ultcraft;
import net.minecraft.network.chat.Component;
public enum UltimateType {
ARMED_AND_DANGEROUS("Armed and Dangerous", Component.literal("Armed and Dangerous")),
LIKE_A_RABBIT("Like a Rabbit", Component.literal("Like a Rabbit"));
private final String id;
private final Component displayName;
UltimateType(String id, Component displayName) {
this.id = id;
this.displayName = displayName;
}
public String getId() {
return id;
}
public Component getDisplayName() {
return displayName;
}
public static UltimateType fromId(String id) {
for (UltimateType type : values()) {
if (type.id.equals(id)) {
return type;
}
}
return ARMED_AND_DANGEROUS; // Default
}
}

View File

@@ -0,0 +1,30 @@
package com.ottohg.ultcraft.network;
import com.ottohg.ultcraft.Ultcraft;
import com.ottohg.ultcraft.UltimateData;
import io.netty.buffer.ByteBuf;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.resources.Identifier;
public record UltimateSwitchPacket(String ultimateId) implements CustomPacketPayload {
public static final CustomPacketPayload.Type<UltimateSwitchPacket> TYPE =
new CustomPacketPayload.Type<>(Identifier.fromNamespaceAndPath(Ultcraft.MOD_ID, "switch_ultimate"));
public static final StreamCodec<FriendlyByteBuf, UltimateSwitchPacket> CODEC = StreamCodec.of(
(buf, packet) -> buf.writeUtf(packet.ultimateId),
(buf) -> new UltimateSwitchPacket(buf.readUtf())
);
@Override
public Type<? extends CustomPacketPayload> type() {
return TYPE;
}
public static void handle(UltimateSwitchPacket packet, net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking.Context context) {
context.server().execute(() -> {
UltimateData.switchUltimate(context.player(), packet.ultimateId);
});
}
}

View File

@@ -1,4 +1,5 @@
{ {
"key.ultcraft.activate_ultimate": "Activate Ultimate", "key.ultcraft.activate_ultimate": "Activate Ultimate",
"key.ultcraft.switch_ultimate_menu": "Switch Ultimate Menu",
"category.ultcraft.ultimate": "Ultcraft" "category.ultcraft.ultimate": "Ultcraft"
} }