diff --git a/blight-assets/src/main/resources/Textures/hud/bar_bg.png b/blight-assets/src/main/resources/Textures/hud/bar_bg.png new file mode 100644 index 0000000..4a5fb4d Binary files /dev/null and b/blight-assets/src/main/resources/Textures/hud/bar_bg.png differ diff --git a/blight-assets/src/main/resources/Textures/hud/bar_fill_health.png b/blight-assets/src/main/resources/Textures/hud/bar_fill_health.png new file mode 100644 index 0000000..f07d7fe Binary files /dev/null and b/blight-assets/src/main/resources/Textures/hud/bar_fill_health.png differ diff --git a/blight-assets/src/main/resources/Textures/hud/bar_fill_mana.png b/blight-assets/src/main/resources/Textures/hud/bar_fill_mana.png new file mode 100644 index 0000000..b3d35a0 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/hud/bar_fill_mana.png differ diff --git a/blight-assets/src/main/resources/Textures/hud/bar_fill_stamina.png b/blight-assets/src/main/resources/Textures/hud/bar_fill_stamina.png new file mode 100644 index 0000000..bfa8479 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/hud/bar_fill_stamina.png differ diff --git a/blight-assets/src/main/resources/Textures/hud/bar_frame.png b/blight-assets/src/main/resources/Textures/hud/bar_frame.png new file mode 100644 index 0000000..8de1a61 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/hud/bar_frame.png differ diff --git a/blight-assets/src/main/resources/audio/ambient/water/wading.ogg b/blight-assets/src/main/resources/audio/ambient/water/wading.ogg new file mode 100644 index 0000000..cfe1658 Binary files /dev/null and b/blight-assets/src/main/resources/audio/ambient/water/wading.ogg differ diff --git a/blight-assets/src/main/resources/audio/footsteps/water/running/.gitkeep b/blight-assets/src/main/resources/audio/footsteps/water/running/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/blight-assets/src/main/resources/audio/footsteps/water/running/Water Run 1.ogg b/blight-assets/src/main/resources/audio/footsteps/water/running/Water Run 1.ogg new file mode 100644 index 0000000..42be193 Binary files /dev/null and b/blight-assets/src/main/resources/audio/footsteps/water/running/Water Run 1.ogg differ diff --git a/blight-assets/src/main/resources/audio/footsteps/water/running/Water Run 2.ogg b/blight-assets/src/main/resources/audio/footsteps/water/running/Water Run 2.ogg new file mode 100644 index 0000000..613c61b Binary files /dev/null and b/blight-assets/src/main/resources/audio/footsteps/water/running/Water Run 2.ogg differ diff --git a/blight-assets/src/main/resources/audio/footsteps/water/running/Water Run 3.ogg b/blight-assets/src/main/resources/audio/footsteps/water/running/Water Run 3.ogg new file mode 100644 index 0000000..c2d81f9 Binary files /dev/null and b/blight-assets/src/main/resources/audio/footsteps/water/running/Water Run 3.ogg differ diff --git a/blight-assets/src/main/resources/audio/footsteps/water/running/Water Run 4.ogg b/blight-assets/src/main/resources/audio/footsteps/water/running/Water Run 4.ogg new file mode 100644 index 0000000..e056d53 Binary files /dev/null and b/blight-assets/src/main/resources/audio/footsteps/water/running/Water Run 4.ogg differ diff --git a/blight-assets/src/main/resources/audio/footsteps/water/running/Water Run 5.ogg b/blight-assets/src/main/resources/audio/footsteps/water/running/Water Run 5.ogg new file mode 100644 index 0000000..357a67d Binary files /dev/null and b/blight-assets/src/main/resources/audio/footsteps/water/running/Water Run 5.ogg differ diff --git a/blight-assets/src/main/resources/audio/footsteps/water/walking/.gitkeep b/blight-assets/src/main/resources/audio/footsteps/water/walking/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/blight-assets/src/main/resources/audio/footsteps/water/walking/Water Walk 1.ogg b/blight-assets/src/main/resources/audio/footsteps/water/walking/Water Walk 1.ogg new file mode 100644 index 0000000..e191a62 Binary files /dev/null and b/blight-assets/src/main/resources/audio/footsteps/water/walking/Water Walk 1.ogg differ diff --git a/blight-assets/src/main/resources/audio/footsteps/water/walking/Water Walk 2.ogg b/blight-assets/src/main/resources/audio/footsteps/water/walking/Water Walk 2.ogg new file mode 100644 index 0000000..5dd528a Binary files /dev/null and b/blight-assets/src/main/resources/audio/footsteps/water/walking/Water Walk 2.ogg differ diff --git a/blight-assets/src/main/resources/audio/footsteps/water/walking/Water Walk 3.ogg b/blight-assets/src/main/resources/audio/footsteps/water/walking/Water Walk 3.ogg new file mode 100644 index 0000000..95352b0 Binary files /dev/null and b/blight-assets/src/main/resources/audio/footsteps/water/walking/Water Walk 3.ogg differ diff --git a/blight-assets/src/main/resources/audio/footsteps/water/walking/Water Walk 4.ogg b/blight-assets/src/main/resources/audio/footsteps/water/walking/Water Walk 4.ogg new file mode 100644 index 0000000..f5e68a8 Binary files /dev/null and b/blight-assets/src/main/resources/audio/footsteps/water/walking/Water Walk 4.ogg differ diff --git a/blight-assets/src/main/resources/audio/footsteps/water/walking/Water Walk 5.ogg b/blight-assets/src/main/resources/audio/footsteps/water/walking/Water Walk 5.ogg new file mode 100644 index 0000000..72c693d Binary files /dev/null and b/blight-assets/src/main/resources/audio/footsteps/water/walking/Water Walk 5.ogg differ diff --git a/blight-game/src/main/java/de/blight/game/state/DrownState.java b/blight-game/src/main/java/de/blight/game/state/DrownState.java new file mode 100644 index 0000000..045499d --- /dev/null +++ b/blight-game/src/main/java/de/blight/game/state/DrownState.java @@ -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); + } +} diff --git a/blight-game/src/main/java/de/blight/game/state/HudState.java b/blight-game/src/main/java/de/blight/game/state/HudState.java new file mode 100644 index 0000000..33465d9 --- /dev/null +++ b/blight-game/src/main/java/de/blight/game/state/HudState.java @@ -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 (0–1). */ + 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); + } + } +} diff --git a/blight-map/src/main/map/blight_map.blm.tmp b/blight-map/src/main/map/blight_map.blm.tmp new file mode 100644 index 0000000..f669a81 Binary files /dev/null and b/blight-map/src/main/map/blight_map.blm.tmp differ diff --git a/com/jme3/app/SimpleApplication.java b/com/jme3/app/SimpleApplication.java new file mode 100644 index 0000000..704d0eb --- /dev/null +++ b/com/jme3/app/SimpleApplication.java @@ -0,0 +1,420 @@ +/* + * Copyright (c) 2009-2025 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.jme3.app; + +import com.jme3.app.state.AppState; +import com.jme3.app.state.ConstantVerifierState; +import com.jme3.audio.AudioListenerState; +import com.jme3.font.BitmapFont; +import com.jme3.font.BitmapText; +import com.jme3.input.FlyByCamera; +import com.jme3.input.KeyInput; +import com.jme3.input.controls.ActionListener; +import com.jme3.input.controls.KeyTrigger; +import com.jme3.profile.AppStep; +import com.jme3.renderer.RenderManager; +import com.jme3.renderer.queue.RenderQueue.Bucket; +import com.jme3.scene.Node; +import com.jme3.scene.Spatial.CullHint; +import com.jme3.scene.threadwarden.SceneGraphThreadWarden; +import com.jme3.system.AppSettings; +import com.jme3.system.JmeContext.Type; +import com.jme3.system.JmeSystem; + +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * `SimpleApplication` is the foundational base class for all jMonkeyEngine 3 (jME3) applications. + * It provides a streamlined setup for common game development tasks, including scene management, + * camera controls, and performance monitoring. + * + *

By default, `SimpleApplication` attaches several essential {@link com.jme3.app.state.AppState} instances: + *

+ * + *

Default Key Bindings:

+ * + * + *

Applications extending `SimpleApplication` should implement the + * {@link #simpleInitApp()} method to set up their initial scene and game logic. + */ +public abstract class SimpleApplication extends LegacyApplication { + + protected static final Logger logger = Logger.getLogger(SimpleApplication.class.getName()); + + public static final String INPUT_MAPPING_EXIT = "SIMPLEAPP_Exit"; + public static final String INPUT_MAPPING_CAMERA_POS = DebugKeysAppState.INPUT_MAPPING_CAMERA_POS; + public static final String INPUT_MAPPING_MEMORY = DebugKeysAppState.INPUT_MAPPING_MEMORY; + public static final String INPUT_MAPPING_HIDE_STATS = "SIMPLEAPP_HideStats"; + + protected Node rootNode = new Node("Root Node"); + protected Node guiNode = new Node("Gui Node"); + protected BitmapText fpsText; + protected BitmapFont guiFont; + protected FlyByCamera flyCam; + protected boolean showSettings = true; + private final AppActionListener actionListener = new AppActionListener(); + + private class AppActionListener implements ActionListener { + + @Override + public void onAction(String name, boolean isPressed, float tpf) { + if (!isPressed) { + return; + } + + if (name.equals(INPUT_MAPPING_EXIT)) { + stop(); + } else if (name.equals(INPUT_MAPPING_HIDE_STATS)) { + StatsAppState statsState = stateManager.getState(StatsAppState.class); + if (statsState != null) { + statsState.toggleStats(); + } + } + } + } + + /** + * Constructs a `SimpleApplication` with a predefined set of default + * {@link com.jme3.app.state.AppState} instances. + * These states provide common functionalities like statistics display, + * fly camera control, audio listener, debug keys, and constant verification. + */ + public SimpleApplication() { + this(new StatsAppState(), + new FlyCamAppState(), + new AudioListenerState(), + new DebugKeysAppState(), + new ConstantVerifierState()); + } + + /** + * Constructs a `SimpleApplication` with a custom array of initial + * {@link com.jme3.app.state.AppState} instances. + * + * @param initialStates An array of `AppState` instances to be attached + * to the `stateManager` upon initialization. + */ + public SimpleApplication(AppState... initialStates) { + super(initialStates); + } + + @Override + public void start() { + // set some default settings in-case + // settings dialog is not shown + boolean loadSettings = false; + if (settings == null) { + logger.log(Level.INFO, "AppSettings not set, creating default settings."); + setSettings(new AppSettings(true)); + loadSettings = true; + } + + // show settings dialog + if (showSettings) { + if (!JmeSystem.showSettingsDialog(settings, loadSettings)) { + return; + } + } + //re-setting settings they can have been merged from the registry. + setSettings(settings); + super.start(); + } + + /** + * Returns the current speed multiplier of the application. + * This value affects how quickly the game world updates relative to real time. + * A value of 1.0f means normal speed, 0.5f means half speed, 2.0f means double speed. + * + * @return The current speed of the application. + */ + public float getSpeed() { + return speed; + } + + /** + * Changes the application's speed multiplier. + * A `speed` of 0.0f effectively pauses the application's update cycle. + * + * @param speed The desired speed multiplier. A value of 1.0f is normal speed. + * Must be non-negative. + */ + public void setSpeed(float speed) { + this.speed = speed; + } + + /** + * Retrieves the `FlyByCamera` instance associated with this application. + * This camera allows free-form navigation within the 3D scene. + * + * @return The `FlyByCamera` object, or `null` if `FlyCamAppState` is not attached + * or has not yet initialized the camera. + */ + public FlyByCamera getFlyByCamera() { + return flyCam; + } + + /** + * Retrieves the `Node` dedicated to 2D graphical user interface (GUI) elements. + * Objects attached to this node are rendered on top of the 3D scene, + * typically without perspective effects, suitable for HUDs and UI. + * + * @return The `Node` object representing the GUI root. + */ + public Node getGuiNode() { + return guiNode; + } + + /** + * Retrieves the root `Node` of the 3D scene graph. + * All main 3D spatial objects and models should be attached to this node + * to be part of the rendered scene. + * + * @return The `Node` object representing the 3D scene root. + */ + public Node getRootNode() { + return rootNode; + } + + /** + * Checks whether the settings dialog is configured to be shown at application startup. + * + * @return `true` if the settings dialog will be displayed, `false` otherwise. + */ + public boolean isShowSettings() { + return showSettings; + } + + /** + * Sets whether the jME3 settings dialog should be displayed before the application starts. + * + * @param showSettings `true` to show the settings dialog, `false` to suppress it. + */ + public void setShowSettings(boolean showSettings) { + this.showSettings = showSettings; + } + + /** + * Creates the font that will be set to the guiFont field + * and subsequently set as the font for the stats text. + * + * @return the loaded BitmapFont + */ + protected BitmapFont loadGuiFont() { + return assetManager.loadFont("Interface/Fonts/Default.fnt"); + } + + @Override + public void initialize() { + super.initialize(); + + //noinspection AssertWithSideEffects + assert SceneGraphThreadWarden.setup(rootNode); + //noinspection AssertWithSideEffects + assert SceneGraphThreadWarden.setup(guiNode); + + // Several things rely on having this + guiFont = loadGuiFont(); + + guiNode.setQueueBucket(Bucket.Gui); + guiNode.setCullHint(CullHint.Never); + + viewPort.attachScene(rootNode); + guiViewPort.attachScene(guiNode); + + if (inputManager != null) { + // Special handling for FlyCamAppState: + // Although FlyCamAppState manages the FlyByCamera, SimpleApplication + // historically initializes and configures a default FlyByCamera instance + // and sets its initial speed. This allows subclasses to directly access + // 'flyCam' early in simpleInitApp(). + + FlyCamAppState flyCamState = stateManager.getState(FlyCamAppState.class); + if (flyCamState != null) { + flyCam = new FlyByCamera(cam); + flyCam.setMoveSpeed(1f); // Set a default movement speed for the camera + flyCamState.setCamera(flyCam); // Link the FlyCamAppState to this camera instance + } + + // Register the "Exit" input mapping for the Escape key, but only for Display contexts. + if (context.getType() == Type.Display) { + inputManager.addMapping(INPUT_MAPPING_EXIT, new KeyTrigger(KeyInput.KEY_ESCAPE)); + } + + // Register the "Hide Stats" input mapping for the F5 key, if StatsAppState is active. + StatsAppState statsState = stateManager.getState(StatsAppState.class); + if (statsState != null) { + inputManager.addMapping(INPUT_MAPPING_HIDE_STATS, new KeyTrigger(KeyInput.KEY_F5)); + inputManager.addListener(actionListener, INPUT_MAPPING_HIDE_STATS); + } + + // Attach the action listener to the "Exit" mapping. + inputManager.addListener(actionListener, INPUT_MAPPING_EXIT); + } + + // Configure the StatsAppState if it exists. + StatsAppState statsState = stateManager.getState(StatsAppState.class); + if (statsState != null) { + statsState.setFont(guiFont); + fpsText = statsState.getFpsText(); + } + + // Call the user's application initialization code. + simpleInitApp(); + } + + @Override + public void stop(boolean waitFor) { + //noinspection AssertWithSideEffects + assert SceneGraphThreadWarden.reset(); + super.stop(waitFor); + } + + @Override + public void update() { + if (prof != null) { + prof.appStep(AppStep.BeginFrame); + } + + // Executes AppTasks from the main thread + super.update(); + + // Skip updates if paused or speed is zero + if (speed == 0 || paused) { + return; + } + + float tpf = timer.getTimePerFrame() * speed; + + // Update AppStates + if (prof != null) { + prof.appStep(AppStep.StateManagerUpdate); + } + stateManager.update(tpf); + + // Call user's per-frame update method + simpleUpdate(tpf); + + // Update scene graph nodes (logical and geometric states) + if (prof != null) { + prof.appStep(AppStep.SpatialUpdate); + } + rootNode.updateLogicalState(tpf); + guiNode.updateLogicalState(tpf); + + rootNode.updateGeometricState(); + guiNode.updateGeometricState(); + + // Render AppStates and the scene + if (prof != null) { + prof.appStep(AppStep.StateManagerRender); + } + stateManager.render(renderManager); + + if (prof != null) { + prof.appStep(AppStep.RenderFrame); + } + renderManager.render(tpf, context.isRenderable()); + // Call user's custom render method + simpleRender(renderManager); + stateManager.postRender(); + + if (prof != null) { + prof.appStep(AppStep.EndFrame); + } + } + + /** + * Controls the visibility of the frames-per-second (FPS) display on the screen. + * + * @param show `true` to display the FPS, `false` to hide it. + */ + public void setDisplayFps(boolean show) { + StatsAppState statsState = stateManager.getState(StatsAppState.class); + if (statsState != null) { + statsState.setDisplayFps(show); + } + } + + /** + * Controls the visibility of the comprehensive statistics view on the screen. + * This view typically includes details about memory, triangles, and other performance metrics. + * + * @param show `true` to display the statistics view, `false` to hide it. + */ + public void setDisplayStatView(boolean show) { + StatsAppState statsState = stateManager.getState(StatsAppState.class); + if (statsState != null) { + statsState.setDisplayStatView(show); + } + } + + public abstract void simpleInitApp(); + + /** + * An optional method that can be overridden by subclasses for per-frame update logic. + * This method is called during the application's update loop, after AppStates are updated + * and before the scene graph's logical state is updated. + * + * @param tpf The time per frame (in seconds), adjusted by the application's speed. + */ + public void simpleUpdate(float tpf) { + // Default empty implementation; subclasses can override + } + + /** + * An optional method that can be overridden by subclasses for custom rendering logic. + * This method is called during the application's render loop, after the main scene + * has been rendered and before post-rendering for states. + * Useful for drawing overlays or specific rendering tasks outside the main scene graph. + * + * @param rm The `RenderManager` instance, which provides access to rendering functionalities. + */ + public void simpleRender(RenderManager rm) { + // Default empty implementation; subclasses can override + } +}