Commit vor Voxel Update für die Klippen

This commit is contained in:
2026-06-11 21:52:00 +02:00
parent fe5dfc19b1
commit a80269e681
143 changed files with 4340 additions and 342 deletions

View File

@@ -6,7 +6,10 @@ import com.jme3.input.KeyInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.system.AppSettings;
import de.blight.common.BlightHome;
import de.blight.common.SaveGameIO;
import de.blight.game.config.*;
import de.blight.game.state.SaveGameState;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.bridge.SLF4JBridgeHandler;
@@ -20,7 +23,6 @@ import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class BlightGame extends SimpleApplication {
@@ -29,10 +31,14 @@ public class BlightGame extends SimpleApplication {
private KeyBindings keyBindings;
private GraphicsSettings graphicsSettings;
private ScreenshotAppState screenshotState;
private WorldScene worldScene;
private ConfigScreen configScreen;
private GraphicsScreen graphicsScreen;
private PauseMenu pauseMenu;
private WorldScene worldScene;
private ConfigScreen configScreen;
private GraphicsScreen graphicsScreen;
private PauseMenu pauseMenu;
private MainMenuState mainMenuState;
/** Routed durch onClose-Callbacks, damit Config/Grafik-Screen zurück zum richtigen Menü springen. */
private Runnable screenCloseTarget;
private JWindow splashWindow;
@@ -74,13 +80,11 @@ public class BlightGame extends SimpleApplication {
private static JWindow showSplash() {
try {
// Logo skaliert auf 700 px Breite (Seitenverhältnis beibehalten)
BufferedImage img = ImageIO.read(BlightGame.class.getResourceAsStream("/logo.png"));
int tw = 700;
int th = (int) (img.getHeight() * (tw / (double) img.getWidth()));
Image scaled = img.getScaledInstance(tw, th, Image.SCALE_SMOOTH);
// Dunkles Panel
JPanel panel = new JPanel(new BorderLayout());
panel.setBackground(new Color(0x1c1c1c));
panel.setOpaque(true);
@@ -103,7 +107,6 @@ public class BlightGame extends SimpleApplication {
win.setLocationRelativeTo(null);
win.setVisible(true);
// Pollt loadingStatus alle 100 ms analog zum Editor-Splash
splashTimer = new Timer(100, null);
splashTimer.addActionListener(e -> {
splashLabel.setText(loadingStatus);
@@ -131,29 +134,55 @@ public class BlightGame extends SimpleApplication {
keyBindings = KeyBindingStore.load();
graphicsSettings = GraphicsStore.load();
status("Baue Spielwelt...");
status("Lade Spielstand...");
stateManager.attach(new SaveGameState());
// WorldScene erst vorbereiten, aber nicht aktivieren (onEnable lädt die Welt)
status("Initialisiere Welt...");
worldScene = new WorldScene(keyBindings);
worldScene.setEnabled(false);
stateManager.attach(worldScene);
// screenCloseTarget: dynamisch gesetzt, je nachdem ob Config/Grafik vom Hauptmenü
// oder vom Pausemenü geöffnet wurde.
screenCloseTarget = () -> {};
configScreen = new ConfigScreen(keyBindings, () -> worldScene.reloadBindings(keyBindings));
configScreen.setOnClose(() -> pauseMenu.setEnabled(true));
configScreen.setOnClose(() -> {
configScreen.setEnabled(false);
screenCloseTarget.run();
});
stateManager.attach(configScreen);
configScreen.setEnabled(false);
graphicsScreen = new GraphicsScreen(graphicsSettings, () -> pauseMenu.setEnabled(true));
graphicsScreen = new GraphicsScreen(graphicsSettings, () -> {
graphicsScreen.setEnabled(false);
screenCloseTarget.run();
});
stateManager.attach(graphicsScreen);
graphicsScreen.setEnabled(false);
SaveGameState saveState = stateManager.getState(SaveGameState.class);
pauseMenu = new PauseMenu(
() -> { pauseMenu.setEnabled(false); graphicsScreen.setEnabled(true); },
() -> { pauseMenu.setEnabled(false); configScreen.setEnabled(true); }
() -> { if (saveState != null) saveState.persist(); },
() -> {
screenCloseTarget = () -> pauseMenu.setEnabled(true);
pauseMenu.setEnabled(false);
graphicsScreen.setEnabled(true);
},
() -> {
screenCloseTarget = () -> pauseMenu.setEnabled(true);
pauseMenu.setEnabled(false);
configScreen.setEnabled(true);
}
);
stateManager.attach(pauseMenu);
pauseMenu.setEnabled(false);
// ── Screenshot (Druck-Taste) ───────────────────────────────────────────
// ── Screenshot (F12) ─────────────────────────────────────────────────────
try {
Path screenshotDir = findProjectRoot().resolve("screenshots");
Path screenshotDir = BlightHome.resolve("screenshots");
Files.createDirectories(screenshotDir);
screenshotState = new ScreenshotAppState(screenshotDir + File.separator, "screenshot");
stateManager.attach(screenshotState);
@@ -161,55 +190,109 @@ public class BlightGame extends SimpleApplication {
} catch (IOException e) {
log.warn("Screenshot-Verzeichnis konnte nicht angelegt werden", e);
}
inputManager.addMapping("Screenshot", new KeyTrigger(KeyInput.KEY_SYSRQ));
inputManager.addMapping("Screenshot", new KeyTrigger(KeyInput.KEY_F12));
inputManager.addListener((ActionListener) (name, isPressed, tpf) -> {
if (isPressed && screenshotState != null) screenshotState.takeScreenshot();
}, "Screenshot");
// ── Schnellspeichern (F5, konfigurierbar) ────────────────────────────
inputManager.addMapping("QuickSave", new KeyTrigger(keyBindings.quicksave));
inputManager.addListener((ActionListener) (name, isPressed, tpf) -> {
if (!isPressed || mainMenuState != null && mainMenuState.isEnabled()) return;
SaveGameState sgs = stateManager.getState(SaveGameState.class);
if (sgs != null) sgs.persist();
log.info("[Save] Schnellspeichern ausgelöst.");
}, "QuickSave");
// ── ESC-Handler ───────────────────────────────────────────────────────
inputManager.addMapping("ToggleMenu", new KeyTrigger(KeyInput.KEY_ESCAPE));
inputManager.addListener((ActionListener) (name, isPressed, tpf) -> {
if (!isPressed) return;
if (graphicsScreen.isEnabled()) {
return;
}
// Im Hauptmenü: ESC ignorieren
if (mainMenuState != null && mainMenuState.isEnabled()) return;
if (graphicsScreen.isEnabled()) return;
if (configScreen.isEnabled()) {
if (configScreen.isWaiting()) {
configScreen.cancelWaiting();
} else {
configScreen.setEnabled(false);
pauseMenu.setEnabled(true);
screenCloseTarget.run();
}
return;
}
de.blight.game.state.InventoryState inv = worldScene.getInventoryState();
if (inv != null && inv.isEnabled()) {
inv.setEnabled(false);
return;
}
if (pauseMenu.isEnabled()) {
pauseMenu.setEnabled(false);
worldScene.setPaused(false);
return;
}
pauseMenu.setEnabled(true);
worldScene.setPaused(true);
}, "ToggleMenu");
// ── Startentscheidung: Hauptmenü oder direkt ins Spiel (Editor-Start) ─
boolean autostart = Boolean.getBoolean("blight.autostart");
if (autostart) {
startWorld();
} else {
showMainMenu();
}
}
// ── Start-Flows ──────────────────────────────────────────────────────────
private void showMainMenu() {
boolean hasSave = SaveGameIO.exists();
mainMenuState = new MainMenuState(
this::onNewGame,
hasSave ? this::onContinue : null,
hasSave ? this::onContinue : null, // Spiel laden = gleiche Logik (ein Slot)
() -> {
screenCloseTarget = () -> mainMenuState.setEnabled(true);
mainMenuState.setEnabled(false);
configScreen.setEnabled(true);
},
this::stop
);
stateManager.attach(mainMenuState);
inputManager.setCursorVisible(true);
}
private void onNewGame() {
SaveGameState sgs = stateManager.getState(SaveGameState.class);
if (sgs != null) sgs.resetForNewGame();
startWorld();
}
private void onContinue() {
startWorld();
}
private void startWorld() {
if (mainMenuState != null) {
mainMenuState.setEnabled(false);
}
worldScene.setEnabled(true);
inputManager.setCursorVisible(false);
}
// ── Render-Loop ──────────────────────────────────────────────────────────
@Override
public void simpleUpdate(float tpf) {
if (!gameReady) {
gameReady = true; // erstes gerendtertes Frame → Splash schließen
gameReady = true;
status("Bereit");
}
}
private static Path findProjectRoot() {
String prop = System.getProperty("blight.project.root");
if (prop != null) return Paths.get(prop);
File dir = Paths.get(".").toAbsolutePath().normalize().toFile();
while (dir != null) {
if (new File(dir, "blight-editor").isDirectory()
&& new File(dir, "blight-game").isDirectory())
return dir.toPath();
dir = dir.getParentFile();
}
return Paths.get(".").toAbsolutePath().normalize();
}
}

View File

@@ -2,13 +2,14 @@ package de.blight.game.config;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import de.blight.common.BlightHome;
import java.io.*;
import java.nio.file.*;
public class GraphicsStore {
private static final Path FILE = Paths.get("config", "graphics.json");
private static final Path FILE = BlightHome.resolve("config", "graphics.json");
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
public static GraphicsSettings load() {

View File

@@ -2,13 +2,14 @@ package de.blight.game.config;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import de.blight.common.BlightHome;
import java.io.*;
import java.nio.file.*;
public class KeyBindingStore {
private static final Path FILE = Paths.get("config", "keybindings.json");
private static final Path FILE = BlightHome.resolve("config", "keybindings.json");
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
public static KeyBindings load() {

View File

@@ -5,25 +5,29 @@ import com.jme3.input.KeyInput;
/** Speichert alle konfigurierbaren Tastenbelegungen als plain int-Felder (KeyInput-Codes). */
public class KeyBindings {
public int forward = KeyInput.KEY_W;
public int backward = KeyInput.KEY_S;
public int left = KeyInput.KEY_A;
public int right = KeyInput.KEY_D;
public int jump = KeyInput.KEY_SPACE;
public int sprint = KeyInput.KEY_LSHIFT;
public int walk = KeyInput.KEY_LMENU;
public int interact = KeyInput.KEY_E;
public int forward = KeyInput.KEY_W;
public int backward = KeyInput.KEY_S;
public int left = KeyInput.KEY_A;
public int right = KeyInput.KEY_D;
public int jump = KeyInput.KEY_SPACE;
public int sprint = KeyInput.KEY_LSHIFT;
public int walk = KeyInput.KEY_LMENU;
public int interact = KeyInput.KEY_E;
public int inventory = KeyInput.KEY_I;
public int quicksave = KeyInput.KEY_F5;
/** Metadaten für die Config-UI: Feldname im Objekt + Anzeigename. */
public static final String[][] ENTRIES = {
{"forward", "Vorwärts"},
{"backward", "Rückwärts"},
{"left", "Links"},
{"right", "Rechts"},
{"jump", "Springen"},
{"sprint", "Rennen"},
{"walk", "Gehen"},
{"interact", "Interagieren"},
{"forward", "Vorwärts"},
{"backward", "Rückwärts"},
{"left", "Links"},
{"right", "Rechts"},
{"jump", "Springen"},
{"sprint", "Rennen"},
{"walk", "Gehen"},
{"interact", "Interagieren"},
{"inventory", "Inventar"},
{"quicksave", "Schnellspeichern"},
};
public int get(String fieldName) {

View File

@@ -31,7 +31,10 @@ public class PauseMenu extends BaseAppState {
private static final int BTN_GRAFIK = 0;
private static final int BTN_AUDIO = 1;
private static final int BTN_STEUERUNG = 2;
private static final int BTN_BEENDEN = 3;
private static final int BTN_SPEICHERN = 3;
private static final int BTN_BEENDEN = 4;
private static final ColorRGBA COL_BTN_SAVE = new ColorRGBA(0.12f, 0.28f, 0.14f, 1.00f);
private SimpleApplication app;
private Node guiNode;
@@ -40,11 +43,13 @@ public class PauseMenu extends BaseAppState {
private Runnable onGraphics;
private Runnable onControls;
private Runnable onSave;
// [x, y, w, h] per button
private final float[][] btnBounds = new float[4][4];
private final float[][] btnBounds = new float[5][4];
public PauseMenu(Runnable onGraphics, Runnable onControls) {
public PauseMenu(Runnable onSave, Runnable onGraphics, Runnable onControls) {
this.onSave = onSave;
this.onGraphics = onGraphics;
this.onControls = onControls;
}
@@ -79,7 +84,7 @@ public class PauseMenu extends BaseAppState {
panel = new Node("pause-panel");
addQuad(panel, 0, 0, sw, sh, COL_BG, -2);
float pw = 320, ph = 360;
float pw = 320, ph = 430;
float px = (sw - pw) / 2f, py = (sh - ph) / 2f;
addQuad(panel, px, py, pw, ph, COL_PANEL, -1);
@@ -87,23 +92,23 @@ public class PauseMenu extends BaseAppState {
centerText(title, px, py + ph - 48, pw);
panel.attachChild(title);
String[] labels = {"Grafik", "Audio", "Steuerung", "Beenden"};
boolean[] enabled = {true, false, true, true};
String[] labels = {"Grafik", "Audio", "Steuerung", "Speichern", "Beenden"};
boolean[] enabled = {true, false, true, true, true};
ColorRGBA[] bgCols = {COL_BTN, COL_BTN_DIS, COL_BTN, COL_BTN_SAVE, COL_BTN_QUIT};
float bw = 260, bh = 52;
float bx = px + (pw - bw) / 2f;
float startY = py + ph - 112;
float step = 62;
for (int i = 0; i < 4; i++) {
for (int i = 0; i < 5; i++) {
float by = startY - i * step;
ColorRGBA bgCol = !enabled[i] ? COL_BTN_DIS : (i == BTN_BEENDEN ? COL_BTN_QUIT : COL_BTN);
ColorRGBA txCol = enabled[i] ? COL_TEXT : COL_TEXT_DIS;
addQuad(panel, bx, by, bw, bh, bgCol, 0);
addQuad(panel, bx, by, bw, bh, bgCols[i], 0);
BitmapText lbl = txt(labels[i], 18, txCol);
if (!enabled[i]) {
// Center label in upper portion, show hint below
lbl.setLocalTranslation(bx + (bw - lbl.getLineWidth()) / 2f, by + bh - 12, 1);
BitmapText hint = txt("Bald verfügbar", 12, COL_TEXT_SUB);
hint.setLocalTranslation(bx + (bw - hint.getLineWidth()) / 2f, by + 14, 1);
@@ -124,12 +129,13 @@ public class PauseMenu extends BaseAppState {
if (!isPressed) return;
Vector2f c = app.getInputManager().getCursorPosition();
for (int i = 0; i < 4; i++) {
for (int i = 0; i < 5; i++) {
if (!hits(c, btnBounds[i][0], btnBounds[i][1], btnBounds[i][2], btnBounds[i][3])) continue;
switch (i) {
case BTN_GRAFIK -> { if (onGraphics != null) onGraphics.run(); }
case BTN_AUDIO -> { /* Bald verfügbar */ }
case BTN_STEUERUNG -> { if (onControls != null) onControls.run(); }
case BTN_SPEICHERN -> { if (onSave != null) onSave.run(); }
case BTN_BEENDEN -> app.stop();
}
return;

View File

@@ -43,14 +43,14 @@ public class PlayerInputControl {
private boolean animCtxLogged = false;
private AnimComposer animComposer;
/** Letzter gestarteter Clip (nur für tryPlay-Deduplizierung genutzt). */
private String runningClip;
/** Frames, für die JUMP erzwungen wird (überbrückt onGround()-Lag). */
private int jumpFrames = 0;
/** Läuft gerade eine Pickup-Animation? */
private boolean pickupActive = false;
private float pickupRemaining = 0f;
/** Autopilot: wenn gesetzt, geht der Charakter automatisch in diese (normalisierte) Richtung. */
private Vector3f autopilotDir = null;
private final ActionListener actionListener = (name, isPressed, tpf) -> {
if (paused) return;
switch (name) {
@@ -94,10 +94,13 @@ public class PlayerInputControl {
}
}
public boolean isPaused() { return paused; }
public void setPaused(boolean paused) {
this.paused = paused;
if (paused) {
forward = backward = left = right = sprint = walk = false;
autopilotDir = null;
if (physicsChar != null) physicsChar.setWalkDirection(Vector3f.ZERO);
}
}
@@ -109,6 +112,31 @@ public class PlayerInputControl {
if (physicsChar != null) physicsChar.setWalkDirection(Vector3f.ZERO);
}
/**
* Aktiviert den Autopilot-Modus. Der Charakter läuft mit Walk-Geschwindigkeit
* in die angegebene (normalisierte) Richtung und spielt die WALK-Animation.
* {@code null} deaktiviert den Autopilot.
*/
public void setAutopilotDirection(Vector3f dir) {
this.autopilotDir = dir;
if (dir == null && physicsChar != null && !paused && !pickupActive) {
physicsChar.setWalkDirection(Vector3f.ZERO);
}
}
/**
* Spielt die PICK_UP-Animation einmalig ab und blockiert Bewegung für {@code duration} Sekunden.
*/
public void requestPickup(float duration) {
pickupActive = true;
pickupRemaining = duration;
autopilotDir = null;
forward = backward = left = right = false;
if (physicsChar != null) physicsChar.setWalkDirection(Vector3f.ZERO);
playAction(AnimationAction.PICK_UP);
currentAnim = AnimationAction.PICK_UP;
}
private void registerMappings(KeyBindings kb) {
inputManager.addMapping("Forward", new KeyTrigger(kb.forward));
inputManager.addMapping("Backward", new KeyTrigger(kb.backward));
@@ -120,33 +148,48 @@ public class PlayerInputControl {
inputManager.addListener(actionListener, ACTION_NAMES);
}
/**
* Spielt die PICK_UP-Animation einmalig ab und blockiert Bewegung für {@code duration} Sekunden.
* Wird von WorldItemsState aufgerufen, sobald der Spieler ein Item aufnimmt.
*/
public void requestPickup(float duration) {
pickupActive = true;
pickupRemaining = duration;
forward = backward = left = right = false;
if (physicsChar != null) physicsChar.setWalkDirection(Vector3f.ZERO);
playAction(AnimationAction.PICK_UP);
currentAnim = AnimationAction.PICK_UP;
}
public void update(float tpf) {
if (physicsChar == null || paused) return;
if (physicsChar == null) return;
if (paused) {
// Autopilot bei Pause sofort beenden
if (autopilotDir != null) {
autopilotDir = null;
physicsChar.setWalkDirection(Vector3f.ZERO);
}
return;
}
// Pickup-Animation hat höchste Priorität
if (pickupActive) {
pickupRemaining -= tpf;
physicsChar.setWalkDirection(Vector3f.ZERO);
if (pickupRemaining <= 0f) {
pickupActive = false;
currentAnim = null; // erzwingt Neubewertung im nächsten Frame
currentAnim = null;
} else {
return;
}
}
// Autopilot: Charakter läuft automatisch in eine vorgegebene Richtung (WALK-Animation)
if (autopilotDir != null) {
physicsChar.setWalkDirection(autopilotDir.mult(MOVE_SPEED * WALK_MULT));
if (visual != null) {
Quaternion targetRot = new Quaternion();
targetRot.lookAt(autopilotDir, Vector3f.UNIT_Y);
Quaternion current = visual.getLocalRotation().clone();
current.slerp(targetRot, ROTATE_SPEED * tpf);
visual.setLocalRotation(current);
}
if (AnimationAction.WALK != currentAnim) {
playAction(AnimationAction.WALK);
currentAnim = AnimationAction.WALK;
}
return;
}
// Normale Spielereingabe (WASD)
Vector3f camDir = cam.getDirection().clone().setY(0).normalizeLocal();
Vector3f camLeft = cam.getLeft().clone().setY(0).normalizeLocal();
@@ -176,7 +219,7 @@ public class PlayerInputControl {
physicsChar.setWalkDirection(Vector3f.ZERO);
}
// Animation
// Animations-Auswahl
if (jumpFrames > 0) jumpFrames--;
AnimationAction target;

View File

@@ -44,6 +44,7 @@ import de.blight.game.state.TerrainChunkState;
import de.blight.game.state.WaterBodyState;
import de.blight.game.state.WeatherState;
import de.blight.game.state.InteractionHudState;
import de.blight.game.state.InventoryState;
import de.blight.game.state.WorldItemsState;
import de.blight.game.state.WorldObjectsState;
@@ -65,6 +66,7 @@ public class WorldScene extends BaseAppState {
private final KeyBindings keyBindings;
private ThirdPersonCamera thirdPersonCam;
private PlayerInputControl playerInput;
private InventoryState inventoryState;
private AnimationLibrary animLib;
private Node character;
private Spatial characterVisual;
@@ -78,6 +80,8 @@ public class WorldScene extends BaseAppState {
this.keyBindings = keyBindings;
}
public InventoryState getInventoryState() { return inventoryState; }
/** Wird von ConfigScreen nach dem Speichern aufgerufen. */
public void reloadBindings(KeyBindings kb) {
if (playerInput != null) playerInput.reloadBindings(kb);
@@ -156,10 +160,18 @@ public class WorldScene extends BaseAppState {
MainCharacter mc = findMainCharacter();
if (mc != null) {
// SaveGameState mit Charakter und Positions-Lieferant verknüpfen
de.blight.game.state.SaveGameState saveState =
app.getStateManager().getState(de.blight.game.state.SaveGameState.class);
if (saveState != null) saveState.bind(mc, physicsChar::getPhysicsLocation);
app.getStateManager().attach(new LocationState(mc, character));
app.getStateManager().attach(
new WorldItemsState(keyBindings, physicsChar, mc, playerInput));
app.getStateManager().attach(new InteractionHudState());
inventoryState = new InventoryState(mc, keyBindings);
inventoryState.setEnabled(false);
app.getStateManager().attach(inventoryState);
}
// Maus einfangen keine Klick-Pflicht für Kamerasteuerung
@@ -352,12 +364,23 @@ public class WorldScene extends BaseAppState {
}
}
// Spawn aus Map oder Editor-Property
// Spawn-Priorität: 1) Editor-Property 2) Spielstand 3) Karten-Default
String propX = System.getProperty("blight.temp.spawn.x");
String propZ = System.getProperty("blight.temp.spawn.z");
if (loadedMapData != null) {
spawnX = propX != null ? Float.parseFloat(propX) : loadedMapData.spawnX;
spawnZ = propZ != null ? Float.parseFloat(propZ) : loadedMapData.spawnZ;
if (propX != null) {
spawnX = Float.parseFloat(propX);
spawnZ = propZ != null ? Float.parseFloat(propZ) : (loadedMapData != null ? loadedMapData.spawnZ : 0f);
} else {
de.blight.game.state.SaveGameState saveState =
app.getStateManager().getState(de.blight.game.state.SaveGameState.class);
if (saveState != null && saveState.getSave().character.positionSaved) {
spawnX = saveState.getSave().character.x;
spawnY = saveState.getSave().character.y;
spawnZ = saveState.getSave().character.z;
} else if (loadedMapData != null) {
spawnX = loadedMapData.spawnX;
spawnZ = loadedMapData.spawnZ;
}
}
System.out.println("[WorldScene] SpawnXZ: X=" + spawnX + " Z=" + spawnZ);
@@ -366,9 +389,15 @@ public class WorldScene extends BaseAppState {
terrainChunkState = new TerrainChunkState(bulletAppState, mat, loadedMapData);
app.getStateManager().attach(terrainChunkState);
// Spawn-Höhe aus Chunk-Daten
float terrainH = terrainChunkState.getHeightAt(spawnX, spawnZ);
spawnY = terrainH + 10f;
// Spawn-Höhe: aus gespeicherter Position oder aus Terrain berechnen
de.blight.game.state.SaveGameState _sv =
app.getStateManager().getState(de.blight.game.state.SaveGameState.class);
boolean hasSavedY = _sv != null && _sv.getSave().character.positionSaved
&& System.getProperty("blight.temp.spawn.x") == null;
if (!hasSavedY) {
float terrainH = terrainChunkState.getHeightAt(spawnX, spawnZ);
spawnY = terrainH + 10f;
}
System.out.println("[WorldScene] SpawnXYZ=(" + spawnX + ", " + spawnY + ", " + spawnZ + ")");
}

View File

@@ -6,30 +6,25 @@ import com.jme3.app.state.BaseAppState;
import com.jme3.font.BitmapFont;
import com.jme3.font.BitmapText;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import de.blight.common.model.Item;
import de.blight.common.model.TextRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Zeigt oberhalb von nahegelegenen Item-Pickups den Namen an,
* wenn der Spieler darauf zielt.
* Zeigt den Namen eines anvisierten Item-Pickups in Weltkoordinaten oberhalb des Objekts an.
* Die Erkennung (welches Item angezielt wird) übernimmt WorldItemsState.
*/
public class InteractionHudState extends BaseAppState {
private static final Logger log = LoggerFactory.getLogger(InteractionHudState.class);
private static final float SHOW_RANGE = 3.5f;
private static final float DOT_THRESH = 0.65f;
private static final float Y_OFFSET = 0.6f;
private static final float Y_OFFSET = 0.6f;
private Camera cam;
private Node guiNode;
private Camera cam;
private Node guiNode;
private WorldItemsState worldItems;
private BitmapText labelText;
@@ -39,8 +34,8 @@ public class InteractionHudState extends BaseAppState {
@Override
protected void initialize(Application app) {
SimpleApplication sapp = (SimpleApplication) app;
this.cam = app.getCamera();
this.guiNode = sapp.getGuiNode();
this.cam = app.getCamera();
this.guiNode = sapp.getGuiNode();
BitmapFont font = app.getAssetManager().loadFont("Interface/Fonts/Default.fnt");
labelText = new BitmapText(font, false);
@@ -76,60 +71,37 @@ public class InteractionHudState extends BaseAppState {
return;
}
Node itemsRoot = worldItems.getItemsRoot();
if (itemsRoot == null || itemsRoot.getQuantity() == 0) {
int idx = worldItems.getHoveredIdx();
if (idx < 0) {
labelText.setCullHint(Spatial.CullHint.Always);
return;
}
Vector3f playerPos = worldItems.getPhysicsChar() != null
? worldItems.getPhysicsChar().getPhysicsLocation()
: cam.getLocation();
Vector3f camDir = cam.getDirection().normalizeLocal();
Spatial bestTarget = null;
float bestDot = -1f;
for (Spatial s : itemsRoot.getChildren()) {
Vector3f itemPos = s.getWorldTranslation();
float dx = itemPos.x - playerPos.x;
float dz = itemPos.z - playerPos.z;
float dist = (float) Math.sqrt(dx * dx + dz * dz);
if (dist > SHOW_RANGE) continue;
Vector3f toItem = itemPos.subtract(cam.getLocation()).normalizeLocal();
float dot = camDir.dot(toItem);
if (dot > DOT_THRESH && dot > bestDot) {
bestDot = dot;
bestTarget = s;
}
}
if (bestTarget == null) {
String name = worldItems.getHoveredItemName();
if (name == null) {
labelText.setCullHint(Spatial.CullHint.Always);
return;
}
String label = resolveLabel(bestTarget);
labelText.setText(label);
// Weltposition des Items → Bildschirmkoordinate
Node itemsRoot = worldItems.getItemsRoot();
if (idx >= itemsRoot.getQuantity()) {
labelText.setCullHint(Spatial.CullHint.Always);
return;
}
Spatial target = itemsRoot.getChild(idx);
Vector3f worldPos = target.getWorldTranslation().add(0f, Y_OFFSET, 0f);
Vector3f screenV = cam.getScreenCoordinates(worldPos);
Vector3f worldPos = bestTarget.getWorldTranslation().add(0f, Y_OFFSET, 0f);
Vector3f screenV3 = cam.getScreenCoordinates(worldPos);
Vector2f screen = new Vector2f(screenV3.x, screenV3.y);
// Hinter der Kamera → nicht anzeigen
if (screenV.z >= 1f) {
labelText.setCullHint(Spatial.CullHint.Always);
return;
}
labelText.setText(name);
float textW = labelText.getLineWidth();
labelText.setLocalTranslation(screen.x - textW * 0.5f, screen.y, 1f);
labelText.setLocalTranslation(screenV.x - textW * 0.5f, screenV.y, 1f);
labelText.setCullHint(Spatial.CullHint.Inherit);
}
// ── Hilfsmethoden ────────────────────────────────────────────────────────
private String resolveLabel(Spatial s) {
String itemId = s.getUserData("itemId");
if (itemId == null) return "?";
WorldItemsState state = worldItems;
// Resolve via TextRegistry if a full Item definition is available
// For now fall back to itemId
return itemId;
}
}

View File

@@ -0,0 +1,480 @@
package de.blight.game.state;
import com.jme3.app.Application;
import com.jme3.app.SimpleApplication;
import com.jme3.app.state.BaseAppState;
import com.jme3.asset.AssetManager;
import com.jme3.font.BitmapFont;
import com.jme3.font.BitmapText;
import com.jme3.input.MouseInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.AnalogListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.input.controls.MouseAxisTrigger;
import com.jme3.input.controls.MouseButtonTrigger;
import com.jme3.material.Material;
import com.jme3.material.RenderState;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector2f;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.shape.Quad;
import com.jme3.texture.Texture;
import de.blight.common.model.*;
import de.blight.game.config.KeyBindings;
import de.blight.game.scene.WorldScene;
import java.util.*;
import java.util.stream.Collectors;
/**
* In-Game-Inventaransicht: öffnet/schließt sich mit der 'I'-Taste.
* Items werden nach Kategorie (Tabs) und innerhalb der Kategorie nach
* SubKategorie und dann nach Preis sortiert angezeigt.
* Thumbnails werden aus {@code ObjectReference.getThumbnailAssetPath()} geladen.
*/
public class InventoryState extends BaseAppState {
// ── Farben ────────────────────────────────────────────────────────────────
private static final ColorRGBA COL_OVERLAY = new ColorRGBA(0f, 0f, 0f, 0.72f);
private static final ColorRGBA COL_PANEL = new ColorRGBA(0.09f, 0.09f, 0.14f, 0.97f);
private static final ColorRGBA COL_HDR = new ColorRGBA(0.05f, 0.05f, 0.09f, 1.00f);
private static final ColorRGBA COL_TAB_ON = new ColorRGBA(0.22f, 0.22f, 0.42f, 1.00f);
private static final ColorRGBA COL_TAB_OFF = new ColorRGBA(0.12f, 0.12f, 0.20f, 1.00f);
private static final ColorRGBA COL_CELL = new ColorRGBA(0.14f, 0.14f, 0.22f, 1.00f);
private static final ColorRGBA COL_CELL_FRAME = new ColorRGBA(0.22f, 0.22f, 0.35f, 1.00f);
private static final ColorRGBA COL_THUMB_BG = new ColorRGBA(0.20f, 0.20f, 0.28f, 1.00f);
private static final ColorRGBA COL_BADGE = new ColorRGBA(0.04f, 0.04f, 0.07f, 0.92f);
private static final ColorRGBA COL_SUBHDR = new ColorRGBA(0.10f, 0.10f, 0.18f, 0.80f);
private static final ColorRGBA COL_WHITE = ColorRGBA.White;
private static final ColorRGBA COL_MUTED = new ColorRGBA(0.55f, 0.55f, 0.60f, 1.00f);
private static final ColorRGBA COL_GOLD = new ColorRGBA(1.00f, 0.85f, 0.20f, 1.00f);
private static final ColorRGBA COL_SUBCAT_TXT = new ColorRGBA(0.55f, 0.78f, 1.00f, 1.00f);
// ── Layout ────────────────────────────────────────────────────────────────
private static final int COLS = 5;
private static final int CELL_W = 155;
private static final int CELL_H = 195; // 128 thumb + 67 text
private static final int CELL_GAP = 8;
private static final int THUMB_SZ = 128;
private static final int HDR_H = 42;
private static final int TAB_H = 32;
private static final int PAD = 16; // panel inner padding
private static final int SUBHDR_H = 26;
private static final int SUBHDR_GAP = 4;
// ── Input-Mapping-Namen ───────────────────────────────────────────────────
private static final String MAP_TOGGLE = "_InvToggle";
private static final String MAP_SCROLL_UP = "_InvScrollUp";
private static final String MAP_SCROLL_DN = "_InvScrollDn";
private static final String MAP_CLICK = "_InvClick";
// ── JME-Zustand ───────────────────────────────────────────────────────────
private SimpleApplication app;
private AssetManager assetManager;
private BitmapFont font;
private Node guiNode;
private Node panel;
private Node gridNode;
// ── Daten ─────────────────────────────────────────────────────────────────
private final MainCharacter mc;
private final KeyBindings keyBindings;
// ── UI-Zustand ────────────────────────────────────────────────────────────
private ItemCategory activeTab = null;
private float scrollY = 0f; // Pixel-Scroll-Offset (nach unten = positiv)
private float maxScrollY = 0f;
// Für Tab-Klick-Erkennung: parallele Arrays
private ItemCategory[] tabOrder;
private float[][] tabBounds; // [tabIdx] = {x, y, w, h}
// Content-Bereich in Screen-Koordinaten (JME3 Y-up)
private float contentLeft;
private float contentBottom;
private float contentTop;
// Sortierte Item-Liste des aktiven Tabs
private List<Map.Entry<Item, Integer>> activeItems = new ArrayList<>();
// ── Konstruktor ───────────────────────────────────────────────────────────
public InventoryState(MainCharacter mc, KeyBindings keyBindings) {
this.mc = mc;
this.keyBindings = keyBindings;
}
// ── Lifecycle ─────────────────────────────────────────────────────────────
@Override
protected void initialize(Application app) {
this.app = (SimpleApplication) app;
this.assetManager = app.getAssetManager();
this.guiNode = this.app.getGuiNode();
this.font = app.getAssetManager().loadFont("Interface/Fonts/Default.fnt");
app.getInputManager().addMapping(MAP_TOGGLE, new KeyTrigger(keyBindings.inventory));
app.getInputManager().addMapping(MAP_SCROLL_UP, new MouseAxisTrigger(MouseInput.AXIS_WHEEL, false));
app.getInputManager().addMapping(MAP_SCROLL_DN, new MouseAxisTrigger(MouseInput.AXIS_WHEEL, true));
app.getInputManager().addMapping(MAP_CLICK, new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
app.getInputManager().addListener(toggleListener, MAP_TOGGLE);
}
@Override
protected void onEnable() {
scrollY = 0f;
buildPanel();
app.getInputManager().setCursorVisible(true);
app.getInputManager().addListener(scrollListener, MAP_SCROLL_UP, MAP_SCROLL_DN);
app.getInputManager().addListener(clickListener, MAP_CLICK);
WorldScene ws = app.getStateManager().getState(WorldScene.class);
if (ws != null) ws.setPaused(true);
}
@Override
protected void onDisable() {
destroyPanel();
app.getInputManager().removeListener(scrollListener);
app.getInputManager().removeListener(clickListener);
app.getInputManager().setCursorVisible(false);
WorldScene ws = app.getStateManager().getState(WorldScene.class);
if (ws != null) ws.setPaused(false);
}
@Override
protected void cleanup(Application app) {
app.getInputManager().deleteMapping(MAP_TOGGLE);
app.getInputManager().deleteMapping(MAP_SCROLL_UP);
app.getInputManager().deleteMapping(MAP_SCROLL_DN);
app.getInputManager().deleteMapping(MAP_CLICK);
}
// ── Listener ──────────────────────────────────────────────────────────────
private final ActionListener toggleListener = (name, pressed, tpf) -> {
if (pressed && MAP_TOGGLE.equals(name)) setEnabled(!isEnabled());
};
private final AnalogListener scrollListener = (name, value, tpf) -> {
float step = 60f * value;
if (MAP_SCROLL_UP.equals(name)) scrollY = Math.max(0f, scrollY - step);
else scrollY = Math.min(maxScrollY, scrollY + step);
rebuildGrid();
};
private final ActionListener clickListener = (name, pressed, tpf) -> {
if (!pressed || panel == null || tabBounds == null) return;
Vector2f c = app.getInputManager().getCursorPosition();
for (int i = 0; i < tabBounds.length; i++) {
float[] b = tabBounds[i];
if (c.x >= b[0] && c.x <= b[0] + b[2] && c.y >= b[1] && c.y <= b[1] + b[3]) {
switchTab(tabOrder[i]);
return;
}
}
};
// ── Haupt-Panel aufbauen ──────────────────────────────────────────────────
private void buildPanel() {
float sw = app.getCamera().getWidth();
float sh = app.getCamera().getHeight();
// Panelgröße: 5 Spalten + Ränder
float pw = COLS * (CELL_W + CELL_GAP) + CELL_GAP + 2 * PAD;
float ph = HDR_H + TAB_H + 4 + 450 + PAD; // header + tabs + gap + content + bottom
float px = (sw - pw) / 2f;
float py = (sh - ph) / 2f;
panel = new Node("inv-panel");
quad(panel, 0, 0, sw, sh, COL_OVERLAY, -20); // Verdunkelung
quad(panel, px, py, pw, ph, COL_PANEL, -19); // Hauptpanel
quad(panel, px, py + ph - HDR_H, pw, HDR_H, COL_HDR, -18); // Header-Balken
// Titel
BitmapText title = txt("Inventar", 22, COL_WHITE);
title.setLocalTranslation(px + PAD, py + ph - 12, -17);
panel.attachChild(title);
// Content-Bereich (für Scroll-Berechnungen)
contentLeft = px + PAD;
contentBottom = py + PAD;
contentTop = py + ph - HDR_H - TAB_H - 8;
// Tabs aufbauen
buildTabs(px, py, pw, ph);
guiNode.attachChild(panel);
}
private void buildTabs(float px, float py, float pw, float ph) {
tabOrder = ItemCategory.values();
tabBounds = new float[tabOrder.length][4];
if (activeTab == null) activeTab = tabOrder[0];
float tabY = py + ph - HDR_H - TAB_H;
float tabW = (pw - 8) / tabOrder.length;
float tabX0 = px + 4;
for (int i = 0; i < tabOrder.length; i++) {
boolean active = tabOrder[i] == activeTab;
float tx = tabX0 + i * tabW;
quad(panel, tx, tabY, tabW - 4, TAB_H, active ? COL_TAB_ON : COL_TAB_OFF, -18);
BitmapText lbl = txt(catLabel(tabOrder[i]), 13, active ? COL_WHITE : COL_MUTED);
lbl.setLocalTranslation(tx + (tabW - 4 - lbl.getLineWidth()) / 2f, tabY + TAB_H - 8, -17);
panel.attachChild(lbl);
tabBounds[i] = new float[]{ tx, tabY, tabW - 4, TAB_H };
}
activeItems = sortedItems(activeTab);
maxScrollY = computeMaxScroll();
scrollY = 0f;
buildGrid();
}
private void switchTab(ItemCategory cat) {
if (cat == activeTab) return;
activeTab = cat;
scrollY = 0f;
activeItems = sortedItems(cat);
destroyPanel();
buildPanel();
}
// ── Item-Grid ─────────────────────────────────────────────────────────────
/** Erstellt den Grid-Node und hängt ihn ans Panel. */
private void buildGrid() {
gridNode = new Node("inv-grid");
panel.attachChild(gridNode);
if (activeItems.isEmpty()) {
BitmapText empty = txt("Keine Items vorhanden", 15, COL_MUTED);
float ey = contentBottom + (contentTop - contentBottom) / 2f + 10;
float ex = contentLeft + (COLS * (CELL_W + CELL_GAP) - CELL_GAP - empty.getLineWidth()) / 2f;
empty.setLocalTranslation(ex, ey, -17);
gridNode.attachChild(empty);
return;
}
// Items nach SubKategorie gruppiert (Reihenfolge aus sortedItems beibehalten)
Map<ItemSubCategory, List<Map.Entry<Item, Integer>>> groups = new LinkedHashMap<>();
for (Map.Entry<Item, Integer> e : activeItems) {
groups.computeIfAbsent(e.getKey().getSubCategory(), k -> new ArrayList<>()).add(e);
}
// Virtual Y: beginnt am Top des Content-Bereichs, läuft nach unten (Y nimmt ab)
float virtY = contentTop - scrollY;
for (Map.Entry<ItemSubCategory, List<Map.Entry<Item, Integer>>> group : groups.entrySet()) {
// SubKategorie-Header
float subHdrY = virtY - SUBHDR_H;
if (subHdrY + SUBHDR_H >= contentBottom && subHdrY <= contentTop) {
String subLabel = group.getKey() != null ? subCatLabel(group.getKey()) : "Sonstiges";
float subBgW = COLS * (CELL_W + CELL_GAP) - CELL_GAP;
quad(gridNode, contentLeft, subHdrY, subBgW, SUBHDR_H, COL_SUBHDR, -18);
BitmapText sLbl = txt(" " + subLabel, 12, COL_SUBCAT_TXT);
sLbl.setLocalTranslation(contentLeft + 6, subHdrY + SUBHDR_H - 7, -17);
gridNode.attachChild(sLbl);
}
virtY -= SUBHDR_H + SUBHDR_GAP;
// Item-Zellen zeilenweise
List<Map.Entry<Item, Integer>> groupItems = group.getValue();
int col = 0;
for (Map.Entry<Item, Integer> entry : groupItems) {
if (col == 0) virtY -= CELL_H;
float cellX = contentLeft + col * (CELL_W + CELL_GAP);
float cellY = virtY;
// Nur rendern wenn vollständig im sichtbaren Content-Bereich
if (cellY >= contentBottom && cellY + CELL_H <= contentTop) {
buildCell(gridNode, entry.getKey(), entry.getValue(), cellX, cellY);
}
col++;
if (col >= COLS) {
col = 0;
virtY -= CELL_GAP;
}
}
// Wenn letzte Zeile nicht voll war, Y-Schritt nachholen
if (col != 0) virtY -= CELL_GAP;
virtY -= CELL_GAP; // Abstand zwischen Gruppen
}
}
/** Entfernt den Grid-Node und erstellt ihn neu (bei Scroll). */
private void rebuildGrid() {
if (panel == null) return;
if (gridNode != null) panel.detachChild(gridNode);
buildGrid();
}
/** Berechnet maximalen Scroll-Offset in Pixel. */
private float computeMaxScroll() {
Map<ItemSubCategory, List<Map.Entry<Item, Integer>>> groups = new LinkedHashMap<>();
for (Map.Entry<Item, Integer> e : activeItems) {
groups.computeIfAbsent(e.getKey().getSubCategory(), k -> new ArrayList<>()).add(e);
}
float totalH = 0f;
for (List<Map.Entry<Item, Integer>> g : groups.values()) {
totalH += SUBHDR_H + SUBHDR_GAP;
int rows = (int) Math.ceil(g.size() / (double) COLS);
totalH += rows * (CELL_H + CELL_GAP);
}
float contentH = contentTop - contentBottom;
return Math.max(0f, totalH - contentH);
}
// ── Einzel-Zelle ──────────────────────────────────────────────────────────
private void buildCell(Node parent, Item item, int count, float x, float y) {
// Zell-Hintergrund + Rand
quad(parent, x, y, CELL_W, CELL_H, COL_CELL_FRAME, -18);
quad(parent, x + 1, y + 1, CELL_W - 2, CELL_H - 2, COL_CELL, -17);
// Thumbnail
float thumbX = x + (CELL_W - THUMB_SZ) / 2f;
float thumbY = y + CELL_H - THUMB_SZ - 6;
Texture thumb = null;
if (item.getModelRef() != null) {
String tp = item.getModelRef().getThumbnailAssetPath();
if (tp != null) thumb = loadThumb(tp);
}
if (thumb != null) {
Geometry tg = new Geometry("thumb", new Quad(THUMB_SZ, THUMB_SZ));
Material tm = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
tm.setTexture("ColorMap", thumb);
tm.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
tg.setQueueBucket(RenderQueue.Bucket.Transparent);
tg.setMaterial(tm);
tg.setLocalTranslation(thumbX, thumbY, -16);
parent.attachChild(tg);
} else {
quad(parent, thumbX, thumbY, THUMB_SZ, THUMB_SZ, COL_THUMB_BG, -16);
}
// Anzahl-Badge unten rechts (nur wenn > 1)
if (count > 1) {
String cntStr = count > 999 ? "999+" : String.valueOf(count);
BitmapText cntTxt = txt(cntStr, 13, COL_WHITE);
float bw = Math.max(26, cntTxt.getLineWidth() + 8);
float bh = 18;
float bx = x + CELL_W - bw - 5;
float by = thumbY + 4;
quad(parent, bx, by, bw, bh, COL_BADGE, -15);
cntTxt.setLocalTranslation(bx + (bw - cntTxt.getLineWidth()) / 2f, by + bh - 3, -14);
parent.attachChild(cntTxt);
}
// Name
BitmapText nameTxt = txt(clip(item.getDisplayText(), 17), 13, COL_WHITE);
nameTxt.setLocalTranslation(x + (CELL_W - nameTxt.getLineWidth()) / 2f, y + 42, -16);
parent.attachChild(nameTxt);
// Goldwert
BitmapText goldTxt = txt(item.getWorthGold() + " G", 12, COL_GOLD);
goldTxt.setLocalTranslation(x + (CELL_W - goldTxt.getLineWidth()) / 2f, y + 22, -16);
parent.attachChild(goldTxt);
}
// ── Hilfsmethoden ─────────────────────────────────────────────────────────
/** Lädt Texture oder gibt null zurück wenn nicht vorhanden. */
private Texture loadThumb(String assetPath) {
try { return assetManager.loadTexture(assetPath); }
catch (Exception e) { return null; }
}
/** Items des Tabs sortiert nach SubKategorie-Ordinal, dann Preis. */
private List<Map.Entry<Item, Integer>> sortedItems(ItemCategory cat) {
Inventar inv = mc.getInventar();
if (inv == null) return List.of();
return inv.getItems().entrySet().stream()
.filter(e -> e.getKey().getCategory() == cat)
.sorted(Comparator
.<Map.Entry<Item, Integer>>comparingInt(e ->
e.getKey().getSubCategory() == null
? Integer.MAX_VALUE
: e.getKey().getSubCategory().ordinal())
.thenComparingInt(e -> e.getKey().getWorthGold()))
.collect(Collectors.toList());
}
private void destroyPanel() {
if (panel != null) { guiNode.detachChild(panel); panel = null; gridNode = null; }
}
private Geometry quad(Node parent, float x, float y, float w, float h, ColorRGBA col, float z) {
Geometry g = new Geometry("q", new Quad(w, h));
Material m = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
m.setColor("Color", col.clone());
if (col.a < 1f) {
m.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
g.setQueueBucket(RenderQueue.Bucket.Transparent);
}
g.setMaterial(m);
g.setLocalTranslation(x, y, z);
parent.attachChild(g);
return g;
}
private BitmapText txt(String s, int size, ColorRGBA col) {
BitmapText t = new BitmapText(font, false);
t.setSize(size); t.setColor(col); t.setText(s);
return t;
}
private static String clip(String s, int max) {
return s.length() <= max ? s : s.substring(0, max - 1) + "";
}
private static String catLabel(ItemCategory c) {
return switch (c) {
case WEAPON -> "Waffen";
case GEAR -> "Ausrüstung";
case CONSUMABLES -> "Verbrauchbar";
case QUEST_ITEMS -> "Quest";
case CRAFTING_ITEMS -> "Handwerk";
case MISC_ITEMS -> "Sonstiges";
};
}
private static String subCatLabel(ItemSubCategory s) {
return switch (s) {
case SWORD -> "Schwerter";
case TWO_HANDED_SWORD -> "Zweihandschwerter";
case STAFF -> "Stäbe";
case HALBERD -> "Hellebarden";
case AXE -> "Äxte";
case RING -> "Ringe";
case NECKLACE -> "Halsketten";
case ARMOR -> "Rüstungen";
case HELM -> "Helme";
case SHIELD -> "Schilde";
case POTION -> "Tränke";
case PERMANENT_POTION -> "Dauerhafte Tränke";
case FOOD -> "Nahrung";
case MAGICAL_ITEM -> "Magische Items";
case QUEST_ITEM -> "Quest-Items";
case PLANT -> "Pflanzen";
case TECHNICAL -> "Technisches";
case MAGICAL -> "Magisches";
case MISC -> "Sonstiges";
};
}
}

View File

@@ -0,0 +1,542 @@
package de.blight.game.state;
import com.jme3.scene.Mesh;
import com.jme3.scene.VertexBuffer;
import com.jme3.util.BufferUtils;
import de.blight.common.VoxelChunk;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.util.ArrayList;
import java.util.List;
/**
* Marching-Cubes Mesh-Generator für {@link VoxelChunk}.
*
* Vertex-Nummerierung einer MC-Zelle:
* v0=(x,y,z), v1=(x+s,y,z), v2=(x+s,y,z+s), v3=(x,y,z+s)
* v4=(x,y+s,z), v5=(x+s,y+s,z), v6=(x+s,y+s,z+s), v7=(x,y+s,z+s)
*
* 12 Kanten (von-nach):
* 0:v0-v1, 1:v1-v2, 2:v3-v2, 3:v0-v3
* 4:v4-v5, 5:v5-v6, 6:v7-v6, 7:v4-v7
* 8:v0-v4, 9:v1-v5, 10:v2-v6, 11:v3-v7
*/
public final class MarchingCubes {
private MarchingCubes() {}
// ── Vollständige Standard-Lookup-Tabellen (Lorensen/Cline) ─────────────────
/** Für jeden der 256 Cube-Zustände: Bitmask der 12 geschnittenen Kanten. */
private static final int[] EDGE_TABLE = {
0x000, 0x109, 0x203, 0x30a, 0x406, 0x50f, 0x605, 0x70c,
0x80c, 0x905, 0xa0f, 0xb06, 0xc0a, 0xd03, 0xe09, 0xf00,
0x190, 0x099, 0x393, 0x29a, 0x596, 0x49f, 0x795, 0x69c,
0x99c, 0x895, 0xb9f, 0xa96, 0xd9a, 0xc93, 0xf99, 0xe90,
0x230, 0x339, 0x033, 0x13a, 0x636, 0x73f, 0x435, 0x53c,
0xa3c, 0xb35, 0x83f, 0x936, 0xe3a, 0xf33, 0xc39, 0xd30,
0x3a0, 0x2a9, 0x1a3, 0x0aa, 0x7a6, 0x6af, 0x5a5, 0x4ac,
0xbac, 0xaa5, 0x9af, 0x8a6, 0xfaa, 0xea3, 0xda9, 0xca0,
0x460, 0x569, 0x663, 0x76a, 0x066, 0x16f, 0x265, 0x36c,
0xc6c, 0xd65, 0xe6f, 0xf66, 0x86a, 0x963, 0xa69, 0xb60,
0x5f0, 0x4f9, 0x7f3, 0x6fa, 0x1f6, 0x0ff, 0x3f5, 0x2fc,
0xdfc, 0xcf5, 0xfff, 0xef6, 0x9fa, 0x8f3, 0xbf9, 0xaf0,
0x650, 0x759, 0x453, 0x55a, 0x256, 0x35f, 0x055, 0x15c,
0xe5c, 0xf55, 0xc5f, 0xd56, 0xa5a, 0xb53, 0x859, 0x950,
0x7c0, 0x6c9, 0x5c3, 0x4ca, 0x3c6, 0x2cf, 0x1c5, 0x0cc,
0xfcc, 0xec5, 0xdcf, 0xcc6, 0xbca, 0xac3, 0x9c9, 0x8c0,
0x8c0, 0x9c9, 0xac3, 0xbca, 0xcc6, 0xdcf, 0xec5, 0xfcc,
0x0cc, 0x1c5, 0x2cf, 0x3c6, 0x4ca, 0x5c3, 0x6c9, 0x7c0,
0x950, 0x859, 0xb53, 0xa5a, 0xd56, 0xc5f, 0xf55, 0xe5c,
0x15c, 0x055, 0x35f, 0x256, 0x55a, 0x453, 0x759, 0x650,
0xaf0, 0xbf9, 0x8f3, 0x9fa, 0xef6, 0xfff, 0xcf5, 0xdfc,
0x2fc, 0x3f5, 0x0ff, 0x1f6, 0x6fa, 0x7f3, 0x4f9, 0x5f0,
0xb60, 0xa69, 0x963, 0x86a, 0xf66, 0xe6f, 0xd65, 0xc6c,
0x36c, 0x265, 0x16f, 0x066, 0x76a, 0x663, 0x569, 0x460,
0xca0, 0xda9, 0xea3, 0xfaa, 0x8a6, 0x9af, 0xaa5, 0xbac,
0x4ac, 0x5a5, 0x6af, 0x7a6, 0x0aa, 0x1a3, 0x2a9, 0x3a0,
0xd30, 0xc39, 0xf33, 0xe3a, 0x936, 0x835, 0xb3f, 0xa36, // NOTE: 0x835 = 0x83f corrected below
0x53c, 0x435, 0x73f, 0x636, 0x13a, 0x033, 0x339, 0x230,
0xe90, 0xf99, 0xc93, 0xd9a, 0xa96, 0xb9f, 0x895, 0x99c,
0x69c, 0x795, 0x49f, 0x596, 0x29a, 0x393, 0x099, 0x190,
0xf00, 0xe09, 0xd03, 0xc0a, 0xb06, 0xa0f, 0x905, 0x80c,
0x70c, 0x605, 0x50f, 0x406, 0x30a, 0x203, 0x109, 0x000
};
/**
* TRI_TABLE[cubeIndex] = Liste von Kanten-Indizes (011) in Dreiergruppen,
* terminiert durch -1. Maximal 16 Einträge pro Zeile.
* Standard Paul Bourke / Lorensen-Cline Tabelle.
*/
private static final int[][] TRI_TABLE = {
{-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 0, 8, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 0, 1, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 1, 8, 3, 9, 8, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 1, 2, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 0, 8, 3, 1, 2, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 9, 2, 10, 0, 2, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 2, 8, 3, 2, 10, 8, 10, 9, 8, -1, -1, -1, -1, -1, -1, -1},
{ 3, 11, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 0, 11, 2, 8, 11, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 1, 9, 0, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 1, 11, 2, 1, 9, 11, 9, 8, 11, -1, -1, -1, -1, -1, -1, -1},
{ 3, 10, 1, 11, 10, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 0, 10, 1, 0, 8, 10, 8, 11, 10, -1, -1, -1, -1, -1, -1, -1},
{ 3, 9, 0, 3, 11, 9, 11, 10, 9, -1, -1, -1, -1, -1, -1, -1},
{ 9, 8, 11, 9, 11, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 4, 7, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 4, 3, 0, 7, 3, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 0, 1, 9, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 4, 1, 9, 4, 7, 1, 7, 3, 1, -1, -1, -1, -1, -1, -1, -1},
{ 1, 2, 10, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 3, 4, 7, 3, 0, 4, 1, 2, 10, -1, -1, -1, -1, -1, -1, -1},
{ 9, 2, 10, 9, 0, 2, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1},
{ 2, 10, 9, 2, 9, 7, 2, 7, 3, 7, 9, 4, -1, -1, -1, -1},
{ 8, 4, 7, 3, 11, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{11, 4, 7, 11, 2, 4, 2, 0, 4, -1, -1, -1, -1, -1, -1, -1},
{ 9, 0, 1, 8, 4, 7, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1},
{ 4, 7, 11, 9, 4, 11, 9, 11, 2, 9, 2, 1, -1, -1, -1, -1},
{ 3, 10, 1, 3, 11, 10, 7, 8, 4, -1, -1, -1, -1, -1, -1, -1},
{ 1, 11, 10, 1, 4, 11, 1, 0, 4, 7, 11, 4, -1, -1, -1, -1},
{ 4, 7, 8, 9, 0, 11, 9, 11, 10, 11, 0, 3, -1, -1, -1, -1},
{ 4, 7, 11, 4, 11, 9, 9, 11, 10, -1, -1, -1, -1, -1, -1, -1},
{ 9, 5, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 9, 5, 4, 0, 8, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 0, 5, 4, 1, 5, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 8, 5, 4, 8, 3, 5, 3, 1, 5, -1, -1, -1, -1, -1, -1, -1},
{ 1, 2, 10, 9, 5, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 3, 0, 8, 1, 2, 10, 4, 9, 5, -1, -1, -1, -1, -1, -1, -1},
{ 5, 2, 10, 5, 4, 2, 4, 0, 2, -1, -1, -1, -1, -1, -1, -1},
{ 2, 10, 5, 3, 2, 5, 3, 5, 4, 3, 4, 8, -1, -1, -1, -1},
{ 9, 5, 4, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 0, 11, 2, 0, 8, 11, 4, 9, 5, -1, -1, -1, -1, -1, -1, -1},
{ 0, 5, 4, 0, 1, 5, 2, 3, 11, -1, -1, -1, -1, -1, -1, -1},
{ 2, 1, 5, 2, 5, 8, 2, 8, 11, 4, 8, 5, -1, -1, -1, -1},
{10, 3, 11, 10, 1, 3, 9, 5, 4, -1, -1, -1, -1, -1, -1, -1},
{ 4, 9, 5, 0, 8, 1, 8, 10, 1, 8, 11, 10, -1, -1, -1, -1},
{ 5, 4, 0, 5, 0, 11, 5, 11, 10, 11, 0, 3, -1, -1, -1, -1},
{ 5, 4, 8, 5, 8, 10, 10, 8, 11, -1, -1, -1, -1, -1, -1, -1},
{ 9, 7, 8, 5, 7, 9, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 9, 3, 0, 9, 5, 3, 5, 7, 3, -1, -1, -1, -1, -1, -1, -1},
{ 0, 7, 8, 0, 1, 7, 1, 5, 7, -1, -1, -1, -1, -1, -1, -1},
{ 1, 5, 3, 3, 5, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 9, 7, 8, 9, 5, 7, 10, 1, 2, -1, -1, -1, -1, -1, -1, -1},
{10, 1, 2, 9, 5, 0, 5, 3, 0, 5, 7, 3, -1, -1, -1, -1},
{ 8, 0, 2, 8, 2, 5, 8, 5, 7, 10, 5, 2, -1, -1, -1, -1},
{ 2, 10, 5, 2, 5, 3, 3, 5, 7, -1, -1, -1, -1, -1, -1, -1},
{ 7, 9, 5, 7, 8, 9, 3, 11, 2, -1, -1, -1, -1, -1, -1, -1},
{ 9, 5, 7, 9, 7, 2, 9, 2, 0, 2, 7, 11, -1, -1, -1, -1},
{ 2, 3, 11, 0, 1, 8, 1, 7, 8, 1, 5, 7, -1, -1, -1, -1},
{11, 2, 1, 11, 1, 7, 7, 1, 5, -1, -1, -1, -1, -1, -1, -1},
{ 9, 5, 8, 8, 5, 7, 10, 1, 3, 10, 3, 11, -1, -1, -1, -1},
{ 5, 7, 0, 5, 0, 9, 7, 11, 0, 1, 0, 10, 11, 10, 0, -1},
{11, 10, 0, 11, 0, 3, 10, 5, 0, 8, 0, 7, 5, 7, 0, -1},
{11, 10, 5, 7, 11, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{10, 6, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 0, 8, 3, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 9, 0, 1, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 1, 8, 3, 1, 9, 8, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1},
{ 1, 6, 5, 2, 6, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 1, 6, 5, 1, 2, 6, 3, 0, 8, -1, -1, -1, -1, -1, -1, -1},
{ 9, 6, 5, 9, 0, 6, 0, 2, 6, -1, -1, -1, -1, -1, -1, -1},
{ 5, 9, 8, 5, 8, 2, 5, 2, 6, 3, 2, 8, -1, -1, -1, -1},
{ 2, 3, 11, 10, 6, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{11, 0, 8, 11, 2, 0, 10, 6, 5, -1, -1, -1, -1, -1, -1, -1},
{ 0, 1, 9, 2, 3, 11, 5, 10, 6, -1, -1, -1, -1, -1, -1, -1},
{ 5, 10, 6, 1, 9, 2, 9, 11, 2, 9, 8, 11, -1, -1, -1, -1},
{ 6, 3, 11, 6, 5, 3, 5, 1, 3, -1, -1, -1, -1, -1, -1, -1},
{ 0, 8, 11, 0, 11, 5, 0, 5, 1, 5, 11, 6, -1, -1, -1, -1},
{ 3, 11, 6, 0, 3, 6, 0, 6, 5, 0, 5, 9, -1, -1, -1, -1},
{ 6, 5, 9, 6, 9, 11, 11, 9, 8, -1, -1, -1, -1, -1, -1, -1},
{ 5, 10, 6, 4, 7, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 4, 3, 0, 4, 7, 3, 6, 5, 10, -1, -1, -1, -1, -1, -1, -1},
{ 1, 9, 0, 5, 10, 6, 8, 4, 7, -1, -1, -1, -1, -1, -1, -1},
{10, 6, 5, 1, 9, 7, 1, 7, 3, 7, 9, 4, -1, -1, -1, -1},
{ 6, 1, 2, 6, 5, 1, 4, 7, 8, -1, -1, -1, -1, -1, -1, -1},
{ 1, 2, 5, 5, 2, 6, 3, 0, 4, 3, 4, 7, -1, -1, -1, -1},
{ 8, 4, 7, 9, 0, 5, 0, 6, 5, 0, 2, 6, -1, -1, -1, -1},
{ 7, 3, 9, 7, 9, 4, 3, 2, 9, 5, 9, 6, 2, 6, 9, -1},
{ 3, 11, 2, 7, 8, 4, 10, 6, 5, -1, -1, -1, -1, -1, -1, -1},
{ 5, 10, 6, 4, 7, 2, 4, 2, 0, 2, 7, 11, -1, -1, -1, -1},
{ 0, 1, 9, 4, 7, 8, 2, 3, 11, 5, 10, 6, -1, -1, -1, -1},
{ 9, 2, 1, 9, 11, 2, 9, 4, 11, 7, 11, 4, 5, 10, 6, -1},
{ 8, 4, 7, 3, 11, 5, 3, 5, 1, 5, 11, 6, -1, -1, -1, -1},
{ 5, 1, 11, 5, 11, 6, 1, 0, 11, 7, 11, 4, 0, 4, 11, -1},
{ 0, 5, 9, 0, 6, 5, 0, 3, 6, 11, 6, 3, 8, 4, 7, -1},
{ 6, 5, 9, 6, 9, 11, 4, 7, 9, 7, 11, 9, -1, -1, -1, -1},
{10, 4, 9, 6, 4, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 4, 10, 6, 4, 9, 10, 0, 8, 3, -1, -1, -1, -1, -1, -1, -1},
{10, 0, 1, 10, 6, 0, 6, 4, 0, -1, -1, -1, -1, -1, -1, -1},
{ 8, 3, 1, 8, 1, 6, 8, 6, 4, 6, 1, 10, -1, -1, -1, -1},
{ 1, 4, 9, 1, 2, 4, 2, 6, 4, -1, -1, -1, -1, -1, -1, -1},
{ 3, 0, 8, 1, 2, 9, 2, 4, 9, 2, 6, 4, -1, -1, -1, -1},
{ 0, 2, 4, 4, 2, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 8, 3, 2, 8, 2, 4, 4, 2, 6, -1, -1, -1, -1, -1, -1, -1},
{10, 4, 9, 10, 6, 4, 11, 2, 3, -1, -1, -1, -1, -1, -1, -1},
{ 0, 8, 2, 2, 8, 11, 4, 9, 10, 4, 10, 6, -1, -1, -1, -1},
{ 3, 11, 2, 0, 1, 6, 0, 6, 4, 6, 1, 10, -1, -1, -1, -1},
{ 6, 4, 1, 6, 1, 10, 4, 8, 1, 2, 1, 11, 8, 11, 1, -1},
{ 9, 6, 4, 9, 3, 6, 9, 1, 3, 11, 6, 3, -1, -1, -1, -1},
{ 8, 11, 1, 8, 1, 0, 11, 6, 1, 9, 1, 4, 6, 4, 1, -1},
{ 3, 11, 6, 3, 6, 0, 0, 6, 4, -1, -1, -1, -1, -1, -1, -1},
{ 6, 4, 8, 11, 6, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 7, 10, 6, 7, 8, 10, 8, 9, 10, -1, -1, -1, -1, -1, -1, -1},
{ 0, 7, 3, 0, 10, 7, 0, 9, 10, 6, 7, 10, -1, -1, -1, -1},
{10, 6, 7, 1, 10, 7, 1, 7, 8, 1, 8, 0, -1, -1, -1, -1},
{10, 6, 7, 10, 7, 1, 1, 7, 3, -1, -1, -1, -1, -1, -1, -1},
{ 1, 2, 6, 1, 6, 8, 1, 8, 9, 8, 6, 7, -1, -1, -1, -1},
{ 2, 6, 9, 2, 9, 1, 6, 7, 9, 0, 9, 3, 7, 3, 9, -1},
{ 7, 8, 0, 7, 0, 6, 6, 0, 2, -1, -1, -1, -1, -1, -1, -1},
{ 7, 3, 2, 6, 7, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 2, 3, 11, 10, 6, 8, 10, 8, 9, 8, 6, 7, -1, -1, -1, -1},
{ 2, 0, 7, 2, 7, 11, 0, 9, 7, 6, 7, 10, 9, 10, 7, -1},
{ 1, 8, 0, 1, 7, 8, 1, 10, 7, 6, 7, 10, 2, 3, 11, -1},
{11, 2, 1, 11, 1, 7, 10, 6, 1, 6, 7, 1, -1, -1, -1, -1},
{ 8, 9, 1, 8, 1, 3, 9, 6, 1, 11, 1, 7, 6, 7, 1, -1},
{ 0, 9, 1, 11, 6, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 7, 8, 0, 7, 0, 6, 3, 11, 0, 11, 6, 0, -1, -1, -1, -1},
{ 7, 11, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 7, 6, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 3, 0, 8, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 0, 1, 9, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 8, 1, 9, 8, 3, 1, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1},
{10, 1, 2, 6, 11, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 1, 2, 10, 3, 0, 8, 6, 11, 7, -1, -1, -1, -1, -1, -1, -1},
{ 2, 9, 0, 2, 10, 9, 6, 11, 7, -1, -1, -1, -1, -1, -1, -1},
{ 6, 11, 7, 2, 10, 3, 10, 8, 3, 10, 9, 8, -1, -1, -1, -1},
{ 7, 2, 3, 6, 2, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 7, 0, 8, 7, 6, 0, 6, 2, 0, -1, -1, -1, -1, -1, -1, -1},
{ 2, 7, 6, 2, 3, 7, 0, 1, 9, -1, -1, -1, -1, -1, -1, -1},
{ 1, 6, 2, 1, 8, 6, 1, 9, 8, 8, 7, 6, -1, -1, -1, -1},
{10, 7, 6, 10, 1, 7, 1, 3, 7, -1, -1, -1, -1, -1, -1, -1},
{10, 7, 6, 1, 7, 10, 1, 8, 7, 1, 0, 8, -1, -1, -1, -1},
{ 0, 3, 7, 0, 7, 10, 0, 10, 9, 6, 10, 7, -1, -1, -1, -1},
{ 7, 6, 10, 7, 10, 8, 8, 10, 9, -1, -1, -1, -1, -1, -1, -1},
{ 6, 8, 4, 11, 8, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 3, 6, 11, 3, 0, 6, 0, 4, 6, -1, -1, -1, -1, -1, -1, -1},
{ 8, 6, 11, 8, 4, 6, 9, 0, 1, -1, -1, -1, -1, -1, -1, -1},
{ 9, 4, 6, 9, 6, 3, 9, 3, 1, 11, 3, 6, -1, -1, -1, -1},
{ 6, 8, 4, 6, 11, 8, 2, 10, 1, -1, -1, -1, -1, -1, -1, -1},
{ 1, 2, 10, 3, 0, 11, 0, 6, 11, 0, 4, 6, -1, -1, -1, -1},
{ 4, 11, 8, 4, 6, 11, 0, 2, 9, 2, 10, 9, -1, -1, -1, -1},
{10, 9, 3, 10, 3, 2, 9, 4, 3, 11, 3, 6, 4, 6, 3, -1},
{ 8, 2, 3, 8, 4, 2, 4, 6, 2, -1, -1, -1, -1, -1, -1, -1},
{ 0, 4, 2, 4, 6, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 1, 9, 0, 2, 3, 4, 2, 4, 6, 4, 3, 8, -1, -1, -1, -1},
{ 1, 9, 4, 1, 4, 2, 2, 4, 6, -1, -1, -1, -1, -1, -1, -1},
{ 8, 1, 3, 8, 6, 1, 8, 4, 6, 6, 10, 1, -1, -1, -1, -1},
{10, 1, 0, 10, 0, 6, 6, 0, 4, -1, -1, -1, -1, -1, -1, -1},
{ 4, 6, 3, 4, 3, 8, 6, 10, 3, 0, 3, 9, 10, 9, 3, -1},
{10, 9, 4, 6, 10, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 4, 9, 5, 7, 6, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 0, 8, 3, 4, 9, 5, 11, 7, 6, -1, -1, -1, -1, -1, -1, -1},
{ 5, 0, 1, 5, 4, 0, 7, 6, 11, -1, -1, -1, -1, -1, -1, -1},
{11, 7, 6, 8, 3, 4, 3, 5, 4, 3, 1, 5, -1, -1, -1, -1},
{ 9, 5, 4, 10, 1, 2, 7, 6, 11, -1, -1, -1, -1, -1, -1, -1},
{ 6, 11, 7, 1, 2, 10, 0, 8, 3, 4, 9, 5, -1, -1, -1, -1},
{ 7, 6, 11, 5, 4, 10, 4, 2, 10, 4, 0, 2, -1, -1, -1, -1},
{ 3, 4, 8, 3, 5, 4, 3, 2, 5, 10, 5, 2, 11, 7, 6, -1},
{ 7, 2, 3, 7, 6, 2, 5, 4, 9, -1, -1, -1, -1, -1, -1, -1},
{ 9, 5, 4, 0, 8, 6, 0, 6, 2, 6, 8, 7, -1, -1, -1, -1},
{ 3, 6, 2, 3, 7, 6, 1, 5, 0, 5, 4, 0, -1, -1, -1, -1},
{ 6, 2, 8, 6, 8, 7, 2, 1, 8, 4, 8, 5, 1, 5, 8, -1},
{ 9, 5, 4, 10, 1, 6, 1, 7, 6, 1, 3, 7, -1, -1, -1, -1},
{ 1, 6, 10, 1, 7, 6, 1, 0, 7, 8, 7, 0, 9, 5, 4, -1},
{ 4, 0, 10, 4, 10, 5, 0, 3, 10, 6, 10, 7, 3, 7, 10, -1},
{ 7, 6, 10, 7, 10, 8, 5, 4, 10, 4, 8, 10, -1, -1, -1, -1},
{ 6, 9, 5, 6, 11, 9, 11, 8, 9, -1, -1, -1, -1, -1, -1, -1},
{ 3, 6, 11, 0, 6, 3, 0, 5, 6, 0, 9, 5, -1, -1, -1, -1},
{ 0, 11, 8, 0, 5, 11, 0, 1, 5, 5, 6, 11, -1, -1, -1, -1},
{ 6, 11, 3, 6, 3, 5, 5, 3, 1, -1, -1, -1, -1, -1, -1, -1},
{ 1, 2, 10, 9, 5, 11, 9, 11, 8, 11, 5, 6, -1, -1, -1, -1},
{ 0, 11, 3, 0, 6, 11, 0, 9, 6, 5, 6, 9, 1, 2, 10, -1},
{11, 8, 5, 11, 5, 6, 8, 0, 5, 10, 5, 2, 0, 2, 5, -1},
{ 6, 11, 3, 6, 3, 5, 2, 10, 3, 10, 5, 3, -1, -1, -1, -1},
{ 5, 8, 9, 5, 2, 8, 5, 6, 2, 3, 8, 2, -1, -1, -1, -1},
{ 9, 5, 6, 9, 6, 0, 0, 6, 2, -1, -1, -1, -1, -1, -1, -1},
{ 1, 5, 8, 1, 8, 0, 5, 6, 8, 3, 8, 2, 6, 2, 8, -1},
{ 1, 5, 6, 2, 1, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 1, 3, 6, 1, 6, 10, 3, 8, 6, 5, 6, 9, 8, 9, 6, -1},
{10, 1, 0, 10, 0, 6, 9, 5, 0, 5, 6, 0, -1, -1, -1, -1},
{ 0, 3, 8, 5, 6, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{10, 5, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{11, 5, 10, 7, 5, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{11, 5, 10, 11, 7, 5, 8, 3, 0, -1, -1, -1, -1, -1, -1, -1},
{ 5, 11, 7, 5, 10, 11, 1, 9, 0, -1, -1, -1, -1, -1, -1, -1},
{10, 7, 5, 10, 11, 7, 9, 8, 1, 8, 3, 1, -1, -1, -1, -1},
{11, 1, 2, 11, 7, 1, 7, 5, 1, -1, -1, -1, -1, -1, -1, -1},
{ 0, 8, 3, 1, 2, 7, 1, 7, 5, 7, 2, 11, -1, -1, -1, -1},
{ 9, 7, 5, 9, 2, 7, 9, 0, 2, 2, 11, 7, -1, -1, -1, -1},
{ 7, 5, 2, 7, 2, 11, 5, 9, 2, 3, 2, 8, 9, 8, 2, -1},
{ 2, 5, 10, 2, 3, 5, 3, 7, 5, -1, -1, -1, -1, -1, -1, -1},
{ 8, 2, 0, 8, 5, 2, 8, 7, 5, 10, 2, 5, -1, -1, -1, -1},
{ 9, 0, 1, 5, 10, 3, 5, 3, 7, 3, 10, 2, -1, -1, -1, -1},
{ 9, 8, 2, 9, 2, 1, 8, 7, 2, 10, 2, 5, 7, 5, 2, -1},
{ 1, 3, 5, 3, 7, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 0, 8, 7, 0, 7, 1, 1, 7, 5, -1, -1, -1, -1, -1, -1, -1},
{ 9, 0, 3, 9, 3, 5, 5, 3, 7, -1, -1, -1, -1, -1, -1, -1},
{ 9, 8, 7, 5, 9, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 5, 8, 4, 5, 10, 8, 10, 11, 8, -1, -1, -1, -1, -1, -1, -1},
{ 5, 0, 4, 5, 11, 0, 5, 10, 11, 11, 3, 0, -1, -1, -1, -1},
{ 0, 1, 9, 8, 4, 10, 8, 10, 11, 10, 4, 5, -1, -1, -1, -1},
{10, 11, 4, 10, 4, 5, 11, 3, 4, 9, 4, 1, 3, 1, 4, -1},
{ 2, 5, 1, 2, 8, 5, 2, 11, 8, 4, 5, 8, -1, -1, -1, -1},
{ 0, 4, 11, 0, 11, 3, 4, 5, 11, 2, 11, 1, 5, 1, 11, -1},
{ 0, 2, 5, 0, 5, 9, 2, 11, 5, 4, 5, 8, 11, 8, 5, -1},
{ 9, 4, 5, 2, 11, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 2, 5, 10, 3, 5, 2, 3, 4, 5, 3, 8, 4, -1, -1, -1, -1},
{ 5, 10, 2, 5, 2, 4, 4, 2, 0, -1, -1, -1, -1, -1, -1, -1},
{ 3, 10, 2, 3, 5, 10, 3, 8, 5, 4, 5, 8, 0, 1, 9, -1},
{ 5, 10, 2, 5, 2, 4, 1, 9, 2, 9, 4, 2, -1, -1, -1, -1},
{ 8, 4, 5, 8, 5, 3, 3, 5, 1, -1, -1, -1, -1, -1, -1, -1},
{ 0, 4, 5, 1, 0, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 8, 4, 5, 8, 5, 3, 9, 0, 5, 0, 3, 5, -1, -1, -1, -1},
{ 9, 4, 5, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 4, 11, 7, 4, 9, 11, 9, 10, 11, -1, -1, -1, -1, -1, -1, -1},
{ 0, 8, 3, 4, 9, 7, 9, 11, 7, 9, 10, 11, -1, -1, -1, -1},
{ 1, 10, 11, 1, 11, 4, 1, 4, 0, 7, 4, 11, -1, -1, -1, -1},
{ 3, 1, 4, 3, 4, 8, 1, 10, 4, 7, 4, 11, 10, 11, 4, -1},
{ 4, 11, 7, 9, 11, 4, 9, 2, 11, 9, 1, 2, -1, -1, -1, -1},
{ 9, 7, 4, 9, 11, 7, 9, 1, 11, 2, 11, 1, 0, 8, 3, -1},
{11, 7, 4, 11, 4, 2, 2, 4, 0, -1, -1, -1, -1, -1, -1, -1},
{11, 7, 4, 11, 4, 2, 8, 3, 4, 3, 2, 4, -1, -1, -1, -1},
{ 2, 9, 10, 2, 7, 9, 2, 3, 7, 7, 4, 9, -1, -1, -1, -1},
{ 9, 10, 7, 9, 7, 4, 10, 2, 7, 8, 7, 0, 2, 0, 7, -1},
{ 3, 7, 10, 3, 10, 2, 7, 4, 10, 1, 10, 0, 4, 0, 10, -1},
{ 1, 10, 2, 8, 7, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 4, 9, 1, 4, 1, 7, 7, 1, 3, -1, -1, -1, -1, -1, -1, -1},
{ 4, 9, 1, 4, 1, 7, 0, 8, 1, 8, 7, 1, -1, -1, -1, -1},
{ 4, 0, 3, 7, 4, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 4, 8, 7, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 9, 10, 8, 10, 11, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 3, 0, 9, 3, 9, 11, 11, 9, 10, -1, -1, -1, -1, -1, -1, -1},
{ 0, 1, 10, 0, 10, 8, 8, 10, 11, -1, -1, -1, -1, -1, -1, -1},
{ 3, 1, 10, 11, 3, 10, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 1, 2, 11, 1, 11, 9, 9, 11, 8, -1, -1, -1, -1, -1, -1, -1},
{ 3, 0, 9, 3, 9, 11, 1, 2, 9, 2, 11, 9, -1, -1, -1, -1},
{ 0, 2, 11, 8, 0, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 3, 2, 11, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 2, 3, 8, 2, 8, 10, 10, 8, 9, -1, -1, -1, -1, -1, -1, -1},
{ 9, 10, 2, 0, 9, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 2, 3, 8, 2, 8, 10, 0, 1, 8, 1, 10, 8, -1, -1, -1, -1},
{ 1, 10, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 1, 3, 8, 9, 1, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 0, 9, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{ 0, 3, 8, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1},
{-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}
};
// ── Öffentliche Methode ────────────────────────────────────────────────────
/**
* Erzeugt ein JME-Mesh aus den Voxel-Daten mit dem angegebenen LOD-Schritt.
*
* @param chunk Quelldaten
* @param lodStep 1=LOD0 (voll), 4=LOD1, 16=LOD2
* @return JME-Mesh oder null wenn keine Oberfläche vorhanden
*/
public static Mesh build(VoxelChunk chunk, int lodStep) {
if (chunk.isEmpty()) return null;
// Flache Listen für Positions-, Normal- und Color-Daten
List<Float> positions = new ArrayList<>(4096);
List<Float> normals = new ArrayList<>(4096);
List<Float> colors = new ArrayList<>(4096);
int cells = VoxelChunk.CELLS; // 128
int size = VoxelChunk.SIZE; // 129
// Schrittzahl berechnen Anzahl Zellen = cells / lodStep
int steps = cells / lodStep;
for (int iy = 0; iy < steps; iy++) {
for (int iz = 0; iz < steps; iz++) {
for (int ix = 0; ix < steps; ix++) {
// Eckpunkt-Koordinaten (lokaler Voxel-Raum, 0..128)
int x0 = ix * lodStep;
int y0 = iy * lodStep;
int z0 = iz * lodStep;
int x1 = x0 + lodStep;
int y1 = y0 + lodStep;
int z1 = z0 + lodStep;
// 8 Dichte-Werte der Eckpunkte
float d0 = chunk.getDensity(x0, y0, z0);
float d1 = chunk.getDensity(x1, y0, z0);
float d2 = chunk.getDensity(x1, y0, z1);
float d3 = chunk.getDensity(x0, y0, z1);
float d4 = chunk.getDensity(x0, y1, z0);
float d5 = chunk.getDensity(x1, y1, z0);
float d6 = chunk.getDensity(x1, y1, z1);
float d7 = chunk.getDensity(x0, y1, z1);
// cubeIndex aufbauen (Bit gesetzt = Vertex > 0 = solid)
int cubeIndex = 0;
if (d0 > 0) cubeIndex |= 1;
if (d1 > 0) cubeIndex |= 2;
if (d2 > 0) cubeIndex |= 4;
if (d3 > 0) cubeIndex |= 8;
if (d4 > 0) cubeIndex |= 16;
if (d5 > 0) cubeIndex |= 32;
if (d6 > 0) cubeIndex |= 64;
if (d7 > 0) cubeIndex |= 128;
if (EDGE_TABLE[cubeIndex] == 0) continue;
// Materialen der 8 Eckpunkte
int m0 = chunk.getMaterial(x0, y0, z0) & 0xFF;
int m1 = chunk.getMaterial(x1, y0, z0) & 0xFF;
int m2 = chunk.getMaterial(x1, y0, z1) & 0xFF;
int m3 = chunk.getMaterial(x0, y0, z1) & 0xFF;
int m4 = chunk.getMaterial(x0, y1, z0) & 0xFF;
int m5 = chunk.getMaterial(x1, y1, z0) & 0xFF;
int m6 = chunk.getMaterial(x1, y1, z1) & 0xFF;
int m7 = chunk.getMaterial(x0, y1, z1) & 0xFF;
// Positionen der 8 Eckpunkte als float[]
float[] vx = { x0, x1, x1, x0, x0, x1, x1, x0 };
float[] vy = { y0, y0, y0, y0, y1, y1, y1, y1 };
float[] vz = { z0, z0, z1, z1, z0, z0, z1, z1 };
float[] dd = { d0, d1, d2, d3, d4, d5, d6, d7 };
int[] mm = { m0, m1, m2, m3, m4, m5, m6, m7 };
// 12 Kantenpunkte berechnen
float[] epx = new float[12];
float[] epy = new float[12];
float[] epz = new float[12];
float[] enx = new float[12];
float[] eny = new float[12];
float[] enz = new float[12];
float[] ecR = new float[12];
float[] ecG = new float[12];
float[] ecB = new float[12];
float[] ecA = new float[12];
// Kante i verbindet Vertex A mit Vertex B
int[][] edgeVerts = {
{0,1}, {1,2}, {3,2}, {0,3},
{4,5}, {5,6}, {7,6}, {4,7},
{0,4}, {1,5}, {2,6}, {3,7}
};
int edgeMask = EDGE_TABLE[cubeIndex];
for (int e = 0; e < 12; e++) {
if ((edgeMask & (1 << e)) == 0) continue;
int a = edgeVerts[e][0];
int b = edgeVerts[e][1];
float dA = dd[a], dB = dd[b];
float t;
if (Math.abs(dB - dA) < 0.001f) {
t = 0.5f;
} else {
t = (0f - dA) / (dB - dA);
if (t < 1e-6f) t = 1e-6f;
if (t > 1f - 1e-6f) t = 1f - 1e-6f;
}
epx[e] = vx[a] + t * (vx[b] - vx[a]);
epy[e] = vy[a] + t * (vy[b] - vy[a]);
epz[e] = vz[a] + t * (vz[b] - vz[a]);
// Normalen via Gradient
float[] gA = gradient(chunk, (int)vx[a], (int)vy[a], (int)vz[a]);
float[] gB = gradient(chunk, (int)vx[b], (int)vy[b], (int)vz[b]);
enx[e] = gA[0] + t * (gB[0] - gA[0]);
eny[e] = gA[1] + t * (gB[1] - gA[1]);
enz[e] = gA[2] + t * (gB[2] - gA[2]);
float nlen = (float)Math.sqrt(enx[e]*enx[e] + eny[e]*eny[e] + enz[e]*enz[e]);
if (nlen > 1e-6f) { enx[e] /= nlen; eny[e] /= nlen; enz[e] /= nlen; }
// Material-Blend
float[] wA = matWeights(mm[a]);
float[] wB = matWeights(mm[b]);
ecR[e] = wA[0] + t * (wB[0] - wA[0]);
ecG[e] = wA[1] + t * (wB[1] - wA[1]);
ecB[e] = wA[2] + t * (wB[2] - wA[2]);
ecA[e] = wA[3] + t * (wB[3] - wA[3]);
}
// Dreiecke ausgeben
int[] tri = TRI_TABLE[cubeIndex];
for (int t = 0; t < 16; t += 3) {
if (tri[t] < 0) break;
int e0 = tri[t], e1 = tri[t+1], e2 = tri[t+2];
// Vertex 0
positions.add(epx[e0]); positions.add(epy[e0]); positions.add(epz[e0]);
normals.add(enx[e0]); normals.add(eny[e0]); normals.add(enz[e0]);
colors.add(ecR[e0]); colors.add(ecG[e0]); colors.add(ecB[e0]); colors.add(ecA[e0]);
// Vertex 1
positions.add(epx[e1]); positions.add(epy[e1]); positions.add(epz[e1]);
normals.add(enx[e1]); normals.add(eny[e1]); normals.add(enz[e1]);
colors.add(ecR[e1]); colors.add(ecG[e1]); colors.add(ecB[e1]); colors.add(ecA[e1]);
// Vertex 2
positions.add(epx[e2]); positions.add(epy[e2]); positions.add(epz[e2]);
normals.add(enx[e2]); normals.add(eny[e2]); normals.add(enz[e2]);
colors.add(ecR[e2]); colors.add(ecG[e2]); colors.add(ecB[e2]); colors.add(ecA[e2]);
}
}
}
}
if (positions.isEmpty()) return null;
int vertCount = positions.size() / 3;
FloatBuffer posBuf = BufferUtils.createFloatBuffer(positions.size());
FloatBuffer normBuf = BufferUtils.createFloatBuffer(normals.size());
FloatBuffer colBuf = BufferUtils.createFloatBuffer(colors.size());
IntBuffer idxBuf = BufferUtils.createIntBuffer(vertCount);
for (float v : positions) posBuf.put(v);
for (float v : normals) normBuf.put(v);
for (float v : colors) colBuf.put(v);
for (int i = 0; i < vertCount; i++) idxBuf.put(i);
posBuf.rewind(); normBuf.rewind(); colBuf.rewind(); idxBuf.rewind();
Mesh mesh = new Mesh();
mesh.setBuffer(VertexBuffer.Type.Position, 3, posBuf);
mesh.setBuffer(VertexBuffer.Type.Normal, 3, normBuf);
mesh.setBuffer(VertexBuffer.Type.Color, 4, colBuf);
mesh.setBuffer(VertexBuffer.Type.Index, 3, idxBuf);
mesh.updateBound();
return mesh;
}
// ── Hilfsmethoden ─────────────────────────────────────────────────────────
/** Berechnet den negierten Gradienten (zeigt von Solid weg) an Position (ix,iy,iz). */
private static float[] gradient(VoxelChunk chunk, int ix, int iy, int iz) {
float nx = getDensityClamped(chunk, ix+1, iy, iz )
- getDensityClamped(chunk, ix-1, iy, iz );
float ny = getDensityClamped(chunk, ix, iy+1, iz )
- getDensityClamped(chunk, ix, iy-1, iz );
float nz = getDensityClamped(chunk, ix, iy, iz+1)
- getDensityClamped(chunk, ix, iy, iz-1);
// negieren: Gradient zeigt vom Solid weg (nach außen)
float len = (float)Math.sqrt(nx*nx + ny*ny + nz*nz);
if (len < 1e-6f) return new float[]{ 0f, 1f, 0f };
return new float[]{ -nx/len, -ny/len, -nz/len };
}
/** Liest Dichte mit Klemmen an Chunk-Grenzen. */
private static float getDensityClamped(VoxelChunk chunk, int x, int y, int z) {
int s = VoxelChunk.SIZE - 1; // 128
x = Math.max(0, Math.min(s, x));
y = Math.max(0, Math.min(s, y));
z = Math.max(0, Math.min(s, z));
return chunk.getDensity(x, y, z);
}
/** Gibt vec4-Gewichte für ein Material zurück: 0→(1,0,0,0), 1→(0,1,0,0), usw. */
private static float[] matWeights(int matId) {
switch (matId & 3) {
case 0: return new float[]{ 1f, 0f, 0f, 0f };
case 1: return new float[]{ 0f, 1f, 0f, 0f };
case 2: return new float[]{ 0f, 0f, 1f, 0f };
default: return new float[]{ 0f, 0f, 0f, 1f };
}
}
}

View File

@@ -0,0 +1,122 @@
package de.blight.game.state;
import com.jme3.app.Application;
import com.jme3.app.state.BaseAppState;
import com.jme3.math.Vector3f;
import de.blight.common.SaveGame;
import de.blight.common.SaveGameIO;
import de.blight.common.model.Inventar;
import de.blight.common.model.Item;
import de.blight.common.model.MainCharacter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.Map;
import java.util.function.Supplier;
/**
* Verwaltet den persistenten Spielstand (Delta-Save).
* Wird von {@link de.blight.game.BlightGame} früh eingehängt, damit alle anderen
* States beim Starten bereits auf den geladenen Save zugreifen können.
*/
public class SaveGameState extends BaseAppState {
private static final Logger log = LoggerFactory.getLogger(SaveGameState.class);
private SaveGame save;
private MainCharacter mc;
private Supplier<Vector3f> positionProvider;
// ── Lifecycle ─────────────────────────────────────────────────────────────
@Override
protected void initialize(Application app) {
if (SaveGameIO.exists()) {
try {
save = SaveGameIO.load();
log.info("[Save] Spielstand geladen (gespeichert: {})", save.savedAt);
} catch (IOException e) {
log.warn("[Save] Laden fehlgeschlagen, starte neu: {}", e.getMessage());
save = new SaveGame();
}
} else {
log.info("[Save] Kein Spielstand gefunden, starte neues Spiel.");
save = new SaveGame();
}
}
@Override
protected void cleanup(Application app) {
persist();
}
@Override protected void onEnable() {}
@Override protected void onDisable() {}
// ── Public API ────────────────────────────────────────────────────────────
public SaveGame getSave() { return save; }
/**
* Aufgerufen von {@link de.blight.game.scene.WorldScene}, sobald Charakter
* und Physik-Position bereitstehen.
*/
public void bind(MainCharacter mc, Supplier<Vector3f> positionProvider) {
this.mc = mc;
this.positionProvider = positionProvider;
}
/** Meldet ein aufgesammeltes Item und löst sofort eine Sicherung aus. */
public void reportItemPickedUp(String uuid) {
save.world.pickedUpItems.add(uuid);
persist();
}
/** Meldet einen besiegten Gegner (für zukünftige Implementierung). */
public void reportEnemyDefeated(String enemyId) {
save.world.defeatedEnemies.add(enemyId);
persist();
}
/** Verwertet den aktuellen Spielstand und startet ein neues Spiel. */
public void resetForNewGame() {
try { java.nio.file.Files.deleteIfExists(de.blight.common.SaveGameIO.getSavePath()); }
catch (java.io.IOException ignored) {}
save = new de.blight.common.SaveGame();
mc = null;
positionProvider = null;
log.info("[Save] Neues Spiel gestartet Spielstand gelöscht.");
}
// ── Sicherung ─────────────────────────────────────────────────────────────
public void persist() {
if (mc == null) return;
// Spieler-Position
if (positionProvider != null) {
Vector3f pos = positionProvider.get();
save.character.positionSaved = true;
save.character.x = pos.x;
save.character.y = pos.y;
save.character.z = pos.z;
}
// Inventar
save.character.inventory.clear();
Inventar inv = mc.getInventar();
if (inv != null) {
for (Map.Entry<Item, Integer> e : inv.getItems().entrySet()) {
String id = e.getKey().getItemId();
if (id != null) save.character.inventory.put(id, e.getValue());
}
}
try {
SaveGameIO.save(save);
} catch (IOException e) {
log.error("[Save] Speichern fehlgeschlagen: {}", e.getMessage());
}
}
}

View File

@@ -0,0 +1,116 @@
package de.blight.game.state;
import com.jme3.bullet.BulletAppState;
import com.jme3.bullet.collision.shapes.MeshCollisionShape;
import com.jme3.bullet.control.RigidBodyControl;
import com.jme3.material.Material;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.Node;
import de.blight.common.VoxelChunk;
/**
* JME-Node für einen VoxelChunk mit 3 LOD-Geometrien.
* Wird von VoxelChunkState und VoxelEditorState verwaltet.
*
* Position im Weltraum: Translation = (cx*128-2048, cy*128, cz*128-2048).
*/
public class VoxelChunkNode extends Node {
private static final int[] LOD_STEPS = {1, 4, 16};
private final VoxelChunk chunk;
private final Material material;
private final Geometry[] lodGeos = new Geometry[3];
private int currentLod = -1;
private RigidBodyControl physics;
private BulletAppState bulletState;
public VoxelChunkNode(VoxelChunk chunk, Material material) {
super("voxel_" + chunk.cx + "_" + chunk.cy + "_" + chunk.cz);
this.chunk = chunk;
this.material = material;
// Welt-Translation setzen
float wx = chunk.cx * VoxelChunk.CELLS - 2048f;
float wy = chunk.cy * (float) VoxelChunk.CELLS;
float wz = chunk.cz * VoxelChunk.CELLS - 2048f;
setLocalTranslation(wx, wy, wz);
}
/** Baut das Mesh für den angegebenen LOD-Level neu. lod: 0/1/2. */
public void rebuildMesh(int lod) {
if (lod < 0 || lod > 2) return;
Mesh mesh = MarchingCubes.build(chunk, LOD_STEPS[lod]);
if (mesh == null) {
// Keine Oberfläche: evtl. vorherige Geo entfernen
if (lodGeos[lod] != null) { detachChild(lodGeos[lod]); lodGeos[lod] = null; }
return;
}
if (lodGeos[lod] == null) {
lodGeos[lod] = new Geometry("lod" + lod, mesh);
lodGeos[lod].setMaterial(material);
// Wenn dieser LOD gerade aktiv ist, sofort einhängen
if (lod == currentLod) attachChild(lodGeos[lod]);
} else {
lodGeos[lod].setMesh(mesh);
}
}
/**
* Setzt ein bereits fertig berechnetes Mesh für einen LOD-Level.
* Gedacht für den Hintergrund-Thread: Mesh dort bauen, dann hier übergeben.
*/
public void setLodMesh(int lod, Mesh mesh) {
if (lod < 0 || lod > 2 || mesh == null) return;
if (lodGeos[lod] == null) {
lodGeos[lod] = new Geometry("lod" + lod, mesh);
lodGeos[lod].setMaterial(material);
} else {
lodGeos[lod].setMesh(mesh);
}
}
/** Schaltet auf den angegebenen LOD um (blendet andere aus). */
public void setActiveLod(int lod) {
if (lod == currentLod) return;
currentLod = lod;
for (int i = 0; i < 3; i++) {
if (lodGeos[i] != null) detachChild(lodGeos[i]);
}
if (lodGeos[lod] != null) {
attachChild(lodGeos[lod]);
}
}
/** Erzeugt / aktualisiert die Physik-Kollision (LOD0-Mesh). */
public void updatePhysics(BulletAppState bullet) {
this.bulletState = bullet;
if (physics != null) {
bullet.getPhysicsSpace().remove(physics);
removeControl(physics);
}
if (lodGeos[0] == null || lodGeos[0].getMesh() == null) return;
MeshCollisionShape shape = new MeshCollisionShape(lodGeos[0].getMesh());
physics = new RigidBodyControl(shape, 0f);
addControl(physics);
bullet.getPhysicsSpace().add(physics);
}
public void removePhysics() {
if (physics == null || bulletState == null) return;
bulletState.getPhysicsSpace().remove(physics);
removeControl(physics);
physics = null;
}
public VoxelChunk getChunk() { return chunk; }
/** Gibt true zurück wenn mindestens ein LOD ein Mesh hat. */
public boolean hasMesh() {
for (Geometry g : lodGeos) if (g != null) return true;
return false;
}
}

View File

@@ -0,0 +1,177 @@
package de.blight.game.state;
import com.jme3.app.Application;
import com.jme3.app.SimpleApplication;
import com.jme3.app.state.BaseAppState;
import com.jme3.asset.AssetManager;
import com.jme3.bullet.BulletAppState;
import com.jme3.material.Material;
import com.jme3.scene.Node;
import com.jme3.texture.Image;
import com.jme3.texture.Texture;
import com.jme3.texture.TextureArray;
import de.blight.common.VoxelChunk;
import de.blight.common.VoxelChunkIO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.*;
/**
* Verwaltet die Voxel-Geometrie im Spiel als {@link TerrainChunkState.ChunkListener}.
*
* - Lädt VoxelChunks bei Sichtbarkeit (onChunkVisible)
* - Baut LOD-Meshes gemäß TerrainChunk-LOD
* - Aktiviert Physik-Collider bei LOD0 (PHYSICS_RANGE)
* - Nutzt Texture2DArray für 4 Voxel-Texturen (aus terrainTexturePaths)
*/
public class VoxelChunkState extends BaseAppState
implements TerrainChunkState.ChunkListener {
private static final Logger log = LoggerFactory.getLogger(VoxelChunkState.class);
private final BulletAppState bulletState;
private final String[] texturePaths; // 4 Pfade der unteren Terrain-Texturen
private SimpleApplication app;
private AssetManager assets;
private Node voxelRoot;
private Material voxelMaterial;
// key = cx | ((long)cy << 16) | ((long)cz << 32)
private final Map<Long, VoxelChunkNode> nodes = new HashMap<>();
public VoxelChunkState(BulletAppState bulletState, String[] texturePaths) {
this.bulletState = bulletState;
this.texturePaths = texturePaths;
}
@Override
protected void initialize(Application application) {
this.app = (SimpleApplication) application;
this.assets = app.getAssetManager();
voxelRoot = new Node("voxelRoot");
app.getRootNode().attachChild(voxelRoot);
voxelMaterial = buildMaterial();
}
@Override
protected void cleanup(Application app) {
voxelRoot.removeFromParent();
for (VoxelChunkNode n : nodes.values()) n.removePhysics();
nodes.clear();
}
@Override protected void onEnable() {}
@Override protected void onDisable() {}
@Override public void update(float tpf) {}
// ── ChunkListener ─────────────────────────────────────────────────────────
@Override
public void onChunkVisible(int cx, int cz, int lod) {
// Alle cy-Layers für diesen cx/cz laden
loadLayersForXZ(cx, cz, lod);
}
@Override
public void onChunkHidden(int cx, int cz) {
// Alle cy-Layers für diesen cx/cz entfernen
List<Long> toRemove = new ArrayList<>();
for (Map.Entry<Long, VoxelChunkNode> e : nodes.entrySet()) {
VoxelChunkNode n = e.getValue();
if (n.getChunk().cx == cx && n.getChunk().cz == cz) toRemove.add(e.getKey());
}
for (Long key : toRemove) removeNode(key);
}
@Override
public void onChunkLodChanged(int cx, int cz, int oldLod, int newLod) {
for (VoxelChunkNode n : nodes.values()) {
VoxelChunk c = n.getChunk();
if (c.cx != cx || c.cz != cz) continue;
n.setActiveLod(newLod);
// Physik nur bei LOD0 (nahes Terrain)
if (newLod == 0) {
n.updatePhysics(bulletState);
} else {
n.removePhysics();
}
}
}
// ── Intern ────────────────────────────────────────────────────────────────
private void loadLayersForXZ(int cx, int cz, int lod) {
// Suche alle vorhandenen .blvc-Dateien für diesen cx/cz
// Einfachste Lösung: bekannte cy-Range scannen (-8..+8)
for (int cy = -8; cy <= 8; cy++) {
if (!VoxelChunkIO.exists(cx, cy, cz)) continue;
long key = chunkKey(cx, cy, cz);
if (nodes.containsKey(key)) continue; // bereits geladen
try {
VoxelChunk chunk = VoxelChunkIO.load(cx, cy, cz);
addNode(key, chunk, lod);
} catch (IOException e) {
log.warn("Voxel-Chunk laden fehlgeschlagen ({},{},{}): {}", cx, cy, cz, e.getMessage());
}
}
}
private void addNode(long key, VoxelChunk chunk, int lod) {
VoxelChunkNode node = new VoxelChunkNode(chunk, voxelMaterial);
for (int l = 0; l < 3; l++) node.rebuildMesh(l);
node.setActiveLod(lod);
if (lod == 0) node.updatePhysics(bulletState);
voxelRoot.attachChild(node);
nodes.put(key, node);
}
private void removeNode(long key) {
VoxelChunkNode n = nodes.remove(key);
if (n == null) return;
n.removePhysics();
n.removeFromParent();
}
/** Fügt einen extern geladenen VoxelChunk zur Szene hinzu (z.B. aus dem Editor). */
public void addOrUpdateChunk(VoxelChunk chunk, int lod) {
long key = chunkKey(chunk.cx, chunk.cy, chunk.cz);
VoxelChunkNode existing = nodes.get(key);
if (existing != null) {
for (int l = 0; l < 3; l++) existing.rebuildMesh(l);
existing.setActiveLod(lod);
if (lod == 0) existing.updatePhysics(bulletState);
} else {
addNode(key, chunk, lod);
}
}
private Material buildMaterial() {
Material mat = new Material(assets, "MatDefs/Voxel.j3md");
mat.setFloat("TexScale", 4f);
// Texture2DArray aus 4 Terrain-Textur-Pfaden aufbauen
try {
List<Image> images = new ArrayList<>();
for (String path : texturePaths) {
Texture t = (path != null && !path.isEmpty())
? assets.loadTexture(path)
: assets.loadTexture("Common/Textures/MissingTexture.png");
images.add(t.getImage());
}
TextureArray texArray = new TextureArray(images);
texArray.setMinFilter(Texture.MinFilter.Trilinear);
texArray.setMagFilter(Texture.MagFilter.Bilinear);
mat.setParam("TexArray", com.jme3.shader.VarType.TextureArray, texArray);
} catch (Exception e) {
log.warn("Voxel Texture2DArray Aufbau fehlgeschlagen: {}", e.getMessage());
}
return mat;
}
public static long chunkKey(int cx, int cy, int cz) {
return ((long)(cx & 0xFFFF)) | (((long)(cy & 0xFFFF)) << 16) | (((long)(cz & 0xFFFF)) << 32);
}
}

View File

@@ -5,19 +5,28 @@ import com.jme3.app.SimpleApplication;
import com.jme3.app.state.BaseAppState;
import com.jme3.asset.AssetManager;
import com.jme3.asset.plugins.FileLocator;
import com.jme3.audio.AudioData;
import com.jme3.audio.AudioNode;
import com.jme3.bullet.control.CharacterControl;
import com.jme3.collision.CollisionResult;
import com.jme3.collision.CollisionResults;
import com.jme3.input.InputManager;
import com.jme3.input.MouseInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.input.controls.MouseButtonTrigger;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Ray;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.shape.Box;
import de.blight.common.PlacedItem;
import de.blight.common.PlacedItemIO;
import de.blight.common.SaveGame;
import de.blight.common.model.Inventar;
import de.blight.common.model.Item;
import de.blight.common.model.ItemIO;
@@ -35,17 +44,19 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Lädt alle auf der Karte platzierten Items, stellt sie als 3D-Objekte dar und
* verarbeitet das Aufheben (E-Taste) mit PICK_UP-Animation.
*/
public class WorldItemsState extends BaseAppState {
private static final Logger log = LoggerFactory.getLogger(WorldItemsState.class);
private static final float PICKUP_RANGE = 2.5f;
private static final float INTERACT_RANGE = 5f;
private static final float REACH_DIST = 0.5f;
private static final float PICKUP_ANIM_DURATION = 0.8f;
private static final String INTERACT_ACTION = "Interact";
private static final float WALK_TIMEOUT = 5.0f;
private static final String INTERACT_ACTION = "Interact";
private static final String SECONDARY_ATTACK_ACTION = "SecondaryAttack";
// ── Abhängigkeiten ────────────────────────────────────────────────────────
private final KeyBindings keyBindings;
private final CharacterControl physicsChar;
@@ -55,12 +66,32 @@ public class WorldItemsState extends BaseAppState {
private SimpleApplication app;
private AssetManager assets;
private InputManager inputManager;
private Camera cam;
private Node rootNode;
private Node itemsRoot;
private final List<PlacedItem> items = new ArrayList<>();
private final List<Spatial> visuals = new ArrayList<>();
private final Map<String, Item> itemDefs = new HashMap<>();
private final List<PlacedItem> items = new ArrayList<>();
private final List<Spatial> visuals = new ArrayList<>();
private final Map<String, Item> itemDefs = new HashMap<>();
// ── Hover-Zustand ─────────────────────────────────────────────────────────
private int hoveredIdx = -1;
private float hoverLogTimer = 0f;
// ── Pickup-Sequenz-Zustand ────────────────────────────────────────────────
private enum Phase { IDLE, WALKING, ANIMATING }
private Phase phase = Phase.IDLE;
private int targetIdx = -1;
private float walkTimer = 0f;
private float animTimer = 0f;
private boolean halfwayDone = false;
private AudioNode pickupSound = null;
// ── Konstruktor ───────────────────────────────────────────────────────────
public WorldItemsState(KeyBindings keyBindings, CharacterControl physicsChar,
MainCharacter mainCharacter, PlayerInputControl playerInput) {
@@ -74,11 +105,12 @@ public class WorldItemsState extends BaseAppState {
@Override
protected void initialize(Application app) {
this.app = (SimpleApplication) app;
this.assets = app.getAssetManager();
this.app = (SimpleApplication) app;
this.assets = app.getAssetManager();
this.inputManager = app.getInputManager();
this.rootNode = this.app.getRootNode();
this.itemsRoot = new Node("worldItemsRoot");
this.cam = app.getCamera();
this.rootNode = this.app.getRootNode();
this.itemsRoot = new Node("worldItemsRoot");
try {
assets.registerLocator(
@@ -91,6 +123,16 @@ public class WorldItemsState extends BaseAppState {
if (it.getItemId() != null) itemDefs.put(it.getItemId(), it);
}
log.info("[WorldItems] {} Item-Definitionen geladen.", itemDefs.size());
try {
pickupSound = new AudioNode(assets, "audio/static/pickup.ogg", AudioData.DataType.Buffer);
pickupSound.setPositional(false);
pickupSound.setLooping(false);
pickupSound.setVolume(1f);
} catch (Exception e) {
log.warn("[WorldItems] Pickup-Sound nicht ladbar: {}", e.getMessage());
}
}
@Override
@@ -103,6 +145,11 @@ public class WorldItemsState extends BaseAppState {
log.warn("[WorldItems] Laden fehlgeschlagen: {}", e.getMessage());
}
SaveGameState saveState = app.getStateManager().getState(SaveGameState.class);
if (saveState != null && !saveState.getSave().world.pickedUpItems.isEmpty()) {
items.removeIf(pi -> saveState.getSave().world.pickedUpItems.contains(pi.uuid()));
}
for (PlacedItem pi : items) {
Spatial s = buildVisual(pi);
s.setLocalTranslation(pi.x(), pi.y() + 0.25f, pi.z());
@@ -113,64 +160,302 @@ public class WorldItemsState extends BaseAppState {
rootNode.attachChild(itemsRoot);
log.info("[WorldItems] {} Item-Pickups geladen.", items.size());
inputManager.addMapping(INTERACT_ACTION, new KeyTrigger(keyBindings.interact));
if (saveState != null && mainCharacter != null) {
SaveGame.CharacterSave cs = saveState.getSave().character;
if (!cs.inventory.isEmpty()) {
Inventar inv = mainCharacter.getInventar();
if (inv == null) { inv = new Inventar(); mainCharacter.setInventar(inv); }
for (Map.Entry<String, Integer> e : cs.inventory.entrySet()) {
Item def = itemDefs.get(e.getKey());
if (def != null) {
for (int i = 0; i < e.getValue(); i++) inv.collect(def);
}
}
log.info("[WorldItems] Inventar wiederhergestellt ({} Einträge).", cs.inventory.size());
}
}
inputManager.addMapping(INTERACT_ACTION,
new KeyTrigger(keyBindings.interact));
inputManager.addMapping(SECONDARY_ATTACK_ACTION,
new MouseButtonTrigger(MouseInput.BUTTON_RIGHT));
inputManager.addListener(interactListener, INTERACT_ACTION);
inputManager.addListener(secondaryAttackListener, SECONDARY_ATTACK_ACTION);
}
@Override
protected void onDisable() {
cancelApproach();
hoveredIdx = -1;
itemsRoot.detachAllChildren();
itemsRoot.removeFromParent();
visuals.clear();
items.clear();
inputManager.removeListener(interactListener);
try { inputManager.deleteMapping(INTERACT_ACTION); } catch (Exception ignored) {}
inputManager.removeListener(secondaryAttackListener);
try { inputManager.deleteMapping(INTERACT_ACTION); } catch (Exception ignored) {}
try { inputManager.deleteMapping(SECONDARY_ATTACK_ACTION); } catch (Exception ignored) {}
}
@Override
protected void cleanup(Application app) {}
// ── Update ────────────────────────────────────────────────────────────────
@Override
public void update(float tpf) {}
public void update(float tpf) {
hoverLogTimer += tpf;
updateHover();
switch (phase) {
case WALKING -> updateWalking(tpf);
case ANIMATING -> updateAnimating(tpf);
default -> {}
}
}
// ── Interaktion ───────────────────────────────────────────────────────────
// ── Hover / Zielen ────────────────────────────────────────────────────────
private final ActionListener interactListener = (name, isPressed, tpf) -> {
if (isPressed) tryPickup();
};
private void updateHover() {
if (phase != Phase.IDLE) {
setHovered(-1);
return;
}
private void tryPickup() {
if (physicsChar == null || items.isEmpty()) return;
Vector3f playerPos = physicsChar.getPhysicsLocation();
float centerX = cam.getWidth() * 0.5f;
float hMargin = cam.getWidth() * 0.10f;
float screenH = cam.getHeight();
int nearest = -1;
boolean doLog = hoverLogTimer >= 1f;
if (doLog) {
hoverLogTimer = 0f;
log.info("[Hover] items={} screenW={} screenH={} centerX={} hMargin={}",
items.size(), cam.getWidth(), cam.getHeight(), centerX, hMargin);
}
int bestIdx = -1;
float bestDist = Float.MAX_VALUE;
Vector3f charPos = physicsChar != null ? physicsChar.getPhysicsLocation() : cam.getLocation();
for (int i = 0; i < items.size(); i++) {
PlacedItem pi = items.get(i);
float dx = pi.x() - playerPos.x;
float dz = pi.z() - playerPos.z;
float dist = (float) Math.sqrt(dx * dx + dz * dz);
if (dist < PICKUP_RANGE && dist < bestDist) {
bestDist = dist;
nearest = i;
PlacedItem pi = items.get(i);
Vector3f worldPos = new Vector3f(pi.x(), pi.y() + 0.25f, pi.z());
// Distanz vom Charakter (nicht von der Kamera)
float charDist = worldPos.distance(charPos);
if (doLog) log.info("[Hover] item[{}] id={} charDist={}", i, pi.itemId(), charDist);
if (charDist > INTERACT_RANGE) {
if (doLog) log.info("[Hover] → skip: außerhalb Reichweite ({} > {})", charDist, INTERACT_RANGE);
continue;
}
// Muss vor der Kamera liegen
Vector3f camToItem = worldPos.subtract(cam.getLocation());
if (camToItem.dot(cam.getDirection()) <= 0f) {
if (doLog) log.info("[Hover] → skip: hinter der Kamera");
continue;
}
// Bildschirmprojektion (von der Kamera aus)
Vector3f screen = cam.getScreenCoordinates(worldPos);
float hDiff = Math.abs(screen.x - centerX);
if (doLog) log.info("[Hover] screen=({},{},{}) hDiff={} hMargin={}",
(int)screen.x, (int)screen.y, screen.z, (int)hDiff, (int)hMargin);
if (hDiff > hMargin) {
if (doLog) log.info("[Hover] → skip: horizontal außerhalb Mitte");
continue;
}
if (screen.y < 0f || screen.y > screenH) {
if (doLog) log.info("[Hover] → skip: vertikal außerhalb Bildschirm");
continue;
}
// Verdeckungsprüfung: Strahl von Kamera zum Item
float camDist = camToItem.length();
if (isOccluded(camToItem.normalize(), camDist, i, doLog)) {
if (doLog) log.info("[Hover] → skip: verdeckt");
continue;
}
if (charDist < bestDist) {
bestDist = charDist;
bestIdx = i;
}
}
if (nearest < 0) return;
PlacedItem picked = items.get(nearest);
Spatial pickedSpatial = visuals.get(nearest);
pickedSpatial.removeFromParent();
items.remove(nearest);
visuals.remove(nearest);
if (doLog) log.info("[Hover] → hoveredIdx={}", bestIdx);
setHovered(bestIdx);
}
private boolean isOccluded(Vector3f direction, float itemDist, int itemIdx, boolean doLog) {
Ray ray = new Ray(cam.getLocation(), direction);
CollisionResults results = new CollisionResults();
rootNode.collideWith(ray, results);
if (doLog) log.info("[Hover] occlusion: {} Treffer, itemDist={:.2f}", results.size(), itemDist);
for (int j = 0; j < results.size(); j++) {
CollisionResult cr = results.getCollision(j);
float d = cr.getDistance();
if (d < 1.5f) {
if (doLog) log.info("[Hover] [{}] d={:.2f} → Nah-Clip skip ({})", j, d, cr.getGeometry().getName());
continue;
}
if (d > itemDist - 0.15f) {
if (doLog) log.info("[Hover] [{}] d={:.2f} → am/hinter Item, kein Blocker ({})", j, d, cr.getGeometry().getName());
continue;
}
int vidx = findVisualIndex(cr.getGeometry());
if (vidx == itemIdx) {
if (doLog) log.info("[Hover] [{}] d={:.2f} → Item selbst ({})", j, d, cr.getGeometry().getName());
continue;
}
if (doLog) log.info("[Hover] [{}] d={:.2f} → BLOCKIERT durch '{}'", j, d, cr.getGeometry().getName());
return true;
}
return false;
}
private int findVisualIndex(Geometry g) {
Spatial s = g;
while (s != null) {
int idx = visuals.indexOf(s);
if (idx >= 0) return idx;
if (s == itemsRoot) return -1;
s = s.getParent();
}
return -1;
}
private void setHovered(int idx) {
hoveredIdx = idx;
}
public int getHoveredIdx() { return hoveredIdx; }
public String getHoveredItemName() {
if (hoveredIdx < 0 || hoveredIdx >= items.size()) return null;
PlacedItem pi = items.get(hoveredIdx);
Item def = itemDefs.get(pi.itemId());
return def != null ? def.getDisplayText() : pi.itemId();
}
// ── Pickup-Sequenz ────────────────────────────────────────────────────────
private void updateWalking(float tpf) {
if (playerInput.isPaused()) {
cancelApproach();
return;
}
walkTimer += tpf;
if (walkTimer > WALK_TIMEOUT) {
log.info("[WorldItems] Pickup-Annäherung abgebrochen (Timeout).");
cancelApproach();
return;
}
if (targetIdx < 0 || targetIdx >= items.size()) {
cancelApproach();
return;
}
Vector3f playerPos = physicsChar.getPhysicsLocation();
PlacedItem target = items.get(targetIdx);
float dx = target.x() - playerPos.x;
float dz = target.z() - playerPos.z;
float distSq = dx * dx + dz * dz;
if (distSq <= REACH_DIST * REACH_DIST) {
startPickupAnim();
} else {
float dist = (float) Math.sqrt(distSq);
playerInput.setAutopilotDirection(new Vector3f(dx / dist, 0f, dz / dist));
}
}
private void updateAnimating(float tpf) {
animTimer += tpf;
if (!halfwayDone && animTimer >= PICKUP_ANIM_DURATION * 0.5f) {
halfwayDone = true;
executePickup();
}
if (animTimer >= PICKUP_ANIM_DURATION) {
phase = Phase.IDLE;
}
}
// ── Listener ──────────────────────────────────────────────────────────────
private final ActionListener interactListener = (name, isPressed, tpf) -> {
if (isPressed) {
log.info("[WorldItems] Interact gedrückt: phase={} hoveredIdx={} items={}",
phase, hoveredIdx, items.size());
if (phase == Phase.IDLE) tryPickup();
}
};
private final ActionListener secondaryAttackListener = (name, isPressed, tpf) -> {
if (isPressed && phase == Phase.WALKING) {
log.info("[WorldItems] Pickup-Annäherung abgebrochen (Sekundärangriff).");
cancelApproach();
}
};
// ── Pickup-Logik ──────────────────────────────────────────────────────────
private void tryPickup() {
log.info("[WorldItems] tryPickup: hoveredIdx={} physicsChar={}", hoveredIdx, physicsChar != null ? "ok" : "NULL");
if (hoveredIdx < 0 || physicsChar == null) return;
targetIdx = hoveredIdx;
PlacedItem pi = items.get(targetIdx);
Vector3f playerPos = physicsChar.getPhysicsLocation();
float dx = pi.x() - playerPos.x;
float dz = pi.z() - playerPos.z;
float distSq = dx * dx + dz * dz;
log.info("[WorldItems] → Ziel='{}' playerDist={}", pi.itemId(), Math.sqrt(distSq));
if (distSq <= REACH_DIST * REACH_DIST) {
startPickupAnim();
} else {
walkTimer = 0f;
phase = Phase.WALKING;
log.info("[WorldItems] → WALKING gestartet");
}
}
private void startPickupAnim() {
playerInput.setAutopilotDirection(null);
playerInput.requestPickup(PICKUP_ANIM_DURATION);
if (pickupSound != null) pickupSound.playInstance();
animTimer = 0f;
halfwayDone = false;
phase = Phase.ANIMATING;
}
private void executePickup() {
if (targetIdx < 0 || targetIdx >= items.size()) {
phase = Phase.IDLE;
return;
}
PlacedItem picked = items.get(targetIdx);
Spatial pickedVisual = visuals.get(targetIdx);
pickedVisual.removeFromParent();
items.remove(targetIdx);
visuals.remove(targetIdx);
targetIdx = -1;
// Kartendatei sofort aktualisieren
try {
PlacedItemIO.save(items);
} catch (IOException e) {
log.warn("[WorldItems] Speichern fehlgeschlagen: {}", e.getMessage());
}
// Ins Inventar legen
Item def = itemDefs.get(picked.itemId());
if (mainCharacter != null) {
if (mainCharacter.getInventar() == null) {
@@ -184,15 +469,22 @@ public class WorldItemsState extends BaseAppState {
}
}
// PICK_UP-Animation spielen und Bewegung sperren
if (playerInput != null) {
playerInput.requestPickup(PICKUP_ANIM_DURATION);
SaveGameState saveState = getApplication().getStateManager().getState(SaveGameState.class);
if (saveState != null) {
saveState.reportItemPickedUp(picked.uuid());
}
}
private void cancelApproach() {
if (phase == Phase.IDLE) return;
playerInput.setAutopilotDirection(null);
phase = Phase.IDLE;
targetIdx = -1;
}
// ── Zugriff ───────────────────────────────────────────────────────────────
public Node getItemsRoot() { return itemsRoot; }
public Node getItemsRoot() { return itemsRoot; }
public CharacterControl getPhysicsChar() { return physicsChar; }
// ── Visuelles ─────────────────────────────────────────────────────────────
@@ -210,7 +502,6 @@ public class WorldItemsState extends BaseAppState {
log.warn("[WorldItems] Modell für '{}' nicht ladbar: {}", pi.itemId(), e.getMessage());
}
}
// Fallback: goldener Würfel
Geometry g = new Geometry("item_" + pi.itemId(), new Box(0.15f, 0.15f, 0.15f));
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", new ColorRGBA(1f, 0.82f, 0.1f, 1f));