diff --git a/src/main/java/net/fretux/mindmotion/ConfigMM.java b/src/main/java/net/fretux/mindmotion/ConfigMM.java index 881ca72..59764e8 100644 --- a/src/main/java/net/fretux/mindmotion/ConfigMM.java +++ b/src/main/java/net/fretux/mindmotion/ConfigMM.java @@ -33,6 +33,9 @@ public final class ConfigMM { public final ForgeConfigSpec.IntValue TEMPO_BAR_Y_OFFSET; public final ForgeConfigSpec.IntValue SANITY_BAR_X_OFFSET; public final ForgeConfigSpec.IntValue SANITY_BAR_Y_OFFSET; + public final ForgeConfigSpec.BooleanValue ENABLE_MADNESS_BAR; + public final ForgeConfigSpec.IntValue MADNESS_BAR_X_OFFSET; + public final ForgeConfigSpec.IntValue MADNESS_BAR_Y_OFFSET; Client(ForgeConfigSpec.Builder b) { b.push("visual"); @@ -75,6 +78,18 @@ public final class ConfigMM { .comment("Vertical offset for the sanity bar relative to its default position.") .defineInRange("sanityBarYOffset", 0, -300, 300); + ENABLE_MADNESS_BAR = b + .comment("Show the top-center Madness danger bar while Madness is above zero.") + .define("enableMadnessBar", true); + + MADNESS_BAR_X_OFFSET = b + .comment("Horizontal offset for the Madness bar relative to the centered default position.") + .defineInRange("madnessBarXOffset", 0, -300, 300); + + MADNESS_BAR_Y_OFFSET = b + .comment("Vertical offset for the Madness bar relative to the top-center default position.") + .defineInRange("madnessBarYOffset", 0, -300, 300); + b.pop(); } } diff --git a/src/main/java/net/fretux/mindmotion/client/ClientData.java b/src/main/java/net/fretux/mindmotion/client/ClientData.java index 5266bce..9269fe4 100644 --- a/src/main/java/net/fretux/mindmotion/client/ClientData.java +++ b/src/main/java/net/fretux/mindmotion/client/ClientData.java @@ -5,10 +5,62 @@ public class ClientData { public static float INSANITY = 0f; public static int TEMPO = 0; public static int VENT_COOLDOWN = 0; + public static float MADNESS = 0f; + public static float MAX_MADNESS = 20f; + public static float MADNESS_DECAY_PER_TICK = 0.01f; + public static float MADNESS_DECAY_DELAY_TICKS = 0f; + private static float displayedSanity = 80f; + private static float displayedInsanity = 0f; + private static float displayedTempo = 0f; + private static float displayedMadness = 0f; + private static float madnessRenderTarget = 0f; + private static long lastMadnessRenderNanos = System.nanoTime(); // NEW: synced from server public static float MAX_SANITY = 80f; public static int MAX_TEMPO = 120; public static boolean SANITY_ENABLED = true; public static boolean TEMPO_ENABLED = true; + + public static void setMadness(float madness) { + MADNESS = Math.max(0f, madness); + madnessRenderTarget = MADNESS; + } + + public static void updateInterpolatedValues() { + long now = System.nanoTime(); + float elapsedSeconds = Math.min(0.25f, (now - lastMadnessRenderNanos) / 1_000_000_000f); + lastMadnessRenderNanos = now; + + float elapsedTicks = elapsedSeconds * 20f; + float pausedTicks = Math.min(elapsedTicks, MADNESS_DECAY_DELAY_TICKS); + MADNESS_DECAY_DELAY_TICKS -= pausedTicks; + float decayTicks = elapsedTicks - pausedTicks; + if (decayTicks > 0f) { + madnessRenderTarget = Math.max(0f, + madnessRenderTarget - decayTicks * MADNESS_DECAY_PER_TICK); + } + + float lerp = 1f - (float) Math.exp(-10f * elapsedSeconds); + displayedSanity += (SANITY - displayedSanity) * lerp; + displayedInsanity += (INSANITY - displayedInsanity) * lerp; + displayedTempo += (TEMPO - displayedTempo) * lerp; + displayedMadness += (madnessRenderTarget - displayedMadness) * lerp; + } + + public static float getDisplayedSanity() { + return displayedSanity; + } + + public static float getDisplayedInsanity() { + return displayedInsanity; + } + + public static float getDisplayedTempo() { + return displayedTempo; + } + + public static float getDisplayedMadness() { + return displayedMadness; + } } diff --git a/src/main/java/net/fretux/mindmotion/client/HudEditorScreen.java b/src/main/java/net/fretux/mindmotion/client/HudEditorScreen.java index 90a4a14..aaa1bfb 100644 --- a/src/main/java/net/fretux/mindmotion/client/HudEditorScreen.java +++ b/src/main/java/net/fretux/mindmotion/client/HudEditorScreen.java @@ -21,6 +21,9 @@ public class HudEditorScreen extends Screen { private static final int DEFAULT_OFFSET = 0; private static final int BASE_TEMPO_X = 8; private static final int BASE_Y_OFFSET = 48; + private static final int MADNESS_BAR_WIDTH = 182; + private static final int MADNESS_BAR_HEIGHT = 7; + private static final int BASE_MADNESS_Y = 13; private final Screen previous; @@ -30,15 +33,20 @@ public class HudEditorScreen extends Screen { private int tempoOffsetY; private int sanityOffsetX; private int sanityOffsetY; + private int madnessOffsetX; + private int madnessOffsetY; private boolean draggingTempo; private boolean draggingSanity; + private boolean draggingMadness; private double dragStartX; private double dragStartY; private int dragStartTempoX; private int dragStartTempoY; private int dragStartSanityX; private int dragStartSanityY; + private int dragStartMadnessX; + private int dragStartMadnessY; private IntSlider barWidthSlider; private IntSlider barHeightSlider; @@ -46,6 +54,8 @@ public class HudEditorScreen extends Screen { private IntSlider tempoYSlider; private IntSlider sanityXSlider; private IntSlider sanityYSlider; + private IntSlider madnessXSlider; + private IntSlider madnessYSlider; public HudEditorScreen(Screen previous) { super(Component.translatable("screen.mindmotion.hud_editor.title")); @@ -56,6 +66,8 @@ public class HudEditorScreen extends Screen { this.tempoOffsetY = ConfigMM.CLIENT.TEMPO_BAR_Y_OFFSET.get(); this.sanityOffsetX = ConfigMM.CLIENT.SANITY_BAR_X_OFFSET.get(); this.sanityOffsetY = ConfigMM.CLIENT.SANITY_BAR_Y_OFFSET.get(); + this.madnessOffsetX = ConfigMM.CLIENT.MADNESS_BAR_X_OFFSET.get(); + this.madnessOffsetY = ConfigMM.CLIENT.MADNESS_BAR_Y_OFFSET.get(); } @Override @@ -141,6 +153,22 @@ public class HudEditorScreen extends Screen { sanityOffsetY, value -> setSanityOffsetY(value, false) )); + y += step; + } + + if (isMadnessEnabled()) { + madnessXSlider = addRenderableWidget(new IntSlider( + leftX, y, sliderWidth, 20, + "screen.mindmotion.hud_editor.madness_x", + MIN_OFFSET, MAX_OFFSET, madnessOffsetX, + value -> setMadnessOffsetX(value, false) + )); + madnessYSlider = addRenderableWidget(new IntSlider( + rightX, y, sliderWidth, 20, + "screen.mindmotion.hud_editor.madness_y", + MIN_OFFSET, MAX_OFFSET, madnessOffsetY, + value -> setMadnessOffsetY(value, false) + )); } int buttonY = this.height - 28; @@ -193,6 +221,15 @@ public class HudEditorScreen extends Screen { dragStartSanityY = sanityOffsetY; return true; } + BarPosition madnessBar = getMadnessBarPosition(); + if (isMadnessEnabled() && isMouseOver(mouseX, mouseY, madnessBar)) { + draggingMadness = true; + dragStartX = mouseX; + dragStartY = mouseY; + dragStartMadnessX = madnessOffsetX; + dragStartMadnessY = madnessOffsetY; + return true; + } } return super.mouseClicked(mouseX, mouseY, button); } @@ -214,6 +251,13 @@ public class HudEditorScreen extends Screen { setSanityOffsetY(newY, true); return true; } + if (draggingMadness) { + int newX = clamp(dragStartMadnessX + (int) Math.round(mouseX - dragStartX), MIN_OFFSET, MAX_OFFSET); + int newY = clamp(dragStartMadnessY + (int) Math.round(mouseY - dragStartY), MIN_OFFSET, MAX_OFFSET); + setMadnessOffsetX(newX, true); + setMadnessOffsetY(newY, true); + return true; + } } return super.mouseDragged(mouseX, mouseY, button, dragX, dragY); } @@ -223,6 +267,7 @@ public class HudEditorScreen extends Screen { if (button == 0) { draggingTempo = false; draggingSanity = false; + draggingMadness = false; } return super.mouseReleased(mouseX, mouseY, button); } @@ -237,12 +282,13 @@ public class HudEditorScreen extends Screen { private void renderPreview(GuiGraphics gui) { BarPosition tempoBar = getTempoBarPosition(); BarPosition sanityBar = getSanityBarPosition(); + BarPosition madnessBar = getMadnessBarPosition(); if (isTempoEnabled()) { drawRoundedBar(gui, tempoBar, 0xFF33AFFF, (int) (barWidth * 0.75f)); gui.drawString(this.font, Component.literal("Tempo"), tempoBar.x, tempoBar.y - 12, 0x66CCFF, false); } if (isSanityEnabled()) { - drawRoundedBar(gui, sanityBar, 0xFFFFFFFF, (int) (barWidth * 0.6f)); + drawRoundedBar(gui, sanityBar, 0xFFE8E8E8, (int) (barWidth * 0.6f)); int sanityLabelWidth = this.font.width("Sanity"); gui.drawString( this.font, @@ -253,6 +299,11 @@ public class HudEditorScreen extends Screen { false ); } + if (isMadnessEnabled()) { + drawBossStyleBar(gui, madnessBar, 0.72f); + gui.drawCenteredString(this.font, Component.literal("Madness"), + madnessBar.x + madnessBar.width / 2, madnessBar.y - 11, 0xFFFFFF55); + } } private BarPosition getTempoBarPosition() { @@ -276,6 +327,15 @@ public class HudEditorScreen extends Screen { ); } + private BarPosition getMadnessBarPosition() { + return new BarPosition( + (this.width - MADNESS_BAR_WIDTH) / 2 + madnessOffsetX, + BASE_MADNESS_Y + madnessOffsetY, + MADNESS_BAR_WIDTH, + MADNESS_BAR_HEIGHT + ); + } + private void setBarWidth(int value, boolean updateSlider) { barWidth = clamp(value, MIN_BAR_WIDTH, MAX_BAR_WIDTH); ConfigMM.CLIENT.BAR_WIDTH.set(barWidth); @@ -324,6 +384,22 @@ public class HudEditorScreen extends Screen { } } + private void setMadnessOffsetX(int value, boolean updateSlider) { + madnessOffsetX = clamp(value, MIN_OFFSET, MAX_OFFSET); + ConfigMM.CLIENT.MADNESS_BAR_X_OFFSET.set(madnessOffsetX); + if (updateSlider && madnessXSlider != null) { + madnessXSlider.setIntValue(madnessOffsetX); + } + } + + private void setMadnessOffsetY(int value, boolean updateSlider) { + madnessOffsetY = clamp(value, MIN_OFFSET, MAX_OFFSET); + ConfigMM.CLIENT.MADNESS_BAR_Y_OFFSET.set(madnessOffsetY); + if (updateSlider && madnessYSlider != null) { + madnessYSlider.setIntValue(madnessOffsetY); + } + } + private void resetDefaults() { setBarWidth(DEFAULT_BAR_WIDTH, true); setBarHeight(DEFAULT_BAR_HEIGHT, true); @@ -331,6 +407,8 @@ public class HudEditorScreen extends Screen { setTempoOffsetY(DEFAULT_OFFSET, true); setSanityOffsetX(DEFAULT_OFFSET, true); setSanityOffsetY(DEFAULT_OFFSET, true); + setMadnessOffsetX(DEFAULT_OFFSET, true); + setMadnessOffsetY(DEFAULT_OFFSET, true); } private void saveConfig() { @@ -345,6 +423,10 @@ public class HudEditorScreen extends Screen { return ConfigMM.COMMON.ENABLE_SANITY.get() && ClientData.SANITY_ENABLED; } + private static boolean isMadnessEnabled() { + return isSanityEnabled() && ConfigMM.CLIENT.ENABLE_MADNESS_BAR.get(); + } + private static int clamp(int value, int min, int max) { return Math.min(Math.max(value, min), max); } @@ -355,6 +437,31 @@ public class HudEditorScreen extends Screen { int top = color; int bottom = (color & 0x00FFFFFF) | 0xFF000000; drawRoundedRect(gui, pos.x, pos.y, fillWidth, pos.height, top, bottom); + if (fillWidth > 2) { + gui.fill(pos.x + 1, pos.y, pos.x + fillWidth - 1, pos.y + 1, lightenColor(color, 0.18f)); + } + } + + private void drawBossStyleBar(GuiGraphics gui, BarPosition pos, float progress) { + gui.fill(pos.x, pos.y, pos.x + pos.width, pos.y + pos.height, 0xFF000000); + gui.fill(pos.x + 1, pos.y + 1, pos.x + pos.width - 1, pos.y + pos.height - 1, 0xFF4A4000); + int fillWidth = Math.round((pos.width - 2) * progress); + int fillColor = mixColor(0xFFAD8D2C, 0xFFFFF4B0, progress); + gui.fillGradient(pos.x + 1, pos.y + 1, pos.x + 1 + fillWidth, pos.y + pos.height - 1, + lightenColor(fillColor, 0.16f), fillColor); + } + + private static int mixColor(int from, int to, float amount) { + float t = Math.max(0f, Math.min(1f, amount)); + int a = Math.round(((from >>> 24) & 0xFF) + (((to >>> 24) & 0xFF) - ((from >>> 24) & 0xFF)) * t); + int r = Math.round(((from >>> 16) & 0xFF) + (((to >>> 16) & 0xFF) - ((from >>> 16) & 0xFF)) * t); + int g = Math.round(((from >>> 8) & 0xFF) + (((to >>> 8) & 0xFF) - ((from >>> 8) & 0xFF)) * t); + int b = Math.round((from & 0xFF) + ((to & 0xFF) - (from & 0xFF)) * t); + return (a << 24) | (r << 16) | (g << 8) | b; + } + + private static int lightenColor(int color, float amount) { + return mixColor(color, (color & 0xFF000000) | 0x00FFFFFF, amount); } private void drawRoundedRect(GuiGraphics gui, int x, int y, int width, int height, int color) { diff --git a/src/main/java/net/fretux/mindmotion/client/SanityTempoOverlay.java b/src/main/java/net/fretux/mindmotion/client/SanityTempoOverlay.java index 525b5b6..ebcb05d 100644 --- a/src/main/java/net/fretux/mindmotion/client/SanityTempoOverlay.java +++ b/src/main/java/net/fretux/mindmotion/client/SanityTempoOverlay.java @@ -22,7 +22,9 @@ public class SanityTempoOverlay { if (mc.player == null || mc.options.hideGui) return; boolean tempoEnabled = ConfigMM.COMMON.ENABLE_TEMPO.get() && ClientData.TEMPO_ENABLED; boolean sanityEnabled = ConfigMM.COMMON.ENABLE_SANITY.get() && ClientData.SANITY_ENABLED; + boolean madnessEnabled = sanityEnabled && ConfigMM.CLIENT.ENABLE_MADNESS_BAR.get(); if (!tempoEnabled && !sanityEnabled) return; + ClientData.updateInterpolatedValues(); GuiGraphics gui = event.getGuiGraphics(); int width = event.getWindow().getGuiScaledWidth(); int height = event.getWindow().getGuiScaledHeight(); @@ -37,15 +39,19 @@ public class SanityTempoOverlay { int yTempo = baseY + ConfigMM.CLIENT.TEMPO_BAR_Y_OFFSET.get(); int xSanity = baseSanityX + ConfigMM.CLIENT.SANITY_BAR_X_OFFSET.get(); int ySanity = baseY + ConfigMM.CLIENT.SANITY_BAR_Y_OFFSET.get(); - float sanity = ClientData.SANITY; - float insanity = ClientData.INSANITY; - float tempo = ClientData.TEMPO; - float sanityPercent = sanity / ClientData.MAX_SANITY; - float insanityPercent = insanity / ClientData.MAX_SANITY; - float tempoPercent = (float) tempo / (float) ClientData.MAX_TEMPO; - int sanityFill = (int) (barWidth * sanityPercent); - int tempoFill = (int) (barWidth * tempoPercent); - int insanityFill = (int) (barWidth * insanityPercent); + float sanity = ClientData.getDisplayedSanity(); + float insanity = ClientData.getDisplayedInsanity(); + float madness = ClientData.getDisplayedMadness(); + float tempo = ClientData.getDisplayedTempo(); + float sanityPercent = clamp01(sanity / ClientData.MAX_SANITY); + float insanityPercent = clamp01(insanity / ClientData.MAX_SANITY); + float tempoPercent = clamp01(tempo / ClientData.MAX_TEMPO); + int sanityFill = Math.round(barWidth * sanityPercent); + int tempoFill = Math.round(barWidth * tempoPercent); + int insanityFill = Math.round(barWidth * insanityPercent); + double animationTime = System.nanoTime() / 1_000_000_000.0; + double mouseX = mc.mouseHandler.xpos() * width / (double) event.getWindow().getScreenWidth(); + double mouseY = mc.mouseHandler.ypos() * height / (double) event.getWindow().getScreenHeight(); RenderSystem.enableBlend(); if (tempoEnabled) { drawRoundedBar(gui, xTempo, yTempo, barWidth, barHeight, 0xFF33AFFF, tempoFill); @@ -63,25 +69,47 @@ public class SanityTempoOverlay { false ); } - boolean insaneMode = sanity <= 0; + boolean insaneMode = ClientData.SANITY <= 0f; + boolean showMadness = ClientData.MADNESS > 0f; if (sanityEnabled) { if (!insaneMode) { - drawRoundedBar(gui, xSanity, ySanity, barWidth, barHeight, 0xFFFFFFFF, sanityFill); + drawRoundedBar(gui, xSanity, ySanity, barWidth, barHeight, 0xFFE8E8E8, sanityFill); int sanityLabelWidth = mc.font.width("Sanity"); int sanityLabelX = xSanity + barWidth - sanityLabelWidth; gui.drawString(mc.font, Component.literal("Sanity"), sanityLabelX, ySanity - labelOffset, 0xFFFFFF, false); } else { - int purple = 0xFFAA33FF; - int darkPurple = 0xFF6611AA; + float insanityPulse = 0.92f + 0.08f * (0.5f + 0.5f * (float) Math.sin(animationTime * 3.5)); + int purple = scaleColor(0xFFAA33FF, insanityPulse); + int darkPurple = scaleColor(0xFF6611AA, insanityPulse); drawRoundedRect(gui, xSanity, ySanity, barWidth, barHeight, 0xDD000000); drawRoundedRect(gui, xSanity, ySanity, insanityFill, barHeight, purple, darkPurple); + drawTopHighlight(gui, xSanity, ySanity, insanityFill, lightenColor(purple, 0.18f)); int insanityLabelWidth = mc.font.width("Insanity"); int insanityLabelX = xSanity + barWidth - insanityLabelWidth; gui.drawString(mc.font, Component.literal("Insanity"), insanityLabelX, ySanity - labelOffset, 0xCC66FF, false); } } - double mouseX = mc.mouseHandler.xpos() * width / (double) event.getWindow().getScreenWidth(); - double mouseY = mc.mouseHandler.ypos() * height / (double) event.getWindow().getScreenHeight(); + if (madnessEnabled && showMadness) { + int madnessBarWidth = 182; + int madnessBarHeight = 7; + int madnessX = (width - madnessBarWidth) / 2 + ConfigMM.CLIENT.MADNESS_BAR_X_OFFSET.get(); + int madnessY = 13 + ConfigMM.CLIENT.MADNESS_BAR_Y_OFFSET.get(); + float madnessPercent = ClientData.MAX_MADNESS <= 0f + ? 0f + : clamp01(madness / ClientData.MAX_MADNESS); + int shakeX = madnessPercent >= 0.85f ? urgentShake(animationTime) : 0; + int renderedMadnessX = madnessX + shakeX; + drawBossStyleBar(gui, renderedMadnessX, madnessY, madnessBarWidth, madnessBarHeight, + madnessPercent, animationTime); + gui.drawCenteredString(mc.font, Component.literal("Madness"), + madnessX + madnessBarWidth / 2, madnessY - 11, 0xFFFFFF55); + + if (isMouseOver(mouseX, mouseY, renderedMadnessX, madnessY, madnessBarWidth, madnessBarHeight)) { + String text = String.format("%.0f%%", madnessPercent * 100f); + gui.drawCenteredString(mc.font, text, madnessX + madnessBarWidth / 2, + madnessY + madnessBarHeight + 3, 0xFFFFFF88); + } + } if (tempoEnabled && isMouseOver(mouseX, mouseY, xTempo, yTempo, barWidth, barHeight)) { String text = String.format("%.0f%%", tempoPercent * 100f); gui.drawCenteredString(mc.font, text, xTempo + barWidth / 2, yTempo - (labelOffset + 2), 0x66CCFF); @@ -100,18 +128,84 @@ public class SanityTempoOverlay { private static void drawRoundedBar(GuiGraphics gui, int x, int y, int width, int height, int color, int fillWidth) { int bg = 0xCC000000; drawRoundedRect(gui, x, y, width, height, bg); - int top = color; - int bottom = (color & 0x00FFFFFF) | 0xFF000000; - drawRoundedRect(gui, x, y, fillWidth, height, top, bottom); + if (fillWidth <= 0) return; + + int clampedFill = Math.min(width, fillWidth); + drawRoundedRect(gui, x, y, clampedFill, height, color, scaleColor(color, 0.78f)); + drawTopHighlight(gui, x, y, clampedFill, lightenColor(color, 0.18f)); + } + + private static void drawBossStyleBar(GuiGraphics gui, int x, int y, int width, int height, + float progress, double animationTime) { + gui.fill(x, y, x + width, y + height, 0xFF000000); + gui.fill(x + 1, y + 1, x + width - 1, y + height - 1, 0xFF4A4000); + + int innerWidth = width - 2; + int fillWidth = Math.round(innerWidth * progress); + if (fillWidth > 0) { + int fillColor = mixColor(0xFFAD8D2C, 0xFFFFF4B0, progress); + if (progress >= 0.85f) { + float urgentPulse = 1f + 0.16f + * (0.5f + 0.5f * (float) Math.sin(animationTime * 13.0)); + fillColor = scaleColor(fillColor, urgentPulse); + } + gui.fillGradient(x + 1, y + 1, x + 1 + fillWidth, y + height - 1, + lightenColor(fillColor, 0.16f), scaleColor(fillColor, 0.78f)); + } + } + + private static void drawTopHighlight(GuiGraphics gui, int x, int y, int fillWidth, int color) { + if (fillWidth <= 2) return; + gui.fill(x + 1, y, x + fillWidth - 1, y + 1, color); + } + + private static int urgentShake(double animationTime) { + long step = (long) (animationTime * 24.0); + return (int) Math.floorMod(step * 31L + 17L, 3L) - 1; + } + + private static int mixColor(int from, int to, float amount) { + float t = clamp01(amount); + int a = Math.round(((from >>> 24) & 0xFF) + (((to >>> 24) & 0xFF) - ((from >>> 24) & 0xFF)) * t); + int r = Math.round(((from >>> 16) & 0xFF) + (((to >>> 16) & 0xFF) - ((from >>> 16) & 0xFF)) * t); + int g = Math.round(((from >>> 8) & 0xFF) + (((to >>> 8) & 0xFF) - ((from >>> 8) & 0xFF)) * t); + int b = Math.round((from & 0xFF) + ((to & 0xFF) - (from & 0xFF)) * t); + return (a << 24) | (r << 16) | (g << 8) | b; + } + + private static int scaleColor(int color, float factor) { + int a = (color >>> 24) & 0xFF; + int r = Math.min(255, Math.max(0, Math.round(((color >>> 16) & 0xFF) * factor))); + int g = Math.min(255, Math.max(0, Math.round(((color >>> 8) & 0xFF) * factor))); + int b = Math.min(255, Math.max(0, Math.round((color & 0xFF) * factor))); + return (a << 24) | (r << 16) | (g << 8) | b; + } + + private static int lightenColor(int color, float amount) { + return mixColor(color, (color & 0xFF000000) | 0x00FFFFFF, amount); + } + + private static float clamp01(float value) { + return Math.max(0f, Math.min(1f, value)); } private static void drawRoundedRect(GuiGraphics gui, int x, int y, int width, int height, int color) { + if (width <= 0 || height <= 0) return; + if (width <= 2 || height <= 2) { + gui.fill(x, y, x + width, y + height, color); + return; + } gui.fill(x + 1, y, x + width - 1, y + height, color); // center gui.fill(x, y + 1, x + width, y + height - 1, color); // sides gui.fill(x + 1, y + 1, x + width - 1, y + height - 1, color); } private static void drawRoundedRect(GuiGraphics gui, int x, int y, int width, int height, int topColor, int bottomColor) { + if (width <= 0 || height <= 0) return; + if (width <= 2 || height <= 2) { + gui.fillGradient(x, y, x + width, y + height, topColor, bottomColor); + return; + } gui.fillGradient(x + 1, y, x + width - 1, y + height, topColor, bottomColor); gui.fillGradient(x, y + 1, x + width, y + height - 1, topColor, bottomColor); } diff --git a/src/main/java/net/fretux/mindmotion/compat/AscendCompat.java b/src/main/java/net/fretux/mindmotion/compat/AscendCompat.java index d0fa891..2d9f7cc 100644 --- a/src/main/java/net/fretux/mindmotion/compat/AscendCompat.java +++ b/src/main/java/net/fretux/mindmotion/compat/AscendCompat.java @@ -21,6 +21,8 @@ public final class AscendCompat { player.getCapability(PlayerCapabilityProvider.SANITY).ifPresent(cap -> { if (cap instanceof SanityCapability impl) { impl.setBonusMaxSanity(sanityBonus); + impl.setBonusMaxMadness(willpower * 0.5f); + impl.setBonusMadnessDecayPerTick(willpower * 0.001f); } }); } diff --git a/src/main/java/net/fretux/mindmotion/event/CommonEvents.java b/src/main/java/net/fretux/mindmotion/event/CommonEvents.java index c8e7486..faec8ab 100644 --- a/src/main/java/net/fretux/mindmotion/event/CommonEvents.java +++ b/src/main/java/net/fretux/mindmotion/event/CommonEvents.java @@ -1,16 +1,24 @@ package net.fretux.mindmotion.event; import net.fretux.mindmotion.ConfigMM; +import net.fretux.mindmotion.compat.IronsSpellbooksCompat; +import net.fretux.mindmotion.damage.ModDamageTypes; import net.fretux.mindmotion.player.PlayerCapabilityProvider; import net.fretux.mindmotion.player.SanityCapability; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.effect.MobEffectInstance; +import net.minecraft.world.effect.MobEffects; import net.minecraft.world.entity.monster.Monster; import net.minecraft.world.entity.npc.Villager; +import net.minecraftforge.event.entity.living.LivingDamageEvent; import net.minecraftforge.event.entity.living.LivingHurtEvent; import net.minecraftforge.event.entity.living.LivingDeathEvent; import net.minecraftforge.event.entity.player.PlayerEvent; import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.ModList; public class CommonEvents { + private static final int MADNESS_DECAY_DELAY_TICKS = 2 * 20; @SubscribeEvent public void onPlayerHurt(LivingHurtEvent event) { @@ -22,6 +30,64 @@ public class CommonEvents { player.getCapability(PlayerCapabilityProvider.SANITY).ifPresent(sanity -> sanity.reduceSanity(loss)); } + @SubscribeEvent + public void onPlayerDamaged(LivingDamageEvent event) { + if (!(event.getEntity() instanceof ServerPlayer player)) return; + if (!ConfigMM.COMMON.ENABLE_SANITY.get()) return; + if (!event.getSource().is(ModDamageTypes.TERROR) || event.getAmount() <= 0f) return; + + player.getCapability(PlayerCapabilityProvider.SANITY).ifPresent(sanity -> { + // One heart is two health points, so "2 x hearts taken" equals the + // final post-mitigation health-point damage reported by this event. + sanity.setMadness(sanity.getMadness() + event.getAmount()); + sanity.setMadnessDecayDelayTicks(MADNESS_DECAY_DELAY_TICKS); + if (sanity.getMadness() < sanity.getMaxMadness()) return; + + sanity.setMadness(0f); + // Defer the backlash until the next player tick. Calling hurt from + // inside LivingDamageEvent can complete two nested lethal hits and + // broadcast both of their death messages. + sanity.setMadnessBacklashPending(true); + }); + } + + public static void triggerPendingMadnessBacklash(ServerPlayer player) { + player.getCapability(PlayerCapabilityProvider.SANITY).ifPresent(sanity -> { + if (!sanity.isMadnessBacklashPending()) return; + + sanity.setMadnessBacklashPending(false); + if (!player.isAlive()) return; + + sanity.setMadnessStunTicks(30); + triggerMadnessBacklash(player); + }); + } + + private static void triggerMadnessBacklash(ServerPlayer player) { + if (!player.isAlive()) return; + + player.stopUsingItem(); + player.setSprinting(false); + player.setDeltaMovement(0.0, 0.0, 0.0); + player.hurtMarked = true; + player.addEffect(new MobEffectInstance(MobEffects.MOVEMENT_SLOWDOWN, 30, 255, false, false)); + + float backlashDamage = player.getMaxHealth() * 0.30f; + player.hurt(ModDamageTypes.madnessBacklash(player.level()), backlashDamage); + + int remainingFood = Math.max(0, (int) Math.floor(player.getFoodData().getFoodLevel() * 0.5f)); + player.getFoodData().setFoodLevel(remainingFood); + player.getFoodData().setSaturation(Math.min(player.getFoodData().getSaturationLevel(), remainingFood)); + + if (ModList.get().isLoaded("irons_spellbooks")) { + try { + IronsSpellbooksCompat.reduceMana(player, 0.30f); + } catch (LinkageError | RuntimeException ignored) { + // Optional integrations must never break the backlash itself. + } + } + } + @SubscribeEvent public void onLivingDeath(LivingDeathEvent event) { if (!(event.getSource().getEntity() instanceof net.minecraft.world.entity.player.Player player)) return; @@ -46,13 +112,24 @@ public class CommonEvents { if (newCap instanceof SanityCapability newImpl && oldCap instanceof SanityCapability oldImpl) { newImpl.setBaseMaxSanity(oldImpl.getBaseMaxSanity()); newImpl.setBonusMaxSanity(oldImpl.getBonusMaxSanity()); + newImpl.setBaseMaxMadness(oldImpl.getBaseMaxMadness()); + newImpl.setBonusMaxMadness(oldImpl.getBonusMaxMadness()); + newImpl.setBonusMadnessDecayPerTick(oldImpl.getBonusMadnessDecayPerTick()); } if (event.isWasDeath()) { newCap.setInsanity(0f); newCap.setSanity(newCap.getMaxSanity()); + newCap.setMadness(0f); + newCap.setMadnessDecayDelayTicks(0); + newCap.setMadnessStunTicks(0); + newCap.setMadnessBacklashPending(false); } else { newCap.setSanity(oldCap.getSanity()); newCap.setInsanity(oldCap.getInsanity()); + newCap.setMadness(oldCap.getMadness()); + newCap.setMadnessDecayDelayTicks(oldCap.getMadnessDecayDelayTicks()); + newCap.setMadnessStunTicks(oldCap.getMadnessStunTicks()); + newCap.setMadnessBacklashPending(oldCap.isMadnessBacklashPending()); } }) ); diff --git a/src/main/java/net/fretux/mindmotion/event/PlayerTickHandler.java b/src/main/java/net/fretux/mindmotion/event/PlayerTickHandler.java index 845c395..b86d78d 100644 --- a/src/main/java/net/fretux/mindmotion/event/PlayerTickHandler.java +++ b/src/main/java/net/fretux/mindmotion/event/PlayerTickHandler.java @@ -1,6 +1,7 @@ package net.fretux.mindmotion.event; import net.fretux.mindmotion.ConfigMM; +import net.fretux.mindmotion.damage.ModDamageTypes; import net.fretux.mindmotion.network.ModMessages; import net.fretux.mindmotion.network.SyncStatsS2CPacket; import net.fretux.mindmotion.player.PlayerCapabilityProvider; @@ -81,6 +82,11 @@ public class PlayerTickHandler { boolean sanityEnabled = ConfigMM.COMMON.ENABLE_SANITY.get(); boolean tempoEnabled = ConfigMM.COMMON.ENABLE_TEMPO.get(); + handleMadnessStun(player); + if (player instanceof ServerPlayer serverPlayer) { + CommonEvents.triggerPendingMadnessBacklash(serverPlayer); + } + handleMadnessDecay(player); if (sanityEnabled && sanityTick) handleSanityAndInsanity(player); if (tempoEnabled) { handleTempoTick(player, tick); @@ -100,7 +106,11 @@ public class PlayerTickHandler { sanity.getMaxSanity(), tempo.getMaxTempo(), sanityEnabled, - tempoEnabled + tempoEnabled, + sanity.getMadness(), + sanity.getMaxMadness(), + sanity.getMadnessDecayPerTick(), + sanity.getMadnessDecayDelayTicks() ), serverPlayer.connection.connection, net.minecraftforge.network.NetworkDirection.PLAY_TO_CLIENT @@ -216,7 +226,7 @@ public class PlayerTickHandler { int ticks = insanityDamageTicks.getOrDefault(id, 0) + SANITY_TICK_INTERVAL; insanityDamageTicks.put(id, ticks); float damage = 1.0f + (ticks / 100f); - player.hurt(player.damageSources().magic(), damage); + player.hurt(ModDamageTypes.terror(player.level()), damage); player.addEffect(new MobEffectInstance(MobEffects.MOVEMENT_SLOWDOWN, 40, 1)); return; } @@ -289,6 +299,32 @@ public class PlayerTickHandler { }); } + private static void handleMadnessStun(Player player) { + player.getCapability(PlayerCapabilityProvider.SANITY).ifPresent(sanity -> { + int stunTicks = sanity.getMadnessStunTicks(); + if (stunTicks <= 0) return; + + sanity.setMadnessStunTicks(stunTicks - 1); + player.stopUsingItem(); + player.setSprinting(false); + player.setDeltaMovement(0.0, 0.0, 0.0); + player.hurtMarked = true; + }); + } + + private static void handleMadnessDecay(Player player) { + player.getCapability(PlayerCapabilityProvider.SANITY).ifPresent(sanity -> { + int delayTicks = sanity.getMadnessDecayDelayTicks(); + if (delayTicks > 0) { + sanity.setMadnessDecayDelayTicks(delayTicks - 1); + return; + } + if (sanity.getMadness() > 0f) { + sanity.setMadness(sanity.getMadness() - sanity.getMadnessDecayPerTick()); + } + }); + } + private static float getSurvivalSanityDelta(Player player, int light) { float delta = 0f; float healthPercent = player.getHealth() / player.getMaxHealth(); @@ -483,14 +519,14 @@ public class PlayerTickHandler { private static void applyPanic(Player player) { player.addEffect(new MobEffectInstance(MobEffects.MOVEMENT_SLOWDOWN, 20, 1)); - player.hurt(player.damageSources().magic(), 1f); + player.hurt(ModDamageTypes.terror(player.level()), 1f); applyShiver(player); applyVertigo(player); } private static void applyScratching(Player player) { player.addEffect(new MobEffectInstance(MobEffects.MOVEMENT_SLOWDOWN, 30, 1)); - player.hurt(player.damageSources().magic(), 2f); + player.hurt(ModDamageTypes.terror(player.level()), 2f); applyShiver(player); applyDread(player); } diff --git a/src/main/java/net/fretux/mindmotion/network/ModMessages.java b/src/main/java/net/fretux/mindmotion/network/ModMessages.java index ab4d49e..14d31cf 100644 --- a/src/main/java/net/fretux/mindmotion/network/ModMessages.java +++ b/src/main/java/net/fretux/mindmotion/network/ModMessages.java @@ -9,7 +9,7 @@ import net.fretux.mindmotion.AscendMindMotion; public class ModMessages { - private static final String PROTOCOL_VERSION = "2"; + private static final String PROTOCOL_VERSION = "5"; public static final SimpleChannel CHANNEL = NetworkRegistry.newSimpleChannel( ResourceLocation.fromNamespaceAndPath(AscendMindMotion.MODID, "main"), () -> PROTOCOL_VERSION, diff --git a/src/main/java/net/fretux/mindmotion/network/SyncStatsS2CPacket.java b/src/main/java/net/fretux/mindmotion/network/SyncStatsS2CPacket.java index 58baf39..c3e3935 100644 --- a/src/main/java/net/fretux/mindmotion/network/SyncStatsS2CPacket.java +++ b/src/main/java/net/fretux/mindmotion/network/SyncStatsS2CPacket.java @@ -15,6 +15,10 @@ public class SyncStatsS2CPacket { private final int maxTempo; private final boolean sanityEnabled; private final boolean tempoEnabled; + private final float madness; + private final float maxMadness; + private final float madnessDecayPerTick; + private final int madnessDecayDelayTicks; public SyncStatsS2CPacket(float sanity, float insanity, @@ -23,7 +27,11 @@ public class SyncStatsS2CPacket { float maxSanity, int maxTempo, boolean sanityEnabled, - boolean tempoEnabled) { + boolean tempoEnabled, + float madness, + float maxMadness, + float madnessDecayPerTick, + int madnessDecayDelayTicks) { this.sanity = sanity; this.insanity = insanity; this.tempo = tempo; @@ -32,6 +40,10 @@ public class SyncStatsS2CPacket { this.maxTempo = maxTempo; this.sanityEnabled = sanityEnabled; this.tempoEnabled = tempoEnabled; + this.madness = madness; + this.maxMadness = maxMadness; + this.madnessDecayPerTick = madnessDecayPerTick; + this.madnessDecayDelayTicks = madnessDecayDelayTicks; } public static void encode(SyncStatsS2CPacket msg, FriendlyByteBuf buf) { @@ -43,6 +55,10 @@ public class SyncStatsS2CPacket { buf.writeInt(msg.maxTempo); buf.writeBoolean(msg.sanityEnabled); buf.writeBoolean(msg.tempoEnabled); + buf.writeFloat(msg.madness); + buf.writeFloat(msg.maxMadness); + buf.writeFloat(msg.madnessDecayPerTick); + buf.writeInt(msg.madnessDecayDelayTicks); } public static SyncStatsS2CPacket decode(FriendlyByteBuf buf) { @@ -54,7 +70,12 @@ public class SyncStatsS2CPacket { int maxTempo = buf.readInt(); boolean sanityEnabled = buf.readBoolean(); boolean tempoEnabled = buf.readBoolean(); - return new SyncStatsS2CPacket(sanity, insanity, tempo, ventCooldown, maxSanity, maxTempo, sanityEnabled, tempoEnabled); + float madness = buf.readFloat(); + float maxMadness = buf.readFloat(); + float madnessDecayPerTick = buf.readFloat(); + int madnessDecayDelayTicks = buf.readInt(); + return new SyncStatsS2CPacket(sanity, insanity, tempo, ventCooldown, maxSanity, maxTempo, + sanityEnabled, tempoEnabled, madness, maxMadness, madnessDecayPerTick, madnessDecayDelayTicks); } public static void handle(SyncStatsS2CPacket msg, Supplier ctx) { @@ -67,6 +88,10 @@ public class SyncStatsS2CPacket { ClientData.MAX_TEMPO = msg.maxTempo; ClientData.SANITY_ENABLED = msg.sanityEnabled; ClientData.TEMPO_ENABLED = msg.tempoEnabled; + ClientData.setMadness(msg.madness); + ClientData.MAX_MADNESS = msg.maxMadness; + ClientData.MADNESS_DECAY_PER_TICK = msg.madnessDecayPerTick; + ClientData.MADNESS_DECAY_DELAY_TICKS = msg.madnessDecayDelayTicks; }); ctx.get().setPacketHandled(true); @@ -78,7 +103,11 @@ public class SyncStatsS2CPacket { ", MaxSanity: " + msg.maxSanity + ", MaxTempo: " + msg.maxTempo + ", SanityEnabled: " + msg.sanityEnabled + - ", TempoEnabled: " + msg.tempoEnabled + ", TempoEnabled: " + msg.tempoEnabled + + ", Madness: " + msg.madness + + ", MaxMadness: " + msg.maxMadness + + ", MadnessDecayPerTick: " + msg.madnessDecayPerTick + + ", MadnessDecayDelayTicks: " + msg.madnessDecayDelayTicks ); } } diff --git a/src/main/java/net/fretux/mindmotion/player/ISanity.java b/src/main/java/net/fretux/mindmotion/player/ISanity.java index d3f45a5..fd1cfa4 100644 --- a/src/main/java/net/fretux/mindmotion/player/ISanity.java +++ b/src/main/java/net/fretux/mindmotion/player/ISanity.java @@ -6,6 +6,16 @@ public interface ISanity { float getInsanity(); void setInsanity(float insanity); float getMaxSanity(); + float getMadness(); + void setMadness(float madness); + float getMaxMadness(); + float getMadnessDecayPerTick(); + int getMadnessDecayDelayTicks(); + void setMadnessDecayDelayTicks(int ticks); + int getMadnessStunTicks(); + void setMadnessStunTicks(int ticks); + boolean isMadnessBacklashPending(); + void setMadnessBacklashPending(boolean pending); void addSanity(float amount); void reduceSanity(float amount); } diff --git a/src/main/java/net/fretux/mindmotion/player/PlayerCapabilityProvider.java b/src/main/java/net/fretux/mindmotion/player/PlayerCapabilityProvider.java index 699e480..9e916d1 100644 --- a/src/main/java/net/fretux/mindmotion/player/PlayerCapabilityProvider.java +++ b/src/main/java/net/fretux/mindmotion/player/PlayerCapabilityProvider.java @@ -44,6 +44,13 @@ public class PlayerCapabilityProvider { tag.putFloat("BonusMaxSanity", instance.getBonusMaxSanity()); tag.putFloat("Sanity", instance.getSanity()); tag.putFloat("Insanity", instance.getInsanity()); + tag.putFloat("BaseMaxMadness", instance.getBaseMaxMadness()); + tag.putFloat("BonusMaxMadness", instance.getBonusMaxMadness()); + tag.putFloat("BonusMadnessDecayPerTick", instance.getBonusMadnessDecayPerTick()); + tag.putFloat("Madness", instance.getMadness()); + tag.putInt("MadnessDecayDelayTicks", instance.getMadnessDecayDelayTicks()); + tag.putInt("MadnessStunTicks", instance.getMadnessStunTicks()); + tag.putBoolean("MadnessBacklashPending", instance.isMadnessBacklashPending()); return tag; } @@ -55,8 +62,29 @@ public class PlayerCapabilityProvider { if (nbt.contains("BonusMaxSanity")) { instance.setBonusMaxSanity(nbt.getFloat("BonusMaxSanity")); } + if (nbt.contains("BaseMaxMadness")) { + instance.setBaseMaxMadness(nbt.getFloat("BaseMaxMadness")); + } + if (nbt.contains("BonusMaxMadness")) { + instance.setBonusMaxMadness(nbt.getFloat("BonusMaxMadness")); + } + if (nbt.contains("BonusMadnessDecayPerTick")) { + instance.setBonusMadnessDecayPerTick(nbt.getFloat("BonusMadnessDecayPerTick")); + } instance.setSanity(nbt.getFloat("Sanity")); instance.setInsanity(nbt.getFloat("Insanity")); + if (nbt.contains("Madness")) { + instance.setMadness(nbt.getFloat("Madness")); + } + if (nbt.contains("MadnessDecayDelayTicks")) { + instance.setMadnessDecayDelayTicks(nbt.getInt("MadnessDecayDelayTicks")); + } + if (nbt.contains("MadnessStunTicks")) { + instance.setMadnessStunTicks(nbt.getInt("MadnessStunTicks")); + } + if (nbt.contains("MadnessBacklashPending")) { + instance.setMadnessBacklashPending(nbt.getBoolean("MadnessBacklashPending")); + } } } diff --git a/src/main/java/net/fretux/mindmotion/player/SanityCapability.java b/src/main/java/net/fretux/mindmotion/player/SanityCapability.java index 976bce1..3427247 100644 --- a/src/main/java/net/fretux/mindmotion/player/SanityCapability.java +++ b/src/main/java/net/fretux/mindmotion/player/SanityCapability.java @@ -5,6 +5,13 @@ public class SanityCapability implements ISanity { private float insanity = 0f; private float baseMaxSanity = 80f; private float bonusMaxSanity = 0f; + private float madness = 0f; + private float baseMaxMadness = 20f; + private float bonusMaxMadness = 0f; + private float bonusMadnessDecayPerTick = 0f; + private int madnessDecayDelayTicks = 0; + private int madnessStunTicks = 0; + private boolean madnessBacklashPending = false; @Override public float getSanity() { @@ -41,6 +48,56 @@ public class SanityCapability implements ISanity { return baseMaxSanity + bonusMaxSanity; } + @Override + public float getMadness() { + return madness; + } + + @Override + public void setMadness(float madness) { + this.madness = Math.max(0f, Math.min(getMaxMadness(), madness)); + } + + @Override + public float getMaxMadness() { + return baseMaxMadness + bonusMaxMadness; + } + + @Override + public float getMadnessDecayPerTick() { + return 0.01f + bonusMadnessDecayPerTick; + } + + @Override + public int getMadnessDecayDelayTicks() { + return madnessDecayDelayTicks; + } + + @Override + public void setMadnessDecayDelayTicks(int ticks) { + this.madnessDecayDelayTicks = Math.max(0, ticks); + } + + @Override + public int getMadnessStunTicks() { + return madnessStunTicks; + } + + @Override + public void setMadnessStunTicks(int ticks) { + this.madnessStunTicks = Math.max(0, ticks); + } + + @Override + public boolean isMadnessBacklashPending() { + return madnessBacklashPending; + } + + @Override + public void setMadnessBacklashPending(boolean pending) { + this.madnessBacklashPending = pending; + } + @Override public void addSanity(float amount) { setSanity(this.sanity + amount); @@ -70,6 +127,32 @@ public class SanityCapability implements ISanity { return bonusMaxSanity; } + public void setBaseMaxMadness(float baseMaxMadness) { + this.baseMaxMadness = Math.max(1f, baseMaxMadness); + setMadness(this.madness); + } + + public float getBaseMaxMadness() { + return baseMaxMadness; + } + + public void setBonusMaxMadness(float bonusMaxMadness) { + this.bonusMaxMadness = Math.max(0f, bonusMaxMadness); + setMadness(this.madness); + } + + public float getBonusMaxMadness() { + return bonusMaxMadness; + } + + public void setBonusMadnessDecayPerTick(float bonusMadnessDecayPerTick) { + this.bonusMadnessDecayPerTick = Math.max(0f, bonusMadnessDecayPerTick); + } + + public float getBonusMadnessDecayPerTick() { + return bonusMadnessDecayPerTick; + } + private void clampToMax() { float max = getMaxSanity(); this.sanity = Math.max(0, Math.min(max, this.sanity)); diff --git a/src/main/resources/assets/mindmotion/lang/en_us.json b/src/main/resources/assets/mindmotion/lang/en_us.json index ec4eab8..9e0f03c 100644 --- a/src/main/resources/assets/mindmotion/lang/en_us.json +++ b/src/main/resources/assets/mindmotion/lang/en_us.json @@ -10,5 +10,9 @@ "screen.mindmotion.hud_editor.tempo_y": "Tempo Y Offset: %s", "screen.mindmotion.hud_editor.sanity_x": "Sanity X Offset: %s", "screen.mindmotion.hud_editor.sanity_y": "Sanity Y Offset: %s", - "screen.mindmotion.hud_editor.reset": "Reset" + "screen.mindmotion.hud_editor.madness_x": "Madness X Offset: %s", + "screen.mindmotion.hud_editor.madness_y": "Madness Y Offset: %s", + "screen.mindmotion.hud_editor.reset": "Reset", + "death.attack.mindmotion.terror": "%1$s was consumed by terror", + "death.attack.mindmotion.madness_backlash": "%1$s succumbed to madness" }