Initaler Commit

This commit is contained in:
2026-05-07 11:54:11 +02:00
commit b8a0234ad2
158 changed files with 15138 additions and 0 deletions

View File

@@ -0,0 +1,92 @@
package de.blight.game;
import com.jme3.app.SimpleApplication;
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.game.config.*;
import de.blight.game.scene.WorldScene;
public class BlightApp extends SimpleApplication {
private KeyBindings keyBindings;
private GraphicsSettings graphicsSettings;
private WorldScene worldScene;
private ConfigScreen configScreen;
private GraphicsScreen graphicsScreen;
private PauseMenu pauseMenu;
public static void main(String[] args) {
BlightApp app = new BlightApp();
GraphicsSettings gs = GraphicsStore.load();
AppSettings settings = new AppSettings(true);
settings.setTitle("Blight");
settings.setResolution(gs.width, gs.height);
settings.setFullscreen(gs.fullscreen);
settings.setVSync(gs.vsync);
settings.setSamples(gs.samples);
app.setSettings(settings);
app.setShowSettings(false);
app.start();
}
@Override
public void simpleInitApp() {
flyCam.setEnabled(false);
inputManager.deleteMapping(INPUT_MAPPING_EXIT);
keyBindings = KeyBindingStore.load();
graphicsSettings = GraphicsStore.load();
worldScene = new WorldScene(keyBindings);
stateManager.attach(worldScene);
configScreen = new ConfigScreen(keyBindings, () -> worldScene.reloadBindings(keyBindings));
configScreen.setOnClose(() -> pauseMenu.setEnabled(true));
stateManager.attach(configScreen);
configScreen.setEnabled(false);
graphicsScreen = new GraphicsScreen(graphicsSettings, () -> pauseMenu.setEnabled(true));
stateManager.attach(graphicsScreen);
graphicsScreen.setEnabled(false);
pauseMenu = new PauseMenu(
() -> { pauseMenu.setEnabled(false); graphicsScreen.setEnabled(true); },
() -> { pauseMenu.setEnabled(false); configScreen.setEnabled(true); }
);
stateManager.attach(pauseMenu);
pauseMenu.setEnabled(false);
inputManager.addMapping("ToggleMenu", new KeyTrigger(KeyInput.KEY_ESCAPE));
inputManager.addListener((ActionListener) (name, isPressed, tpf) -> {
if (!isPressed) return;
if (graphicsScreen.isEnabled()) {
// GraphicsScreen wird nur über seine eigenen Buttons geschlossen
return;
}
if (configScreen.isEnabled()) {
if (configScreen.isWaiting()) {
configScreen.cancelWaiting();
} else {
configScreen.setEnabled(false);
pauseMenu.setEnabled(true);
}
return;
}
if (pauseMenu.isEnabled()) {
pauseMenu.setEnabled(false);
worldScene.setPaused(false);
return;
}
pauseMenu.setEnabled(true);
worldScene.setPaused(true);
}, "ToggleMenu");
}
@Override
public void simpleUpdate(float tpf) {}
}

View File

@@ -0,0 +1,309 @@
package de.blight.game.config;
import java.util.ArrayList;
import java.util.List;
import com.jme3.app.Application;
import com.jme3.app.SimpleApplication;
import com.jme3.app.state.BaseAppState;
import com.jme3.font.BitmapFont;
import com.jme3.font.BitmapText;
import com.jme3.input.KeyInput;
import com.jme3.input.MouseInput;
import com.jme3.input.RawInputListener;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.MouseButtonTrigger;
import com.jme3.input.event.JoyAxisEvent;
import com.jme3.input.event.JoyButtonEvent;
import com.jme3.input.event.KeyInputEvent;
import com.jme3.input.event.MouseButtonEvent;
import com.jme3.input.event.MouseMotionEvent;
import com.jme3.input.event.TouchEvent;
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;
/**
* Overlay-AppState der die Tastenbelegungs-Maske anzeigt.
*
* ESC → Schließen (ohne Speichern)
* Klick auf Row → wartet auf neue Taste
* ESC während Warten → bricht nur die Zuweisung ab
* Speichern → schreibt JSON, ruft onSave-Callback
*/
public class ConfigScreen extends BaseAppState implements RawInputListener {
// Farben
private static final ColorRGBA COL_BG = new ColorRGBA(0.05f, 0.05f, 0.08f, 0.88f);
private static final ColorRGBA COL_PANEL = new ColorRGBA(0.10f, 0.10f, 0.16f, 1.00f);
private static final ColorRGBA COL_ROW = new ColorRGBA(0.18f, 0.18f, 0.28f, 1.00f);
private static final ColorRGBA COL_ROW_HOVER = new ColorRGBA(0.25f, 0.25f, 0.40f, 1.00f);
private static final ColorRGBA COL_ROW_WAIT = new ColorRGBA(0.50f, 0.30f, 0.10f, 1.00f);
private static final ColorRGBA COL_BTN_SAVE = new ColorRGBA(0.15f, 0.40f, 0.15f, 1.00f);
private static final ColorRGBA COL_BTN_CANCEL = new ColorRGBA(0.40f, 0.15f, 0.15f, 1.00f);
private static final ColorRGBA COL_TEXT = ColorRGBA.White;
private static final ColorRGBA COL_TEXT_KEY = new ColorRGBA(0.85f, 0.85f, 0.50f, 1.00f);
// -----------------------------------------------------------------------
private SimpleApplication app;
private Node guiNode;
private BitmapFont font;
private KeyBindings liveBindings; // geteilt mit der ganzen App
private KeyBindings editCopy; // wird beim Öffnen geklont
private Runnable onSave; // Callback → PlayerInputControl.reloadBindings
private Runnable onClose; // Callback → PauseMenu wiederherstellen
private Node panel;
private List<Row> rows = new ArrayList<>();
private int waitingRow = -1; // -1 = keine Zuweisung aktiv
// UI-Elemente für Buttons (Bounds in Screen-Koordinaten)
private float saveBtnX, saveBtnY, saveBtnW, saveBtnH;
private float cancelBtnX, cancelBtnY;
// -----------------------------------------------------------------------
private static class Row {
String field;
String label;
BitmapText keyText;
Geometry bg;
float x, y, w, h; // Button-Bounds
}
// -----------------------------------------------------------------------
public ConfigScreen(KeyBindings liveBindings, Runnable onSave) {
this.liveBindings = liveBindings;
this.onSave = onSave;
}
public boolean isWaiting() { return waitingRow >= 0; }
public void setOnClose(Runnable onClose) { this.onClose = onClose; }
public void cancelWaiting() {
if (waitingRow >= 0) { resetRowColor(waitingRow); waitingRow = -1; }
}
// -----------------------------------------------------------------------
// Lifecycle
// -----------------------------------------------------------------------
@Override
protected void initialize(Application app) {
this.app = (SimpleApplication) app;
this.guiNode = this.app.getGuiNode();
this.font = app.getAssetManager().loadFont("Interface/Fonts/Default.fnt");
}
@Override
protected void onEnable() {
editCopy = liveBindings.copy();
waitingRow = -1;
buildUI();
app.getInputManager().setCursorVisible(true);
app.getInputManager().addRawInputListener(this);
app.getInputManager().addMapping("_CfgClick", new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
app.getInputManager().addListener(clickListener, "_CfgClick");
}
@Override
protected void onDisable() {
if (panel != null) { guiNode.detachChild(panel); panel = null; }
rows.clear();
waitingRow = -1;
app.getInputManager().removeRawInputListener(this);
app.getInputManager().deleteMapping("_CfgClick");
app.getInputManager().setCursorVisible(false);
}
@Override protected void cleanup(Application app) {}
// -----------------------------------------------------------------------
// UI aufbauen
// -----------------------------------------------------------------------
private void buildUI() {
float sw = app.getCamera().getWidth();
float sh = app.getCamera().getHeight();
panel = new Node("cfg-panel");
// Halbdurchsichtiger Overlay über dem Spiel
addQuad(panel, 0, 0, sw, sh, COL_BG, -2);
float pw = 720, ph = 440;
float px = (sw - pw) / 2f, py = (sh - ph) / 2f;
addQuad(panel, px, py, pw, ph, COL_PANEL, -1);
// Titel
BitmapText title = text("TASTENBELEGUNG", 20, COL_TEXT);
centerText(title, px, py + ph - 40, pw);
panel.attachChild(title);
BitmapText hint = text("Klicke eine Taste um sie neu zu belegen", 14, new ColorRGBA(0.7f, 0.7f, 0.7f, 1f));
centerText(hint, px, py + ph - 70, pw);
panel.attachChild(hint);
// Reihen
float rowX = px + 30;
float keyX = px + pw - 220;
float rowW = 180;
float rowH = 36;
float startY = py + ph - 110;
float stepY = 48;
for (int i = 0; i < KeyBindings.ENTRIES.length; i++) {
String[] entry = KeyBindings.ENTRIES[i];
float ry = startY - i * stepY;
BitmapText lbl = text(entry[1], 16, COL_TEXT);
lbl.setLocalTranslation(rowX, ry + rowH - 8, 0);
panel.attachChild(lbl);
Geometry bg = addQuad(panel, keyX, ry, rowW, rowH, COL_ROW, 0);
BitmapText kt = text(KeyNames.of(editCopy.get(entry[0])), 16, COL_TEXT_KEY);
kt.setLocalTranslation(keyX + 10, ry + rowH - 8, 1);
panel.attachChild(kt);
Row row = new Row();
row.field = entry[0];
row.label = entry[1];
row.keyText = kt;
row.bg = bg;
row.x = keyX; row.y = ry; row.w = rowW; row.h = rowH;
rows.add(row);
}
// Buttons
float btnW = 160, btnH = 42;
float btnY = py + 25;
saveBtnX = px + pw / 2f - btnW - 15;
saveBtnY = btnY;
saveBtnW = btnW;
saveBtnH = btnH;
cancelBtnX = px + pw / 2f + 15;
cancelBtnY = btnY;
addQuad(panel, saveBtnX, saveBtnY, btnW, btnH, COL_BTN_SAVE, 0);
BitmapText saveLabel = text("Speichern", 16, COL_TEXT);
centerText(saveLabel, saveBtnX, saveBtnY + btnH - 10, btnW);
panel.attachChild(saveLabel);
addQuad(panel, cancelBtnX, cancelBtnY, btnW, btnH, COL_BTN_CANCEL, 0);
BitmapText cancelLabel = text("Abbrechen", 16, COL_TEXT);
centerText(cancelLabel, cancelBtnX, cancelBtnY + btnH - 10, btnW);
panel.attachChild(cancelLabel);
guiNode.attachChild(panel);
}
// -----------------------------------------------------------------------
// Mausklick
// -----------------------------------------------------------------------
private final ActionListener clickListener = (name, isPressed, tpf) -> {
if (!isPressed) return;
Vector2f cursor = app.getInputManager().getCursorPosition();
// Reihen prüfen
for (int i = 0; i < rows.size(); i++) {
Row r = rows.get(i);
if (hits(cursor, r.x, r.y, r.w, r.h)) {
waitingRow = i;
r.bg.getMaterial().setColor("Color", COL_ROW_WAIT);
r.keyText.setText("...");
return;
}
}
// Speichern
if (hits(cursor, saveBtnX, saveBtnY, saveBtnW, saveBtnH)) {
liveBindings.copyFrom(editCopy);
KeyBindingStore.save(liveBindings);
if (onSave != null) onSave.run();
setEnabled(false);
if (onClose != null) onClose.run();
return;
}
// Abbrechen
if (hits(cursor, cancelBtnX, cancelBtnY, saveBtnW, saveBtnH)) {
setEnabled(false);
if (onClose != null) onClose.run();
}
};
// -----------------------------------------------------------------------
// Tastendruck beim Warten auf Zuweisung (RawInputListener)
// -----------------------------------------------------------------------
@Override
public void onKeyEvent(KeyInputEvent evt) {
if (!evt.isPressed() || waitingRow < 0) return;
if (evt.getKeyCode() == KeyInput.KEY_ESCAPE) return; // cancelWaiting() wird von BlightApp aufgerufen
Row r = rows.get(waitingRow);
editCopy.set(r.field, evt.getKeyCode());
r.keyText.setText(KeyNames.of(evt.getKeyCode()));
resetRowColor(waitingRow);
waitingRow = -1;
}
private void resetRowColor(int idx) {
rows.get(idx).bg.getMaterial().setColor("Color", COL_ROW);
}
// RawInputListener-Pflichtmethoden
@Override public void beginInput() {}
@Override public void endInput() {}
@Override public void onMouseMotionEvent(MouseMotionEvent evt) {}
@Override public void onMouseButtonEvent(MouseButtonEvent evt) {}
@Override public void onJoyAxisEvent(JoyAxisEvent evt) {}
@Override public void onJoyButtonEvent(JoyButtonEvent evt) {}
@Override public void onTouchEvent(TouchEvent evt) {}
// -----------------------------------------------------------------------
// Hilfsmethoden
// -----------------------------------------------------------------------
private Geometry addQuad(Node parent, float x, float y, float w, float h, ColorRGBA color, float z) {
Geometry geo = new Geometry("q", new Quad(w, h));
Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", color.clone());
if (color.a < 1f) {
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
geo.setQueueBucket(RenderQueue.Bucket.Transparent);
}
geo.setMaterial(mat);
geo.setLocalTranslation(x, y, z);
parent.attachChild(geo);
return geo;
}
private BitmapText text(String content, int size, ColorRGBA color) {
BitmapText t = new BitmapText(font, false);
t.setSize(size);
t.setColor(color);
t.setText(content);
return t;
}
private void centerText(BitmapText t, float x, float y, float width) {
t.setLocalTranslation(x + (width - t.getLineWidth()) / 2f, y, 1);
}
private boolean hits(Vector2f p, float x, float y, float w, float h) {
return p.x >= x && p.x <= x + w && p.y >= y && p.y <= y + h;
}
}

View File

@@ -0,0 +1,290 @@
package de.blight.game.config;
import com.jme3.app.Application;
import com.jme3.app.SimpleApplication;
import com.jme3.app.state.BaseAppState;
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.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.system.AppSettings;
public class GraphicsScreen extends BaseAppState {
private static final ColorRGBA COL_BG = new ColorRGBA(0.05f, 0.05f, 0.08f, 0.88f);
private static final ColorRGBA COL_PANEL = new ColorRGBA(0.10f, 0.10f, 0.16f, 1.00f);
private static final ColorRGBA COL_ROW = new ColorRGBA(0.18f, 0.18f, 0.28f, 1.00f);
private static final ColorRGBA COL_ARROW = new ColorRGBA(0.28f, 0.28f, 0.44f, 1.00f);
private static final ColorRGBA COL_BTN_OK = new ColorRGBA(0.15f, 0.40f, 0.15f, 1.00f);
private static final ColorRGBA COL_BTN_CANCEL = new ColorRGBA(0.40f, 0.15f, 0.15f, 1.00f);
private static final ColorRGBA COL_TEXT = ColorRGBA.White;
private static final ColorRGBA COL_TEXT_VAL = new ColorRGBA(0.85f, 0.85f, 0.50f, 1.00f);
private static final int[][] RESOLUTIONS = {
{1280, 720}, {1600, 900}, {1920, 1080}, {2560, 1440}, {3840, 2160}
};
private static final int[] SAMPLES = {0, 2, 4, 8};
private static final int ROW_RES = 0;
private static final int ROW_FULL = 1;
private static final int ROW_VSYNC = 2;
private static final int ROW_AA = 3;
private SimpleApplication app;
private Node guiNode;
private BitmapFont font;
private Node panel;
private final GraphicsSettings live;
private GraphicsSettings edit;
private final Runnable onClose;
private int resIdx;
private int samplesIdx;
// Per-row layout (indexed by ROW_*)
private final float[] cellX = new float[4];
private final float[] cellY = new float[4];
private final float[] cellW = new float[4];
private final float cellH = 36;
private final float arrW = 30;
private final float[] leftX = new float[4];
private final float[] rightX = new float[4];
private final BitmapText[] valTexts = new BitmapText[4];
private float okX, okY, okW, okH;
private float cancelX, cancelY;
public GraphicsScreen(GraphicsSettings live, Runnable onClose) {
this.live = live;
this.onClose = onClose;
}
@Override
protected void initialize(Application app) {
this.app = (SimpleApplication) app;
this.guiNode = this.app.getGuiNode();
this.font = app.getAssetManager().loadFont("Interface/Fonts/Default.fnt");
}
@Override
protected void onEnable() {
edit = new GraphicsSettings();
edit.width = live.width; edit.height = live.height;
edit.fullscreen = live.fullscreen;
edit.vsync = live.vsync;
edit.samples = live.samples;
resIdx = 0;
for (int i = 0; i < RESOLUTIONS.length; i++) {
if (RESOLUTIONS[i][0] == edit.width && RESOLUTIONS[i][1] == edit.height) {
resIdx = i; break;
}
}
samplesIdx = 0;
for (int i = 0; i < SAMPLES.length; i++) {
if (SAMPLES[i] == edit.samples) { samplesIdx = i; break; }
}
buildUI();
app.getInputManager().setCursorVisible(true);
app.getInputManager().addMapping("_GfxClick", new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
app.getInputManager().addListener(clickListener, "_GfxClick");
}
@Override
protected void onDisable() {
if (panel != null) { guiNode.detachChild(panel); panel = null; }
app.getInputManager().deleteMapping("_GfxClick");
app.getInputManager().setCursorVisible(false);
}
@Override protected void cleanup(Application app) {}
private void buildUI() {
float sw = app.getCamera().getWidth();
float sh = app.getCamera().getHeight();
panel = new Node("gfx-panel");
addQuad(panel, 0, 0, sw, sh, COL_BG, -2);
float pw = 640, ph = 400;
float px = (sw - pw) / 2f, py = (sh - ph) / 2f;
addQuad(panel, px, py, pw, ph, COL_PANEL, -1);
BitmapText title = txt("GRAFIKEINSTELLUNGEN", 20, COL_TEXT);
centerText(title, px, py + ph - 42, pw);
panel.attachChild(title);
String[] labels = {"Auflösung", "Vollbild", "VSync", "Kantenglättung"};
float lblX = px + 30;
float vx = px + pw - 270;
float vw = 190;
float startY = py + ph - 100;
float step = 60;
for (int i = 0; i < 4; i++) {
float ry = startY - i * step;
BitmapText lbl = txt(labels[i], 16, COL_TEXT);
lbl.setLocalTranslation(lblX, ry + cellH - 8, 0);
panel.attachChild(lbl);
// Left arrow
addQuad(panel, vx - arrW - 6, ry, arrW, cellH, COL_ARROW, 0);
BitmapText lt = txt("<", 16, COL_TEXT);
lt.setLocalTranslation(vx - arrW - 6 + (arrW - lt.getLineWidth()) / 2f, ry + cellH - 8, 1);
panel.attachChild(lt);
// Value cell
addQuad(panel, vx, ry, vw, cellH, COL_ROW, 0);
// Right arrow
addQuad(panel, vx + vw + 6, ry, arrW, cellH, COL_ARROW, 0);
BitmapText rt = txt(">", 16, COL_TEXT);
rt.setLocalTranslation(vx + vw + 6 + (arrW - rt.getLineWidth()) / 2f, ry + cellH - 8, 1);
panel.attachChild(rt);
BitmapText vt = txt("", 16, COL_TEXT_VAL);
panel.attachChild(vt);
valTexts[i] = vt;
cellX[i] = vx; cellY[i] = ry; cellW[i] = vw;
leftX[i] = vx - arrW - 6;
rightX[i] = vx + vw + 6;
}
for (int i = 0; i < 4; i++) refreshText(i);
float bw = 160, bh = 42;
okW = bw; okH = bh;
okX = px + pw / 2f - bw - 10;
okY = py + 22;
cancelX = px + pw / 2f + 10;
cancelY = py + 22;
addQuad(panel, okX, okY, bw, bh, COL_BTN_OK, 0);
BitmapText okLbl = txt("Übernehmen", 16, COL_TEXT);
centerText(okLbl, okX, okY + bh - 10, bw);
panel.attachChild(okLbl);
addQuad(panel, cancelX, cancelY, bw, bh, COL_BTN_CANCEL, 0);
BitmapText cancelLbl = txt("Abbrechen", 16, COL_TEXT);
centerText(cancelLbl, cancelX, cancelY + bh - 10, bw);
panel.attachChild(cancelLbl);
guiNode.attachChild(panel);
}
private void refreshText(int row) {
String val = switch (row) {
case ROW_RES -> RESOLUTIONS[resIdx][0] + "x" + RESOLUTIONS[resIdx][1];
case ROW_FULL -> edit.fullscreen ? "An" : "Aus";
case ROW_VSYNC -> edit.vsync ? "An" : "Aus";
case ROW_AA -> SAMPLES[samplesIdx] == 0 ? "Aus" : SAMPLES[samplesIdx] + "x MSAA";
default -> "";
};
BitmapText vt = valTexts[row];
vt.setText(val);
vt.setLocalTranslation(
cellX[row] + (cellW[row] - vt.getLineWidth()) / 2f,
cellY[row] + cellH - 8,
1
);
}
private final ActionListener clickListener = (name, isPressed, tpf) -> {
if (!isPressed) return;
Vector2f c = app.getInputManager().getCursorPosition();
for (int i = 0; i < 4; i++) {
if (hits(c, leftX[i], cellY[i], arrW, cellH)) { cycleRow(i, -1); return; }
if (hits(c, rightX[i], cellY[i], arrW, cellH)) { cycleRow(i, +1); return; }
}
if (hits(c, okX, okY, okW, okH)) { applyAndSave(); return; }
if (hits(c, cancelX, cancelY, okW, okH)) { close(); }
};
private void cycleRow(int row, int dir) {
switch (row) {
case ROW_RES:
resIdx = (resIdx + dir + RESOLUTIONS.length) % RESOLUTIONS.length;
edit.width = RESOLUTIONS[resIdx][0];
edit.height = RESOLUTIONS[resIdx][1];
break;
case ROW_FULL:
edit.fullscreen = !edit.fullscreen;
break;
case ROW_VSYNC:
edit.vsync = !edit.vsync;
break;
case ROW_AA:
samplesIdx = (samplesIdx + dir + SAMPLES.length) % SAMPLES.length;
edit.samples = SAMPLES[samplesIdx];
break;
}
refreshText(row);
}
private void applyAndSave() {
live.width = edit.width; live.height = edit.height;
live.fullscreen = edit.fullscreen;
live.vsync = edit.vsync;
live.samples = edit.samples;
GraphicsStore.save(live);
AppSettings s = app.getContext().getSettings();
s.setResolution(live.width, live.height);
s.setFullscreen(live.fullscreen);
s.setVSync(live.vsync);
s.setSamples(live.samples);
app.setSettings(s);
close();
app.restart();
}
private void close() {
setEnabled(false);
if (onClose != null) onClose.run();
}
// -----------------------------------------------------------------------
private Geometry addQuad(Node parent, float x, float y, float w, float h, ColorRGBA color, float z) {
Geometry geo = new Geometry("q", new Quad(w, h));
Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", color.clone());
if (color.a < 1f) {
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
geo.setQueueBucket(RenderQueue.Bucket.Transparent);
}
geo.setMaterial(mat);
geo.setLocalTranslation(x, y, z);
parent.attachChild(geo);
return geo;
}
private BitmapText txt(String s, int size, ColorRGBA color) {
BitmapText t = new BitmapText(font, false);
t.setSize(size); t.setColor(color); t.setText(s);
return t;
}
private void centerText(BitmapText t, float x, float y, float width) {
t.setLocalTranslation(x + (width - t.getLineWidth()) / 2f, y, 1);
}
private boolean hits(Vector2f p, float x, float y, float w, float h) {
return p.x >= x && p.x <= x + w && p.y >= y && p.y <= y + h;
}
}

View File

@@ -0,0 +1,9 @@
package de.blight.game.config;
public class GraphicsSettings {
public int width = 1280;
public int height = 720;
public boolean fullscreen = false;
public boolean vsync = false;
public int samples = 4;
}

View File

@@ -0,0 +1,35 @@
package de.blight.game.config;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.io.*;
import java.nio.file.*;
public class GraphicsStore {
private static final Path FILE = Paths.get("config", "graphics.json");
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
public static GraphicsSettings load() {
if (Files.exists(FILE)) {
try (Reader r = Files.newBufferedReader(FILE)) {
return GSON.fromJson(r, GraphicsSettings.class);
} catch (IOException e) {
System.err.println("graphics.json konnte nicht geladen werden: " + e.getMessage());
}
}
return new GraphicsSettings();
}
public static void save(GraphicsSettings gs) {
try {
Files.createDirectories(FILE.getParent());
try (Writer w = Files.newBufferedWriter(FILE)) {
GSON.toJson(gs, w);
}
} catch (IOException e) {
System.err.println("graphics.json konnte nicht gespeichert werden: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,35 @@
package de.blight.game.config;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.io.*;
import java.nio.file.*;
public class KeyBindingStore {
private static final Path FILE = Paths.get("config", "keybindings.json");
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
public static KeyBindings load() {
if (Files.exists(FILE)) {
try (Reader r = Files.newBufferedReader(FILE)) {
return GSON.fromJson(r, KeyBindings.class);
} catch (IOException e) {
System.err.println("keybindings.json konnte nicht geladen werden: " + e.getMessage());
}
}
return new KeyBindings();
}
public static void save(KeyBindings kb) {
try {
Files.createDirectories(FILE.getParent());
try (Writer w = Files.newBufferedWriter(FILE)) {
GSON.toJson(kb, w);
}
} catch (IOException e) {
System.err.println("keybindings.json konnte nicht gespeichert werden: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,44 @@
package de.blight.game.config;
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;
/** 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"},
};
public int get(String fieldName) {
try { return (int) KeyBindings.class.getField(fieldName).get(this); }
catch (Exception e) { return 0; }
}
public void set(String fieldName, int keyCode) {
try { KeyBindings.class.getField(fieldName).setInt(this, keyCode); }
catch (Exception ignored) {}
}
public KeyBindings copy() {
KeyBindings c = new KeyBindings();
for (String[] e : ENTRIES) c.set(e[0], get(e[0]));
return c;
}
public void copyFrom(KeyBindings src) {
for (String[] e : ENTRIES) set(e[0], src.get(e[0]));
}
}

View File

@@ -0,0 +1,51 @@
package de.blight.game.config;
import com.jme3.input.KeyInput;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
/** Bildet KeyInput-Codes auf lesbare Namen ab (via Reflection auf KeyInput-Konstanten). */
public final class KeyNames {
private static final Map<Integer, String> NAMES = new HashMap<>();
static {
for (Field f : KeyInput.class.getFields()) {
if (f.getName().startsWith("KEY_") && f.getType() == int.class) {
try {
NAMES.put(f.getInt(null), pretty(f.getName().substring(4)));
} catch (IllegalAccessException ignored) {}
}
}
}
public static String of(int keyCode) {
return NAMES.getOrDefault(keyCode, "Key#" + keyCode);
}
private static String pretty(String raw) {
// "LSHIFT" → "L-Shift", "SPACE" → "Space", "W" → "W"
return switch (raw) {
case "SPACE" -> "Space";
case "LSHIFT" -> "L-Shift";
case "RSHIFT" -> "R-Shift";
case "LCONTROL"-> "L-Ctrl";
case "RCONTROL"-> "R-Ctrl";
case "LMENU" -> "L-Alt";
case "RMENU" -> "R-Alt";
case "RETURN" -> "Enter";
case "BACK" -> "Backspace";
case "UP" -> "Pfeil-Hoch";
case "DOWN" -> "Pfeil-Runter";
case "LEFT" -> "Pfeil-Links";
case "RIGHT" -> "Pfeil-Rechts";
default -> raw.length() == 1 ? raw : capitalize(raw);
};
}
private static String capitalize(String s) {
return s.isEmpty() ? s : s.charAt(0) + s.substring(1).toLowerCase();
}
}

View File

@@ -0,0 +1,168 @@
package de.blight.game.config;
import com.jme3.app.Application;
import com.jme3.app.SimpleApplication;
import com.jme3.app.state.BaseAppState;
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.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;
public class PauseMenu extends BaseAppState {
private static final ColorRGBA COL_BG = new ColorRGBA(0.05f, 0.05f, 0.08f, 0.88f);
private static final ColorRGBA COL_PANEL = new ColorRGBA(0.10f, 0.10f, 0.16f, 1.00f);
private static final ColorRGBA COL_BTN = new ColorRGBA(0.18f, 0.18f, 0.28f, 1.00f);
private static final ColorRGBA COL_BTN_DIS = new ColorRGBA(0.12f, 0.12f, 0.18f, 1.00f);
private static final ColorRGBA COL_BTN_QUIT = new ColorRGBA(0.38f, 0.10f, 0.10f, 1.00f);
private static final ColorRGBA COL_TEXT = ColorRGBA.White;
private static final ColorRGBA COL_TEXT_DIS = new ColorRGBA(0.40f, 0.40f, 0.40f, 1.00f);
private static final ColorRGBA COL_TEXT_SUB = new ColorRGBA(0.35f, 0.35f, 0.35f, 1.00f);
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 SimpleApplication app;
private Node guiNode;
private BitmapFont font;
private Node panel;
private Runnable onGraphics;
private Runnable onControls;
// [x, y, w, h] per button
private final float[][] btnBounds = new float[4][4];
public PauseMenu(Runnable onGraphics, Runnable onControls) {
this.onGraphics = onGraphics;
this.onControls = onControls;
}
@Override
protected void initialize(Application app) {
this.app = (SimpleApplication) app;
this.guiNode = this.app.getGuiNode();
this.font = app.getAssetManager().loadFont("Interface/Fonts/Default.fnt");
}
@Override
protected void onEnable() {
buildUI();
app.getInputManager().setCursorVisible(true);
app.getInputManager().addMapping("_PauseClick", new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
app.getInputManager().addListener(clickListener, "_PauseClick");
}
@Override
protected void onDisable() {
if (panel != null) { guiNode.detachChild(panel); panel = null; }
app.getInputManager().deleteMapping("_PauseClick");
app.getInputManager().setCursorVisible(false);
}
@Override protected void cleanup(Application app) {}
private void buildUI() {
float sw = app.getCamera().getWidth();
float sh = app.getCamera().getHeight();
panel = new Node("pause-panel");
addQuad(panel, 0, 0, sw, sh, COL_BG, -2);
float pw = 320, ph = 360;
float px = (sw - pw) / 2f, py = (sh - ph) / 2f;
addQuad(panel, px, py, pw, ph, COL_PANEL, -1);
BitmapText title = txt("PAUSE", 26, COL_TEXT);
centerText(title, px, py + ph - 48, pw);
panel.attachChild(title);
String[] labels = {"Grafik", "Audio", "Steuerung", "Beenden"};
boolean[] enabled = {true, false, true, true};
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++) {
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);
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);
panel.attachChild(hint);
} else {
centerText(lbl, bx, by + bh - 16, bw);
}
panel.attachChild(lbl);
btnBounds[i][0] = bx; btnBounds[i][1] = by;
btnBounds[i][2] = bw; btnBounds[i][3] = bh;
}
guiNode.attachChild(panel);
}
private final ActionListener clickListener = (name, isPressed, tpf) -> {
if (!isPressed) return;
Vector2f c = app.getInputManager().getCursorPosition();
for (int i = 0; i < 4; 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_BEENDEN -> app.stop();
}
return;
}
};
// -----------------------------------------------------------------------
private Geometry addQuad(Node parent, float x, float y, float w, float h, ColorRGBA color, float z) {
Geometry geo = new Geometry("q", new Quad(w, h));
Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", color.clone());
if (color.a < 1f) {
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
geo.setQueueBucket(RenderQueue.Bucket.Transparent);
}
geo.setMaterial(mat);
geo.setLocalTranslation(x, y, z);
parent.attachChild(geo);
return geo;
}
private BitmapText txt(String s, int size, ColorRGBA color) {
BitmapText t = new BitmapText(font, false);
t.setSize(size); t.setColor(color); t.setText(s);
return t;
}
private void centerText(BitmapText t, float x, float y, float width) {
t.setLocalTranslation(x + (width - t.getLineWidth()) / 2f, y, 1);
}
private boolean hits(Vector2f p, float x, float y, float w, float h) {
return p.x >= x && p.x <= x + w && p.y >= y && p.y <= y + h;
}
}

View File

@@ -0,0 +1,113 @@
package de.blight.game.control;
import com.jme3.bullet.control.CharacterControl;
import com.jme3.input.InputManager;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import com.jme3.scene.Spatial;
import de.blight.game.config.KeyBindings;
public class PlayerInputControl {
private static final float MOVE_SPEED = 0.07f;
private static final float SPRINT_MULT = 1.5f;
private static final float ROTATE_SPEED = 10f;
private static final String[] ACTION_NAMES =
{"Forward", "Backward", "Left", "Right", "Jump", "Sprint"};
private final InputManager inputManager;
private final Camera cam;
private CharacterControl physicsChar;
private Spatial visual;
private boolean forward, backward, left, right, sprint;
private boolean paused = false;
// Listener als Feld, damit er bei reload nicht doppelt registriert wird
private final ActionListener actionListener = (name, isPressed, tpf) -> {
if (paused) return;
switch (name) {
case "Forward" -> forward = isPressed;
case "Backward" -> backward = isPressed;
case "Left" -> left = isPressed;
case "Right" -> right = isPressed;
case "Sprint" -> sprint = isPressed;
case "Jump" -> { if (isPressed && physicsChar != null) physicsChar.jump(); }
}
};
public PlayerInputControl(InputManager inputManager, Camera cam, KeyBindings kb) {
this.inputManager = inputManager;
this.cam = cam;
registerMappings(kb);
}
public void setPhysicsCharacter(CharacterControl physicsChar) {
this.physicsChar = physicsChar;
}
public void setVisual(Spatial visual) {
this.visual = visual;
}
public void setPaused(boolean paused) {
this.paused = paused;
if (paused) {
forward = backward = left = right = sprint = false;
if (physicsChar != null) physicsChar.setWalkDirection(Vector3f.ZERO);
}
}
/** Löscht alte Mappings und registriert neue aus den übergebenen KeyBindings. */
public void reloadBindings(KeyBindings kb) {
for (String a : ACTION_NAMES) inputManager.deleteMapping(a);
registerMappings(kb);
// Zustand zurücksetzen, damit keine Taste „hängt"
forward = backward = left = right = sprint = false;
if (physicsChar != null) physicsChar.setWalkDirection(Vector3f.ZERO);
}
private void registerMappings(KeyBindings kb) {
inputManager.addMapping("Forward", new KeyTrigger(kb.forward));
inputManager.addMapping("Backward", new KeyTrigger(kb.backward));
inputManager.addMapping("Left", new KeyTrigger(kb.left));
inputManager.addMapping("Right", new KeyTrigger(kb.right));
inputManager.addMapping("Jump", new KeyTrigger(kb.jump));
inputManager.addMapping("Sprint", new KeyTrigger(kb.sprint));
inputManager.addListener(actionListener, ACTION_NAMES);
}
public void update(float tpf) {
if (physicsChar == null || paused) return;
Vector3f camDir = cam.getDirection().clone().setY(0).normalizeLocal();
Vector3f camLeft = cam.getLeft().clone().setY(0).normalizeLocal();
Vector3f moveDir = new Vector3f();
if (forward) moveDir.addLocal(camDir);
if (backward) moveDir.subtractLocal(camDir);
if (left) moveDir.addLocal(camLeft);
if (right) moveDir.subtractLocal(camLeft);
if (moveDir.lengthSquared() > 0.001f) {
moveDir.normalizeLocal();
float speed = sprint ? MOVE_SPEED * SPRINT_MULT : MOVE_SPEED;
physicsChar.setWalkDirection(moveDir.mult(speed));
if (visual != null) {
Quaternion targetRot = new Quaternion();
targetRot.lookAt(moveDir, Vector3f.UNIT_Y);
Quaternion current = visual.getLocalRotation().clone();
current.slerp(targetRot, ROTATE_SPEED * tpf);
visual.setLocalRotation(current);
}
} else {
physicsChar.setWalkDirection(Vector3f.ZERO);
}
}
}

View File

@@ -0,0 +1,93 @@
package de.blight.game.control;
import com.jme3.input.InputManager;
import com.jme3.input.MouseInput;
import com.jme3.input.controls.AnalogListener;
import com.jme3.input.controls.MouseAxisTrigger;
import com.jme3.math.*;
import com.jme3.renderer.Camera;
import com.jme3.scene.Spatial;
/**
* Third-Person-Kamera:
* - Mausbewegung → Kamera um den Charakter drehen (immer, kein Klick nötig)
* - Y-Achse invertiert → Maus hoch = Kamera runter (oldschool)
* - Mausrad → Zoom (Abstand)
*/
public class ThirdPersonCamera {
private static final float MOUSE_SENSITIVITY = 1.8f;
private static final float MIN_DISTANCE = 3f;
private static final float MAX_DISTANCE = 20f;
private static final float MIN_VERTICAL_ANGLE = -0.3f;
private static final float MAX_VERTICAL_ANGLE = FastMath.HALF_PI - 0.1f;
private static final float TARGET_HEIGHT = 1.6f;
private final Camera cam;
private final InputManager inputManager;
private Spatial target;
private float yaw = 0f;
private float pitch = 0.4f;
private float distance = 10f;
private boolean paused = false;
public ThirdPersonCamera(Camera cam, InputManager inputManager) {
this.cam = cam;
this.inputManager = inputManager;
registerMappings();
}
public void setTarget(Spatial target) {
this.target = target;
}
public void setPaused(boolean paused) { this.paused = paused; }
// -----------------------------------------------------------------------
private void registerMappings() {
inputManager.addMapping("ZoomIn", new MouseAxisTrigger(MouseInput.AXIS_WHEEL, false));
inputManager.addMapping("ZoomOut", new MouseAxisTrigger(MouseInput.AXIS_WHEEL, true));
inputManager.addMapping("MouseX", new MouseAxisTrigger(MouseInput.AXIS_X, false));
inputManager.addMapping("MouseXNeg", new MouseAxisTrigger(MouseInput.AXIS_X, true));
inputManager.addMapping("MouseY", new MouseAxisTrigger(MouseInput.AXIS_Y, false));
inputManager.addMapping("MouseYNeg", new MouseAxisTrigger(MouseInput.AXIS_Y, true));
AnalogListener analogListener = (name, value, tpf) -> {
if (paused) return;
switch (name) {
// Horizontale Rotation
case "MouseX" -> yaw -= value * MOUSE_SENSITIVITY;
case "MouseXNeg" -> yaw += value * MOUSE_SENSITIVITY;
// Vertikale Rotation — Y invertiert: Maus hoch → Kamera runter
case "MouseY" -> pitch = FastMath.clamp(pitch - value * MOUSE_SENSITIVITY, MIN_VERTICAL_ANGLE, MAX_VERTICAL_ANGLE);
case "MouseYNeg" -> pitch = FastMath.clamp(pitch + value * MOUSE_SENSITIVITY, MIN_VERTICAL_ANGLE, MAX_VERTICAL_ANGLE);
// Zoom
case "ZoomIn" -> distance = FastMath.clamp(distance - value * 20f, MIN_DISTANCE, MAX_DISTANCE);
case "ZoomOut" -> distance = FastMath.clamp(distance + value * 20f, MIN_DISTANCE, MAX_DISTANCE);
}
};
inputManager.addListener(analogListener,
"MouseX", "MouseXNeg", "MouseY", "MouseYNeg", "ZoomIn", "ZoomOut");
}
public void update(float tpf) {
if (target == null) return;
Vector3f pivot = target.getWorldTranslation().add(0, TARGET_HEIGHT, 0);
// Sphärische Koordinaten → kartesisch
float x = distance * FastMath.cos(pitch) * FastMath.sin(yaw);
float y = distance * FastMath.sin(pitch);
float z = distance * FastMath.cos(pitch) * FastMath.cos(yaw);
Vector3f camPos = pivot.add(x, y, z);
cam.setLocation(camPos);
cam.lookAt(pivot, Vector3f.UNIT_Y);
}
/** Aktueller Yaw-Winkel (für CharacterControl nutzbar). */
public float getYaw() { return yaw; }
}

View File

@@ -0,0 +1,308 @@
package de.blight.game.scene;
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.bullet.collision.shapes.CapsuleCollisionShape;
import com.jme3.bullet.control.CharacterControl;
import com.jme3.bullet.control.RigidBodyControl;
import com.jme3.bullet.util.CollisionShapeFactory;
import com.jme3.light.*;
import com.jme3.material.Material;
import com.jme3.math.*;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.scene.*;
import com.jme3.scene.shape.*;
import com.jme3.shadow.*;
import com.jme3.terrain.geomipmap.*;
import com.jme3.texture.*;
import com.jme3.util.SkyFactory;
import de.blight.game.config.KeyBindings;
import de.blight.game.control.PlayerInputControl;
import de.blight.game.control.ThirdPersonCamera;
public class WorldScene extends BaseAppState {
private SimpleApplication app;
private Node rootNode;
private AssetManager assetManager;
private BulletAppState bulletAppState;
private final KeyBindings keyBindings;
private ThirdPersonCamera thirdPersonCam;
private PlayerInputControl playerInput;
public WorldScene(KeyBindings keyBindings) {
this.keyBindings = keyBindings;
}
/** Wird von ConfigScreen nach dem Speichern aufgerufen. */
public void reloadBindings(KeyBindings kb) {
if (playerInput != null) playerInput.reloadBindings(kb);
}
public void setPaused(boolean paused) {
if (playerInput != null) playerInput.setPaused(paused);
if (thirdPersonCam != null) thirdPersonCam.setPaused(paused);
}
// -----------------------------------------------------------------------
// Lifecycle
// -----------------------------------------------------------------------
@Override
protected void initialize(Application app) {
this.app = (SimpleApplication) app;
this.rootNode = this.app.getRootNode();
this.assetManager = app.getAssetManager();
bulletAppState = new BulletAppState();
app.getStateManager().attach(bulletAppState);
}
@Override
protected void onEnable() {
buildLighting();
TerrainQuad terrain = buildTerrain();
buildDecorations(terrain);
Node character = buildCharacter();
rootNode.attachChild(character);
// Bullet-Charakter: Kapsel 0.4 Radius, 1.0 Höhe, Y-Achse (1)
CapsuleCollisionShape capsule = new CapsuleCollisionShape(0.4f, 1.0f, 1);
CharacterControl physicsChar = new CharacterControl(capsule, 0.05f);
physicsChar.setJumpSpeed(12f);
physicsChar.setFallSpeed(35f);
physicsChar.setGravity(35f);
physicsChar.setPhysicsLocation(new Vector3f(0, 5f, 0));
character.addControl(physicsChar);
bulletAppState.getPhysicsSpace().add(physicsChar);
playerInput = new PlayerInputControl(app.getInputManager(), app.getCamera(), keyBindings);
playerInput.setPhysicsCharacter(physicsChar);
playerInput.setVisual(character);
thirdPersonCam = new ThirdPersonCamera(app.getCamera(), app.getInputManager());
thirdPersonCam.setTarget(character);
// Maus einfangen keine Klick-Pflicht für Kamerasteuerung
app.getInputManager().setCursorVisible(false);
}
@Override
public void update(float tpf) {
playerInput.update(tpf);
thirdPersonCam.update(tpf);
}
@Override protected void cleanup(Application app) {}
@Override protected void onDisable() {}
// -----------------------------------------------------------------------
// Beleuchtung
// -----------------------------------------------------------------------
private void buildLighting() {
DirectionalLight sun = new DirectionalLight();
sun.setDirection(new Vector3f(-0.5f, -1f, -0.5f).normalizeLocal());
sun.setColor(ColorRGBA.White.mult(1.4f));
rootNode.addLight(sun);
AmbientLight ambient = new AmbientLight();
ambient.setColor(new ColorRGBA(0.3f, 0.3f, 0.4f, 1f));
rootNode.addLight(ambient);
DirectionalLightShadowRenderer shadowRenderer =
new DirectionalLightShadowRenderer(assetManager, 2048, 3);
shadowRenderer.setLight(sun);
shadowRenderer.setShadowIntensity(0.4f);
app.getViewPort().addProcessor(shadowRenderer);
try {
Spatial sky = SkyFactory.createSky(assetManager,
"Textures/Sky/Bright/BrightSky.dds",
SkyFactory.EnvMapType.CubeMap);
rootNode.attachChild(sky);
} catch (Exception ignored) {}
}
// -----------------------------------------------------------------------
// Terrain
// -----------------------------------------------------------------------
private TerrainQuad buildTerrain() {
int size = 257;
float[] heights = new float[size * size];
for (int z = 0; z < size; z++) {
for (int x = 0; x < size; x++) {
float nx = x / (float) size;
float nz = z / (float) size;
heights[z * size + x] =
FastMath.sin(nx * FastMath.TWO_PI * 2) * 2f
+ FastMath.sin(nz * FastMath.TWO_PI * 3) * 1.5f
+ FastMath.sin((nx + nz) * FastMath.TWO_PI * 1.5f) * 1f;
}
}
TerrainQuad terrain = new TerrainQuad("terrain", 65, size, heights);
Material terrainMat = new Material(assetManager, "Common/MatDefs/Terrain/Terrain.j3md");
Texture grass = assetManager.loadTexture("Textures/gras.png");
grass.setWrap(Texture.WrapMode.Repeat);
terrainMat.setTexture("Tex1", grass);
terrainMat.setFloat("Tex1Scale", 64f);
terrain.setMaterial(terrainMat);
terrain.setLocalTranslation(0, -5f, 0);
terrain.setLocalScale(0.5f, 0.5f, 0.5f);
terrain.setShadowMode(RenderQueue.ShadowMode.Receive);
// Statischer Physics-Body (mass=0) für Terrain-Kollision
RigidBodyControl terrainPhysics = new RigidBodyControl(
CollisionShapeFactory.createMeshShape(terrain), 0f);
terrain.addControl(terrainPhysics);
bulletAppState.getPhysicsSpace().add(terrainPhysics);
rootNode.attachChild(terrain);
return terrain;
}
// -----------------------------------------------------------------------
// Dekorationen Höhe per TerrainQuad.getHeight() anpassen
// -----------------------------------------------------------------------
private void buildDecorations(TerrainQuad terrain) {
Material stoneMat = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
stoneMat.setBoolean("UseMaterialColors", true);
stoneMat.setColor("Diffuse", new ColorRGBA(0.55f, 0.55f, 0.55f, 1f));
stoneMat.setColor("Ambient", new ColorRGBA(0.3f, 0.3f, 0.3f, 1f));
stoneMat.setColor("Specular", ColorRGBA.White);
stoneMat.setFloat("Shininess", 32f);
Material treeTrunkMat = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
treeTrunkMat.setBoolean("UseMaterialColors", true);
treeTrunkMat.setColor("Diffuse", new ColorRGBA(0.45f, 0.28f, 0.1f, 1f));
treeTrunkMat.setColor("Ambient", new ColorRGBA(0.2f, 0.12f, 0.04f, 1f));
treeTrunkMat.setColor("Specular", ColorRGBA.Black);
Material treeTopMat = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
treeTopMat.setBoolean("UseMaterialColors", true);
treeTopMat.setColor("Diffuse", new ColorRGBA(0.1f, 0.55f, 0.15f, 1f));
treeTopMat.setColor("Ambient", new ColorRGBA(0.05f, 0.25f, 0.07f, 1f));
treeTopMat.setColor("Specular", ColorRGBA.Black);
float[][] treeXZ = {
{12, 8}, {-15, 5}, {20, -10}, {-8, -18},
{5, 25}, {-22, 12}, {18, 20}, {-10, -5}
};
for (float[] xz : treeXZ) {
float worldY = terrainWorldY(terrain, xz[0], xz[1]);
Node tree = new Node("tree");
Geometry trunk = new Geometry("trunk", new Cylinder(8, 8, 0.25f, 2.5f, true));
trunk.setMaterial(treeTrunkMat);
trunk.rotate(FastMath.HALF_PI, 0, 0);
trunk.setLocalTranslation(0, 1.25f, 0);
trunk.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
Geometry crown = new Geometry("crown", new Sphere(12, 12, 2.2f));
crown.setMaterial(treeTopMat);
crown.setLocalTranslation(0, 3.8f, 0);
crown.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
tree.attachChild(trunk);
tree.attachChild(crown);
tree.setLocalTranslation(xz[0], worldY, xz[1]);
rootNode.attachChild(tree);
}
float[][] stoneXZ = {{6, -6}, {-12, 15}, {16, -4}, {-3, 10}};
for (float[] xz : stoneXZ) {
float worldY = terrainWorldY(terrain, xz[0], xz[1]);
float r = 0.6f + FastMath.nextRandomFloat() * 0.8f;
Geometry stone = new Geometry("stone", new Sphere(8, 8, r));
stone.setMaterial(stoneMat);
stone.setLocalTranslation(xz[0], worldY + r * 0.5f, xz[1]);
stone.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
rootNode.attachChild(stone);
}
}
/** Konvertiert Welt-XZ in lokale Terrain-Koordinaten und fragt die Höhe ab. */
private float terrainWorldY(TerrainQuad terrain, float worldX, float worldZ) {
Vector3f terrainTranslation = terrain.getWorldTranslation();
Vector3f terrainScale = terrain.getWorldScale();
float localX = (worldX - terrainTranslation.x) / terrainScale.x;
float localZ = (worldZ - terrainTranslation.z) / terrainScale.z;
float localH = terrain.getHeight(new Vector2f(localX, localZ));
return terrainTranslation.y + localH * terrainScale.y;
}
// -----------------------------------------------------------------------
// Charakter (visueller Node, ohne eigenes Mesh für Physics)
// -----------------------------------------------------------------------
private Node buildCharacter() {
Node character = new Node("character");
Material bodyMat = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
bodyMat.setBoolean("UseMaterialColors", true);
bodyMat.setColor("Diffuse", new ColorRGBA(0.2f, 0.4f, 0.8f, 1f));
bodyMat.setColor("Ambient", new ColorRGBA(0.1f, 0.2f, 0.4f, 1f));
bodyMat.setColor("Specular", ColorRGBA.White);
bodyMat.setFloat("Shininess", 64f);
Material headMat = new Material(assetManager, "Common/MatDefs/Light/Lighting.j3md");
headMat.setBoolean("UseMaterialColors", true);
headMat.setColor("Diffuse", new ColorRGBA(0.9f, 0.75f, 0.6f, 1f));
headMat.setColor("Ambient", new ColorRGBA(0.45f, 0.37f, 0.3f, 1f));
headMat.setColor("Specular", new ColorRGBA(0.2f, 0.2f, 0.2f, 1f));
headMat.setFloat("Shininess", 16f);
Geometry body = new Geometry("body", new Cylinder(8, 16, 0.35f, 1.1f, true));
body.setMaterial(bodyMat);
body.rotate(FastMath.HALF_PI, 0, 0);
body.setLocalTranslation(0, 0.9f, 0);
body.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
Geometry head = new Geometry("head", new Sphere(16, 16, 0.28f));
head.setMaterial(headMat);
head.setLocalTranslation(0, 1.75f, 0);
head.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
Geometry armL = buildLimb(bodyMat, 0.1f, 0.55f);
armL.setLocalTranslation(-0.5f, 0.85f, 0);
armL.rotate(0, 0, FastMath.DEG_TO_RAD * 20f);
Geometry armR = buildLimb(bodyMat, 0.1f, 0.55f);
armR.setLocalTranslation(0.5f, 0.85f, 0);
armR.rotate(0, 0, FastMath.DEG_TO_RAD * -20f);
Geometry legL = buildLimb(bodyMat, 0.12f, 0.6f);
legL.setLocalTranslation(-0.18f, 0.3f, 0);
Geometry legR = buildLimb(bodyMat, 0.12f, 0.6f);
legR.setLocalTranslation(0.18f, 0.3f, 0);
character.attachChild(body);
character.attachChild(head);
character.attachChild(armL);
character.attachChild(armR);
character.attachChild(legL);
character.attachChild(legR);
return character;
}
private Geometry buildLimb(Material mat, float radius, float height) {
Geometry limb = new Geometry("limb", new Cylinder(6, 12, radius, height, true));
limb.setMaterial(mat);
limb.rotate(FastMath.HALF_PI, 0, 0);
limb.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
return limb;
}
}