Commit vor Voxel Update für die Klippen
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 + ")");
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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 (0–11) 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 };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user