Initial commit

This commit is contained in:
2026-01-16 21:02:46 +00:00
commit 5a868d2396
27 changed files with 1334 additions and 0 deletions

View File

@@ -0,0 +1,45 @@
package com.ottohg.ultcraft;
import com.ottohg.ultcraft.client.ClientUltimateData;
import com.ottohg.ultcraft.client.UltimateHudRenderer;
import com.ottohg.ultcraft.client.UltimateKeybinding;
import com.ottohg.ultcraft.network.UltimateActivatePacket;
import com.ottohg.ultcraft.network.UltimateSyncPacket;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
public class UltcraftClient implements ClientModInitializer {
@Override
public void onInitializeClient() {
Ultcraft.LOGGER.info("Initializing Ultcraft Client...");
// Register keybindings
UltimateKeybinding.register();
// Register HUD renderer
UltimateHudRenderer.register();
// Register client packet receiver
ClientPlayNetworking.registerGlobalReceiver(UltimateSyncPacket.TYPE, (packet, context) -> {
context.client().execute(() -> {
if (context.player() != null) {
ClientUltimateData.setCharge(packet.charge());
}
});
});
// Handle keybinding presses
ClientTickEvents.END_CLIENT_TICK.register(client -> {
while (UltimateKeybinding.activateUltimateKey.consumeClick()) {
if (ClientUltimateData.isReady()) {
// Send activation packet to server
ClientPlayNetworking.send(new UltimateActivatePacket());
Ultcraft.LOGGER.info("Ultimate activation requested!");
}
}
});
Ultcraft.LOGGER.info("Ultcraft Client initialized successfully!");
}
}

View File

@@ -0,0 +1,53 @@
package com.ottohg.ultcraft.client;
/**
* Client-side storage for the local player's ultimate charge.
* This is synced from the server.
*/
public class ClientUltimateData {
private static float targetCharge = 0.0f;
private static float displayedCharge = 0.0f;
private static long lastAnimationTime = 0;
private static boolean isAnimatingReset = false;
private static final float RESET_DURATION = 500.0f; // 0.5 seconds
public static float getCharge() {
return targetCharge;
}
public static float getDisplayCharge() {
if (isAnimatingReset) {
long currentTime = System.currentTimeMillis();
float timeDelta = currentTime - lastAnimationTime;
float progress = timeDelta / RESET_DURATION;
if (progress >= 1.0f) {
displayedCharge = targetCharge;
isAnimatingReset = false;
} else {
// Lerp from 100 to 0
displayedCharge = 100.0f * (1.0f - progress);
}
} else {
displayedCharge = targetCharge;
}
return displayedCharge;
}
public static void setCharge(float charge) {
float newCharge = Math.min(charge, 100.0f);
// Check if we should trigger reset animation (high charge -> low charge)
// Relaxed condition to catch cases where passive charge might have ticked once
if (targetCharge > 90.0f && newCharge < 10.0f) {
isAnimatingReset = true;
lastAnimationTime = System.currentTimeMillis();
}
targetCharge = newCharge;
}
public static boolean isReady() {
return targetCharge >= 100.0f;
}
}

View File

@@ -0,0 +1,186 @@
package com.ottohg.ultcraft.client;
import net.fabricmc.fabric.api.client.rendering.v1.HudRenderCallback;
import net.minecraft.client.DeltaTracker;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphics;
/**
* Renders the ultimate charge as a circular progress bar on the HUD.
* Similar to Overwatch's ultimate indicator.
*/
public class UltimateHudRenderer implements HudRenderCallback {
private static final int CIRCLE_RADIUS = 30;
private static final int CIRCLE_THICKNESS = 4;
private static final int HUD_X_OFFSET = 50;
private static final int HUD_Y_OFFSET = 50;
// Colors
private static final int COLOR_BACKGROUND = 0x80000000; // Semi-transparent black
private static final int COLOR_PROGRESS = 0xFFFFD700; // Gold
private static final int COLOR_READY = 0xFF00FF00; // Bright green
@Override
public void onHudRender(GuiGraphics guiGraphics, DeltaTracker deltaTracker) {
Minecraft mc = Minecraft.getInstance();
// Don't render in F3 debug screen
if (mc.getDebugOverlay().showDebugScreen()) {
return;
}
// Get charge percentage
float charge = ClientUltimateData.getDisplayCharge();
boolean isReady = ClientUltimateData.isReady();
// Calculate position (bottom left of screen)
int screenWidth = mc.getWindow().getGuiScaledWidth();
int screenHeight = mc.getWindow().getGuiScaledHeight();
int centerX = HUD_X_OFFSET;
int centerY = screenHeight - HUD_Y_OFFSET;
// Draw background circle
drawCircle(guiGraphics, centerX, centerY, CIRCLE_RADIUS, COLOR_BACKGROUND, true);
// Draw progress arc
int progressColor = isReady ? COLOR_READY : COLOR_PROGRESS;
drawArc(guiGraphics, centerX, centerY, CIRCLE_RADIUS, CIRCLE_THICKNESS,
charge / 100.0f, progressColor);
// Draw percentage text
String text = String.format("%d%%", (int) charge);
int textWidth = mc.font.width(text);
guiGraphics.drawString(mc.font, text,
centerX - textWidth / 2,
centerY - mc.font.lineHeight / 2,
progressColor, false);
// Draw "READY!" text if ultimate is charged
if (isReady) {
String readyText = "READY!";
int readyWidth = mc.font.width(readyText);
guiGraphics.drawString(mc.font, readyText,
centerX - readyWidth / 2,
centerY - CIRCLE_RADIUS - mc.font.lineHeight - 5,
COLOR_READY, true);
}
}
/**
* Draws a filled or outlined circle.
*/
private void drawCircle(GuiGraphics guiGraphics, int centerX, int centerY, int radius,
int color, boolean filled) {
float segments = 36;
float angleStep = (float) (2 * Math.PI / segments);
for (int i = 0; i < segments; i++) {
float angle1 = i * angleStep;
float angle2 = (i + 1) * angleStep;
int x1 = centerX + (int) (Math.cos(angle1) * radius);
int y1 = centerY + (int) (Math.sin(angle1) * radius);
int x2 = centerX + (int) (Math.cos(angle2) * radius);
int y2 = centerY + (int) (Math.sin(angle2) * radius);
if (filled) {
// Draw triangle from center to edge
guiGraphics.fill(centerX, centerY, x1, y1, color);
guiGraphics.fill(x1, y1, x2, y2, color);
} else {
// Draw line segment
drawLine(guiGraphics, x1, y1, x2, y2, color);
}
}
}
/**
* Draws a circular arc representing progress.
*/
private void drawArc(GuiGraphics guiGraphics, int centerX, int centerY, int radius,
int thickness, float progress, int color) {
float segments = 72;
float angleStep = (float) (2 * Math.PI / segments);
float startAngle = -(float) Math.PI / 2; // Start at top
int numSegments = (int) (segments * progress);
for (int i = 0; i < numSegments; i++) {
float angle1 = startAngle + i * angleStep;
float angle2 = startAngle + (i + 1) * angleStep;
// Outer edge
int x1Out = centerX + (int) (Math.cos(angle1) * radius);
int y1Out = centerY + (int) (Math.sin(angle1) * radius);
int x2Out = centerX + (int) (Math.cos(angle2) * radius);
int y2Out = centerY + (int) (Math.sin(angle2) * radius);
// Inner edge
int innerRadius = radius - thickness;
int x1In = centerX + (int) (Math.cos(angle1) * innerRadius);
int y1In = centerY + (int) (Math.sin(angle1) * innerRadius);
int x2In = centerX + (int) (Math.cos(angle2) * innerRadius);
int y2In = centerY + (int) (Math.sin(angle2) * innerRadius);
// Draw quad (two triangles)
drawQuad(guiGraphics, x1Out, y1Out, x2Out, y2Out, x2In, y2In, x1In, y1In, color);
}
}
/**
* Draws a line between two points.
*/
private void drawLine(GuiGraphics guiGraphics, int x1, int y1, int x2, int y2, int color) {
guiGraphics.fill(x1, y1, x2, y2, color);
}
/**
* Draws a quadrilateral using two triangles.
*/
private void drawQuad(GuiGraphics guiGraphics, int x1, int y1, int x2, int y2,
int x3, int y3, int x4, int y4, int color) {
// Triangle 1
fillTriangle(guiGraphics, x1, y1, x2, y2, x3, y3, color);
// Triangle 2
fillTriangle(guiGraphics, x1, y1, x3, y3, x4, y4, color);
}
/**
* Fills a triangle using scanline algorithm.
*/
private void fillTriangle(GuiGraphics guiGraphics, int x1, int y1, int x2, int y2,
int x3, int y3, int color) {
// Simple approximation: fill with small rectangles
int minX = Math.min(x1, Math.min(x2, x3));
int maxX = Math.max(x1, Math.max(x2, x3));
int minY = Math.min(y1, Math.min(y2, y3));
int maxY = Math.max(y1, Math.max(y2, y3));
for (int y = minY; y <= maxY; y++) {
for (int x = minX; x <= maxX; x++) {
if (isPointInTriangle(x, y, x1, y1, x2, y2, x3, y3)) {
guiGraphics.fill(x, y, x + 1, y + 1, color);
}
}
}
}
/**
* Checks if a point is inside a triangle using barycentric coordinates.
*/
private boolean isPointInTriangle(int px, int py, int x1, int y1, int x2, int y2,
int x3, int y3) {
float denominator = ((y2 - y3) * (x1 - x3) + (x3 - x2) * (y1 - y3));
if (Math.abs(denominator) < 0.001f) return false;
float a = ((y2 - y3) * (px - x3) + (x3 - x2) * (py - y3)) / denominator;
float b = ((y3 - y1) * (px - x3) + (x1 - x3) * (py - y3)) / denominator;
float c = 1 - a - b;
return a >= 0 && a <= 1 && b >= 0 && b <= 1 && c >= 0 && c <= 1;
}
public static void register() {
HudRenderCallback.EVENT.register(new UltimateHudRenderer());
}
}

View File

@@ -0,0 +1,24 @@
package com.ottohg.ultcraft.client;
import com.mojang.blaze3d.platform.InputConstants;
import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper;
import net.minecraft.client.KeyMapping;
import net.minecraft.resources.Identifier;
import org.lwjgl.glfw.GLFW;
/**
* Manages the keybinding for activating ultimates.
*/
public class UltimateKeybinding {
public static KeyMapping activateUltimateKey;
public static final KeyMapping.Category ULTIMATE_CATEGORY = KeyMapping.Category.register(Identifier.fromNamespaceAndPath("ultcraft", "ultimate"));
public static void register() {
activateUltimateKey = KeyBindingHelper.registerKeyBinding(new KeyMapping(
"key.ultcraft.activate_ultimate", // Translation key
InputConstants.Type.KEYSYM,
GLFW.GLFW_KEY_Z, // Default to 'Z' key
ULTIMATE_CATEGORY // Category
));
}
}

View File

@@ -0,0 +1,15 @@
package com.ottohg.ultcraft.mixin.client;
import net.minecraft.client.Minecraft;
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(Minecraft.class)
public class ExampleClientMixin {
@Inject(at = @At("HEAD"), method = "run")
private void init(CallbackInfo info) {
// This code is injected into the start of Minecraft.run()V
}
}

View File

@@ -0,0 +1,12 @@
{
"required": true,
"package": "com.ottohg.ultcraft.mixin.client",
"compatibilityLevel": "JAVA_21",
"refmap": "ultcraft.refmap.json",
"client": [
"ExampleClientMixin"
],
"injectors": {
"defaultRequire": 1
}
}

View File

@@ -0,0 +1,50 @@
package com.ottohg.ultcraft;
import com.ottohg.ultcraft.network.UltimateActivatePacket;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry;
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
import net.minecraft.server.level.ServerPlayer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Ultcraft implements ModInitializer {
public static final String MOD_ID = "ultcraft";
public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID);
@Override
public void onInitialize() {
LOGGER.info("Initializing Ultcraft - Ultimate System Loading...");
// Register network packets
PayloadTypeRegistry.playC2S().register(UltimateActivatePacket.TYPE, UltimateActivatePacket.CODEC);
PayloadTypeRegistry.playS2C().register(com.ottohg.ultcraft.network.UltimateSyncPacket.TYPE,
com.ottohg.ultcraft.network.UltimateSyncPacket.CODEC);
UltimateActivatePacket.register();
// Register server tick event for passive charging
ServerTickEvents.END_SERVER_TICK.register(server -> {
for (ServerPlayer player : server.getPlayerList().getPlayers()) {
// Tick passive charge
UltimateData.tickPassiveCharge(player.getUUID());
// Sync charge to client every second (20 ticks)
if (server.getTickCount() % 20 == 0) {
float charge = UltimateData.getCharge(player.getUUID());
ServerPlayNetworking.send(player,
new com.ottohg.ultcraft.network.UltimateSyncPacket(charge));
}
}
});
// Clean up data when player disconnects
ServerPlayConnectionEvents.DISCONNECT.register((handler, server) -> {
UltimateData.removePlayer(handler.getPlayer().getUUID());
});
LOGGER.info("Ultcraft initialized successfully!");
}
}

View File

@@ -0,0 +1,112 @@
package com.ottohg.ultcraft;
import com.ottohg.ultcraft.network.UltimateSyncPacket;
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
import net.minecraft.core.Holder;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.resources.Identifier;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.effect.MobEffect;
import net.minecraft.world.effect.MobEffectInstance;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* Manages ultimate charge data for all players.
* Handles passive charging, damage-based charging, and ultimate activation.
*/
public class UltimateData {
private static final Map<UUID, Float> ultimateCharges = new HashMap<>();
private static final Map<UUID, Long> lastTickTimes = new HashMap<>();
// Constants
private static final float PASSIVE_CHARGE_PER_MINUTE = 100.0f / 15.0f; // 100% over 15 minutes
private static final float DAMAGE_CHARGE_RATIO = 3.0f / 6.0f; // 3% per 6 damage points (3 hearts)
private static final float MAX_CHARGE = 100.0f;
public static float getCharge(UUID playerId) {
return ultimateCharges.getOrDefault(playerId, 0.0f);
}
public static void setCharge(UUID playerId, float charge) {
ultimateCharges.put(playerId, Math.min(charge, MAX_CHARGE));
}
public static void addCharge(UUID playerId, float amount) {
float current = getCharge(playerId);
setCharge(playerId, current + amount);
}
public static void addChargeFromDamage(ServerPlayer player, float damage) {
float chargeGain = damage * DAMAGE_CHARGE_RATIO;
addCharge(player.getUUID(), chargeGain);
Ultcraft.LOGGER.info("Player {} gained {}% ultimate charge from {} damage",
player.getUUID(), String.format("%.2f", chargeGain), damage);
// Sync to client immediately to reduce UI delay
ServerPlayNetworking.send(player, new UltimateSyncPacket(getCharge(player.getUUID())));
}
public static void tickPassiveCharge(UUID playerId) {
long currentTime = System.currentTimeMillis();
Long lastTime = lastTickTimes.get(playerId);
if (lastTime != null) {
long deltaMs = currentTime - lastTime;
float deltaMinutes = deltaMs / 60000.0f;
float chargeGain = PASSIVE_CHARGE_PER_MINUTE * deltaMinutes;
float currentCharge = getCharge(playerId);
if (currentCharge < MAX_CHARGE) {
addCharge(playerId, chargeGain);
}
}
lastTickTimes.put(playerId, currentTime);
}
public static boolean isUltimateReady(UUID playerId) {
return getCharge(playerId) >= MAX_CHARGE;
}
public static boolean activateUltimate(ServerPlayer player) {
if (!isUltimateReady(player.getUUID())) {
return false;
}
// Apply effects: Strength 2 and Regeneration 2 for 5 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));
// Reset charge
setCharge(player.getUUID(), 0.0f);
Ultcraft.LOGGER.info("Player {} activated ultimate!", player.getName().getString());
return true;
}
public static void removePlayer(UUID playerId) {
ultimateCharges.remove(playerId);
lastTickTimes.remove(playerId);
}
public static void saveToNBT(UUID playerId, CompoundTag tag) {
tag.putFloat("UltimateCharge", getCharge(playerId));
}
public static void loadFromNBT(UUID playerId, CompoundTag tag) {
if (tag.contains("UltimateCharge")) {
setCharge(playerId, tag.getFloat("UltimateCharge").orElse(0.0f));
}
}
}

View File

@@ -0,0 +1,37 @@
package com.ottohg.ultcraft.mixin;
import com.ottohg.ultcraft.UltimateData;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.damagesource.DamageSource;
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.CallbackInfoReturnable;
/**
* Mixin to track damage dealt by players and charge their ultimate accordingly.
*/
@Mixin(LivingEntity.class)
public class DamageTrackingMixin {
@Inject(method = "hurtServer", at = @At("RETURN"))
private void onHurt(ServerLevel level, DamageSource source, float amount, CallbackInfoReturnable<Boolean> cir) {
// Only process if damage was actually dealt
if (!cir.getReturnValue()) {
return;
}
// Check if the damage source is a player
if (source.getEntity() instanceof ServerPlayer player) {
LivingEntity target = (LivingEntity) (Object) this;
// Only charge from damage to other entities (not self-damage)
if (target != player) {
// Add charge based on damage dealt
UltimateData.addChargeFromDamage(player, amount);
}
}
}
}

View File

@@ -0,0 +1,15 @@
package com.ottohg.ultcraft.mixin;
import net.minecraft.server.MinecraftServer;
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(MinecraftServer.class)
public class ExampleMixin {
@Inject(at = @At("HEAD"), method = "loadLevel")
private void init(CallbackInfo info) {
// This code is injected into the start of MinecraftServer.loadLevel()V
}
}

View File

@@ -0,0 +1,34 @@
package com.ottohg.ultcraft.network;
import com.ottohg.ultcraft.Ultcraft;
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.resources.Identifier;
/**
* Packet sent from client to server to activate ultimate.
*/
public record UltimateActivatePacket() implements CustomPacketPayload {
public static final CustomPacketPayload.Type<UltimateActivatePacket> TYPE =
new CustomPacketPayload.Type<>(Identifier.fromNamespaceAndPath(Ultcraft.MOD_ID, "activate_ultimate"));
public static final StreamCodec<FriendlyByteBuf, UltimateActivatePacket> CODEC = StreamCodec.of(
(buf, packet) -> {}, // Write (nothing to write)
(buf) -> new UltimateActivatePacket() // Read
);
@Override
public Type<? extends CustomPacketPayload> type() {
return TYPE;
}
public static void register() {
ServerPlayNetworking.registerGlobalReceiver(TYPE, (packet, context) -> {
context.server().execute(() -> {
com.ottohg.ultcraft.UltimateData.activateUltimate(context.player());
});
});
}
}

View File

@@ -0,0 +1,25 @@
package com.ottohg.ultcraft.network;
import com.ottohg.ultcraft.Ultcraft;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.resources.Identifier;
/**
* Packet sent from server to client to sync ultimate charge.
*/
public record UltimateSyncPacket(float charge) implements CustomPacketPayload {
public static final CustomPacketPayload.Type<UltimateSyncPacket> TYPE =
new CustomPacketPayload.Type<>(Identifier.fromNamespaceAndPath(Ultcraft.MOD_ID, "sync_ultimate"));
public static final StreamCodec<FriendlyByteBuf, UltimateSyncPacket> CODEC = StreamCodec.of(
(buf, packet) -> buf.writeFloat(packet.charge),
(buf) -> new UltimateSyncPacket(buf.readFloat())
);
@Override
public Type<? extends CustomPacketPayload> type() {
return TYPE;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@@ -0,0 +1,4 @@
{
"key.ultcraft.activate_ultimate": "Activate Ultimate",
"category.ultcraft.ultimate": "Ultcraft"
}

View File

@@ -0,0 +1,38 @@
{
"schemaVersion": 1,
"id": "ultcraft",
"version": "${version}",
"name": "Ultcraft",
"description": "This is an example description! Tell everyone what your mod is about!",
"authors": [
"Me!"
],
"contact": {
"homepage": "https://fabricmc.net/",
"sources": "https://github.com/FabricMC/fabric-example-mod"
},
"license": "CC0-1.0",
"icon": "assets/ultcraft/icon.png",
"environment": "*",
"entrypoints": {
"main": [
"com.ottohg.ultcraft.Ultcraft"
],
"client": [
"com.ottohg.ultcraft.UltcraftClient"
]
},
"mixins": [
"ultcraft.mixins.json",
{
"config": "ultcraft.client.mixins.json",
"environment": "client"
}
],
"depends": {
"fabricloader": ">=0.18.4",
"minecraft": "~1.21.11",
"java": ">=21",
"fabric-api": "*"
}
}

View File

@@ -0,0 +1,16 @@
{
"required": true,
"package": "com.ottohg.ultcraft.mixin",
"compatibilityLevel": "JAVA_21",
"refmap": "ultcraft.refmap.json",
"mixins": [
"ExampleMixin",
"DamageTrackingMixin"
],
"injectors": {
"defaultRequire": 1
},
"overwrites": {
"requireAnnotations": true
}
}