Weiter gearbeitet an allem möglichen

This commit is contained in:
2026-07-06 07:11:55 +02:00
parent 76a192df67
commit 06bb955c78
22 changed files with 987 additions and 0 deletions

View File

@@ -0,0 +1,281 @@
package de.blight.game.state;
import com.jme3.app.Application;
import com.jme3.app.SimpleApplication;
import com.jme3.app.state.BaseAppState;
import com.jme3.bullet.control.CharacterControl;
import com.jme3.material.Material;
import com.jme3.material.RenderState;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector3f;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.scene.Geometry;
import com.jme3.scene.shape.Quad;
import de.blight.common.model.MainCharacter;
import de.blight.game.control.PlayerInputControl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Ertrink-Sequenz:
* 1. MONITORING Tiefe überwachen
* 2. FADING_OUT Tiefe > 1,5 m: Eingabe weg, Charakter läuft weiter,
* Bild + Ton in 3 s ausblenden
* 3. BLACK 2 s Schwarzbild, dann Teleport + HP/Mana/Stamina=1
* 4. WAIT_PHYSICS 1,5 s warten damit Terrain-Physik lädt
* 5. FADING_IN 3 s Einblenden (REVIVE eingefroren)
* 6. REVIVE_PLAYING REVIVE-Animation läuft durch
* → zurück zu MONITORING
*/
public class DrownState extends BaseAppState {
private static final Logger log = LoggerFactory.getLogger(DrownState.class);
/** Tiefe (m) die den Ertrink-Vorgang auslöst. */
private static final float TRIGGER_DEPTH = 1.5f;
private static final float FADE_OUT_DUR = 3.0f;
private static final float BLACK_DUR = 2.0f;
private static final float WAIT_PHYSICS_DUR = 1.5f;
private static final float FADE_IN_DUR = 3.0f;
// Strand-Suche
private static final float MAX_BEACH_H = 5f;
private static final float MAX_SLOPE_DEG = 10f;
private static final float SCAN_STEP_R = 3f;
private static final float SCAN_MAX_R = 300f;
private static final int SCAN_DIRS = 16;
private final TerrainChunkState terrain;
private final CharacterControl physicsChar;
private final PlayerInputControl playerInput;
private final MainCharacter mainCharacter;
private SimpleApplication app;
private Geometry overlay;
private Material overlayMat;
/** Gesetzt von WorldScene nach setupAnimationContext(). */
private String reviveClip = null;
private float reviveLength = 0f;
private enum Phase { MONITORING, FADING_OUT, BLACK, WAIT_PHYSICS, FADING_IN, REVIVE_PLAYING }
private Phase phase = Phase.MONITORING;
private float timer = 0f;
public DrownState(TerrainChunkState terrain, CharacterControl physicsChar,
PlayerInputControl playerInput, MainCharacter mainCharacter) {
this.terrain = terrain;
this.physicsChar = physicsChar;
this.playerInput = playerInput;
this.mainCharacter = mainCharacter;
}
public void setReviveInfo(String clip, float length) {
this.reviveClip = clip;
this.reviveLength = length;
log.info("[DrownState] REVIVE-Info: clip='{}' {}s", clip, length);
}
@Override
protected void initialize(Application application) {
app = (SimpleApplication) application;
float w = app.getCamera().getWidth();
float h = app.getCamera().getHeight();
overlay = new Geometry("drown_overlay", new Quad(w, h));
overlayMat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
overlayMat.setColor("Color", new ColorRGBA(0f, 0f, 0f, 0f));
overlayMat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
overlay.setMaterial(overlayMat);
overlay.setQueueBucket(RenderQueue.Bucket.Gui);
overlay.setLocalTranslation(0f, 0f, 50f);
}
@Override
protected void cleanup(Application application) {
detachOverlay();
restoreVolume();
}
@Override protected void onEnable() {}
@Override protected void onDisable() {}
@Override
public void update(float tpf) {
switch (phase) {
case MONITORING -> updateMonitoring();
case FADING_OUT -> updateFadingOut(tpf);
case BLACK -> updateBlack(tpf);
case WAIT_PHYSICS -> updateWaitPhysics(tpf);
case FADING_IN -> updateFadingIn(tpf);
case REVIVE_PLAYING -> updateRevivePlaying(tpf);
}
}
// ── MONITORING ────────────────────────────────────────────────────────────
private void updateMonitoring() {
if (physicsChar == null) return;
Vector3f pos = physicsChar.getPhysicsLocation();
float depth = Math.max(0f, -terrain.getHeightAt(pos.x, pos.z));
if (depth >= TRIGGER_DEPTH) {
log.info("[DrownState] Tiefe {}m Ertrink-Sequenz gestartet", depth);
playerInput.blockInputsKeepMoving();
attachOverlay();
setAlpha(0f);
setListenerScale(1f);
timer = FADE_OUT_DUR;
phase = Phase.FADING_OUT;
}
}
// ── FADING_OUT (3 s) ──────────────────────────────────────────────────────
private void updateFadingOut(float tpf) {
timer -= tpf;
float alpha = Math.max(0f, 1f - timer / FADE_OUT_DUR);
setAlpha(alpha);
setListenerScale(1f - alpha);
if (timer <= 0f) {
setAlpha(1f);
setListenerScale(0f);
timer = BLACK_DUR;
phase = Phase.BLACK;
}
}
// ── BLACK (2 s) ───────────────────────────────────────────────────────────
private void updateBlack(float tpf) {
timer -= tpf;
if (timer <= 0f) {
executeDrown();
}
}
private void executeDrown() {
// Vollständig blockieren (stoppt auch Laufrichtung)
playerInput.blockForIntro();
if (mainCharacter != null) {
mainCharacter.setCurrentHp(1);
mainCharacter.setCurrentMana(1);
mainCharacter.setCurrentStamina(1);
}
Vector3f pos = physicsChar.getPhysicsLocation();
Vector3f beach = findNearestBeach(pos.x, pos.z);
physicsChar.setPhysicsLocation(beach);
playerInput.setGroundGrace(60);
log.info("[DrownState] Teleportiert zu {}", beach);
timer = WAIT_PHYSICS_DUR;
phase = Phase.WAIT_PHYSICS;
}
// ── WAIT_PHYSICS (1,5 s) ──────────────────────────────────────────────────
private void updateWaitPhysics(float tpf) {
timer -= tpf;
if (timer <= 0f) {
if (reviveClip != null) {
playerInput.startFrozenRevive(reviveClip);
}
timer = FADE_IN_DUR;
phase = Phase.FADING_IN;
}
}
// ── FADING_IN (3 s) ───────────────────────────────────────────────────────
private void updateFadingIn(float tpf) {
timer -= tpf;
float alpha = Math.max(0f, timer / FADE_IN_DUR);
setAlpha(alpha);
setListenerScale(1f - alpha);
if (timer <= 0f) {
detachOverlay();
restoreVolume();
if (reviveClip != null) {
playerInput.unfreezeRevive();
timer = reviveLength;
phase = Phase.REVIVE_PLAYING;
} else {
playerInput.unblockInputs();
phase = Phase.MONITORING;
}
}
}
// ── REVIVE_PLAYING ────────────────────────────────────────────────────────
private void updateRevivePlaying(float tpf) {
timer -= tpf;
if (timer <= 0f) {
playerInput.unblockInputs();
phase = Phase.MONITORING;
log.info("[DrownState] Sequenz abgeschlossen Spieler freigegeben");
}
}
// ── Hilfsmethoden ─────────────────────────────────────────────────────────
private void attachOverlay() {
if (overlay.getParent() == null) app.getGuiNode().attachChild(overlay);
}
private void detachOverlay() {
if (overlay != null && overlay.getParent() != null) app.getGuiNode().detachChild(overlay);
}
private void setAlpha(float alpha) {
overlayMat.setColor("Color", new ColorRGBA(0f, 0f, 0f, alpha));
}
private void setListenerScale(float scale) {
AudioSettingsState as = app.getStateManager().getState(AudioSettingsState.class);
float master = (as != null) ? as.getMaster() : 1f;
app.getListener().setVolume(master * Math.max(0f, Math.min(1f, scale)));
}
private void restoreVolume() {
AudioSettingsState as = app.getStateManager().getState(AudioSettingsState.class);
float master = (as != null) ? as.getMaster() : 1f;
app.getListener().setVolume(master);
}
/**
* Sucht radial vom Ausgangspunkt aus den nächsten flachen Landpunkt.
* Nahe Punkte liegen im selben Terrain-Chunk → Physik ist bereits geladen.
*/
private Vector3f findNearestBeach(float startX, float startZ) {
float maxSlope = (float) Math.tan(Math.toRadians(MAX_SLOPE_DEG));
float slopeStep = 4f;
double angleStep = 2 * Math.PI / SCAN_DIRS;
for (float r = SCAN_STEP_R; r <= SCAN_MAX_R; r += SCAN_STEP_R) {
for (int d = 0; d < SCAN_DIRS; d++) {
float x = startX + r * (float) Math.cos(d * angleStep);
float z = startZ + r * (float) Math.sin(d * angleStep);
float h = terrain.getHeightAt(x, z);
if (h < 0.2f || h > MAX_BEACH_H) continue;
float hxp = terrain.getHeightAt(x + slopeStep, z);
float hxn = terrain.getHeightAt(x - slopeStep, z);
float hzp = terrain.getHeightAt(x, z + slopeStep);
float hzn = terrain.getHeightAt(x, z - slopeStep);
float dhdx = (hxp - hxn) / (2f * slopeStep);
float dhdz = (hzp - hzn) / (2f * slopeStep);
float slope = (float) Math.sqrt(dhdx * dhdx + dhdz * dhdz);
if (slope > maxSlope) continue;
log.info("[DrownState] Strandpunkt: ({}, {}), h={}, r={}m", x, z, h, r);
return new Vector3f(x, h + 1.0f, z);
}
}
log.warn("[DrownState] Kein Strandpunkt gefunden Fallback");
return new Vector3f(startX, Math.max(terrain.getHeightAt(startX, startZ), 0f) + 3f, startZ);
}
}

View File

@@ -0,0 +1,286 @@
package de.blight.game.state;
import com.jme3.app.Application;
import com.jme3.app.SimpleApplication;
import com.jme3.app.state.BaseAppState;
import com.jme3.material.Material;
import com.jme3.material.RenderState;
import com.jme3.math.ColorRGBA;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.VertexBuffer;
import com.jme3.texture.Texture;
import com.jme3.ui.Picture;
import com.jme3.util.BufferUtils;
import de.blight.common.model.MainCharacter;
import de.blight.game.animation.AnimationLibrary;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.nio.FloatBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
/**
* HUD-Balken (unten rechts): Health (rot), Stamina (gelb), Mana (blau).
* PNG-basiert Dateien in Textures/hud/ können durch eigene Grafiken ersetzt werden.
*/
public class HudState extends BaseAppState {
private static final Logger log = LoggerFactory.getLogger(HudState.class);
// Dimensionen des Fülbereichs (ohne Rahmen)
private static final float BAR_W = 200f;
private static final float BAR_H = 18f;
private static final float BORDER = 2f;
private static final float MARGIN = 12f;
private static final float GAP = 5f;
private static final float SLOT_W = BAR_W + BORDER * 2;
private static final float SLOT_H = BAR_H + BORDER * 2;
private static final String[] FILL_ASSETS = {
"Textures/hud/bar_fill_health.png",
"Textures/hud/bar_fill_stamina.png",
"Textures/hud/bar_fill_mana.png",
};
private static final ColorRGBA[] FILL_COLORS = {
new ColorRGBA(0.85f, 0.10f, 0.10f, 1f),
new ColorRGBA(0.90f, 0.80f, 0.10f, 1f),
new ColorRGBA(0.10f, 0.30f, 0.90f, 1f),
};
private final MainCharacter mc;
private SimpleApplication app;
private Node hudNode;
private final Geometry[] fills = new Geometry[3];
private final float[] lastRatios = { -1f, -1f, -1f };
public HudState(MainCharacter mc) {
this.mc = mc;
}
@Override
protected void initialize(Application application) {
app = (SimpleApplication) application;
ensureAssets();
hudNode = new Node("hud_bars");
buildBars();
app.getGuiNode().attachChild(hudNode);
}
@Override
protected void cleanup(Application application) {
if (hudNode.getParent() != null) {
app.getGuiNode().detachChild(hudNode);
}
}
@Override
protected void onEnable() {
hudNode.setCullHint(Spatial.CullHint.Inherit);
}
@Override
protected void onDisable() {
hudNode.setCullHint(Spatial.CullHint.Always);
}
@Override
public void update(float tpf) {
if (mc == null) return;
int maxHp = mc.getMaxHp();
int maxSt = mc.getMaxStamina();
int maxMn = mc.getMaxMana();
applyRatio(0, maxHp > 0 ? (float) mc.getCurrentHp() / maxHp : 0f);
applyRatio(1, maxSt > 0 ? (float) mc.getCurrentStamina() / maxSt : 0f);
applyRatio(2, maxMn > 0 ? (float) mc.getCurrentMana() / maxMn : 0f);
}
// ── Aufbau ───────────────────────────────────────────────────────────────────
private void buildBars() {
float screenW = app.getCamera().getWidth();
// Stapelung von unten: idx=2 (mana) ganz unten, idx=0 (health) ganz oben
for (int i = 0; i < 3; i++) {
int stackPos = 2 - i; // health → stack 2 (höchste Y), mana → stack 0
float x = screenW - MARGIN - SLOT_W;
float y = MARGIN + stackPos * (SLOT_H + GAP);
buildBar(i, x, y);
}
}
private void buildBar(int idx, float x, float y) {
// Rahmen (Frame) transparent innen, Rand sichtbar
Picture frame = loadPicture("Textures/hud/bar_frame.png", SLOT_W, SLOT_H,
new ColorRGBA(0.65f, 0.65f, 0.65f, 1f));
frame.setLocalTranslation(x, y, 1f);
hudNode.attachChild(frame);
// Hintergrund (schwarz)
Picture bg = loadPicture("Textures/hud/bar_bg.png", BAR_W, BAR_H, ColorRGBA.Black);
bg.setLocalTranslation(x + BORDER, y + BORDER, 2f);
hudNode.attachChild(bg);
// Füllbalken (UV-geclippt, kein Strecken des Textur-Gradienten)
Geometry fill = buildFillGeometry(idx, x + BORDER, y + BORDER, 3f);
fills[idx] = fill;
hudNode.attachChild(fill);
}
private Geometry buildFillGeometry(int idx, float x, float y, float z) {
Mesh mesh = new Mesh();
fillMesh(mesh, BAR_W, 1f); // initial: voll
Material mat;
try {
Texture tex = app.getAssetManager().loadTexture(FILL_ASSETS[idx]);
tex.setWrap(Texture.WrapMode.EdgeClamp);
mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
mat.setTexture("ColorMap", tex);
} catch (Exception e) {
mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", FILL_COLORS[idx]);
}
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
Geometry g = new Geometry("hud_fill_" + idx, mesh);
g.setMaterial(mat);
g.setQueueBucket(RenderQueue.Bucket.Gui);
g.setLocalTranslation(x, y, z);
return g;
}
/** Setzt Breite und UV-Clipping des Fill-Meshes auf den gegebenen Ratio (01). */
private void applyRatio(int idx, float ratio) {
ratio = Math.max(0f, Math.min(1f, ratio));
if (Math.abs(ratio - lastRatios[idx]) < 0.001f) return;
lastRatios[idx] = ratio;
Geometry g = fills[idx];
float w = BAR_W * ratio;
Mesh mesh = g.getMesh();
FloatBuffer pos = (FloatBuffer) mesh.getBuffer(VertexBuffer.Type.Position).getData();
pos.put(3, w); // Vertex 1: x
pos.put(6, w); // Vertex 2: x
mesh.getBuffer(VertexBuffer.Type.Position).setUpdateNeeded();
// UV-Clipping: nur ratio-Anteil der Textur sichtbar (kein Strecken)
FloatBuffer uv = (FloatBuffer) mesh.getBuffer(VertexBuffer.Type.TexCoord).getData();
uv.put(2, ratio); // UV 1: u
uv.put(4, ratio); // UV 2: u
mesh.getBuffer(VertexBuffer.Type.TexCoord).setUpdateNeeded();
mesh.updateBound();
}
/** Erstellt ein Quad-Mesh mit UV-Koordinaten die bei maxU enden (für Clipping). */
private static void fillMesh(Mesh mesh, float w, float maxU) {
float h = BAR_H;
mesh.setBuffer(VertexBuffer.Type.Position, 3,
BufferUtils.createFloatBuffer(
0, 0, 0,
w, 0, 0,
w, h, 0,
0, h, 0
));
mesh.setBuffer(VertexBuffer.Type.TexCoord, 2,
BufferUtils.createFloatBuffer(
0f, 0f,
maxU, 0f,
maxU, 1f,
0f, 1f
));
mesh.setBuffer(VertexBuffer.Type.Index, 3,
BufferUtils.createShortBuffer((short)0,(short)1,(short)2,(short)0,(short)2,(short)3));
mesh.setMode(Mesh.Mode.Triangles);
mesh.updateBound();
}
// ── PNG-Loader ────────────────────────────────────────────────────────────────
private Picture loadPicture(String assetPath, float w, float h, ColorRGBA fallback) {
Picture p = new Picture("hud_" + assetPath);
try {
p.setImage(app.getAssetManager(), assetPath, true);
} catch (Exception e) {
Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", fallback);
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
p.setMaterial(mat);
}
p.setWidth(w);
p.setHeight(h);
p.setQueueBucket(RenderQueue.Bucket.Gui);
return p;
}
// ── Placeholder-PNGs erzeugen ─────────────────────────────────────────────────
private void ensureAssets() {
Path root = AnimationLibrary.findAssetRoot();
Path dir = root.resolve("Textures").resolve("hud");
try {
Files.createDirectories(dir);
} catch (IOException e) {
log.warn("[HUD] Verzeichnis nicht erstellbar: {}", dir);
return;
}
// Rahmen: grauer Rand, transparente Mitte
ensureFrame(dir.resolve("bar_frame.png"), (int) SLOT_W, (int) SLOT_H, (int) BORDER,
180, 180, 180);
// Hintergrund: schwarz
ensureSolid(dir.resolve("bar_bg.png"), 4, 4, 0, 0, 0, 255);
// Füllfarben (kleine Kacheln, werden skaliert/geclippt)
ensureSolid(dir.resolve("bar_fill_health.png"), 4, 4, 217, 25, 25, 255);
ensureSolid(dir.resolve("bar_fill_stamina.png"), 4, 4, 229, 204, 25, 255);
ensureSolid(dir.resolve("bar_fill_mana.png"), 4, 4, 25, 76, 229, 255);
}
private static void ensureSolid(Path path, int w, int h,
int r, int g, int b, int a) {
if (Files.exists(path)) return;
try {
BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
int argb = (a << 24) | (r << 16) | (g << 8) | b;
for (int py = 0; py < h; py++) {
for (int px = 0; px < w; px++) {
img.setRGB(px, py, argb);
}
}
ImageIO.write(img, "PNG", path.toFile());
} catch (Exception e) {
log.warn("[HUD] PNG nicht erstellbar: {}", path);
}
}
private static void ensureFrame(Path path, int w, int h, int border,
int r, int g, int b) {
if (Files.exists(path)) return;
try {
BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
int borderArgb = (255 << 24) | (r << 16) | (g << 8) | b;
for (int py = 0; py < h; py++) {
for (int px = 0; px < w; px++) {
boolean isBorder = px < border || py < border
|| px >= w - border || py >= h - border;
img.setRGB(px, py, isBorder ? borderArgb : 0);
}
}
ImageIO.write(img, "PNG", path.toFile());
} catch (Exception e) {
log.warn("[HUD] Frame-PNG nicht erstellbar: {}", path);
}
}
}