Weitere Anpassungen bezüglich der Voxel und des Zoning Systems

This commit is contained in:
2026-08-12 23:01:09 +02:00
parent ae3caacebf
commit 400a736804
88 changed files with 2826 additions and 686 deletions

View File

@@ -222,7 +222,8 @@ public class JmeConsole extends BaseAppState {
private BitmapText makeLine(BitmapFont font, ColorRGBA color, float x, float y) {
BitmapText t = new BitmapText(font);
t.setSize(LINE_H - 2f);
t.setSize((LINE_H - 2f) * 2f);
t.setLocalScale(0.5f, 0.5f, 1f);
t.setColor(color);
t.setLocalTranslation(x, y, 0f);
return t;

View File

@@ -102,6 +102,7 @@ public class WorldScene extends BaseAppState {
private float spawnYaw = 0f;
private de.blight.game.state.OceanSoundState oceanSound;
private de.blight.game.state.AmbientSoundSystem ambientSounds;
private de.blight.game.state.MusicSystem musicSystem;
private de.blight.game.audio.FootstepSystem footstepSystem;
private de.blight.game.state.DrownState drownState;
private WaterInteractionState waterInteractionState;
@@ -430,8 +431,12 @@ public class WorldScene extends BaseAppState {
app.getListener().setRotation(app.getCamera().getRotation());
if (oceanSound != null) oceanSound.setPlayerPosition(pos);
if (ambientSounds != null) ambientSounds.setPlayerPosition(pos);
if (musicSystem != null) musicSystem.setPlayerPosition(pos);
}
if (musicSystem != null && dayNight != null)
musicSystem.setDaytime(dayNight.getSunElevation() >= 0f);
if (terrainMaterial != null && dayNight != null && dayNight.getSunLight() != null) {
terrainMaterial.setVector3("LightDir", dayNight.getSunDirection().negate());
ColorRGBA sc = dayNight.getSunLight().getColor();
@@ -945,6 +950,9 @@ public class WorldScene extends BaseAppState {
ambientSounds = new de.blight.game.state.AmbientSoundSystem();
app.getStateManager().attach(ambientSounds);
musicSystem = new de.blight.game.state.MusicSystem();
app.getStateManager().attach(musicSystem);
}
// -----------------------------------------------------------------------

View File

@@ -285,6 +285,14 @@ public class DayNightState extends BaseAppState implements TimeListener {
}
cc.a = 1f;
cloudMat.setColor("CloudColor", cc);
// Abendrot-Glut auf Wolkenunterseite (sunElevation: +1=Zenit, 0=Horizont, 1=Nadir)
// Sichtbar: Sonne bis +8.6° über Horizont (elev=+0.15) bis 5.7° darunter (Afterglow)
float glowCeil = FastMath.clamp(1f - sunElevation / 0.45f, 0f, 1f); // blendet über ~26° aus
float glowFade = FastMath.clamp((sunElevation + 0.10f) / 0.06f, 0f, 1f); // blendet unter 5.7° aus
float sunsetT = glowCeil * glowFade;
sunsetT = sunsetT * sunsetT; // weichere Kurve
cloudMat.setVector3("SunsetGlow", new Vector3f(0.42f * sunsetT, 0.13f * sunsetT, 0f));
}
// Sonnenlicht + Schatten: Okklusion durch CloudDome bestimmt beides

View File

@@ -867,7 +867,8 @@ public class DialogHudState extends BaseAppState {
private BitmapText makeTxt(String s, float size, ColorRGBA color) {
BitmapText t = new BitmapText(font);
t.setSize(size);
t.setSize(size * 2f);
t.setLocalScale(0.5f, 0.5f, 1f);
t.setColor(color);
t.setText(s);
return t;

View File

@@ -264,7 +264,8 @@ public class HotbarState extends BaseAppState {
private BitmapText txt(String s, int size, ColorRGBA col) {
BitmapText t = new BitmapText(font);
t.setSize(size);
t.setSize(size * 2f);
t.setLocalScale(0.5f, 0.5f, 1f);
t.setColor(col);
t.setText(s);
t.setQueueBucket(RenderQueue.Bucket.Gui);

View File

@@ -43,7 +43,8 @@ public class InteractionHudState extends BaseAppState {
BitmapFont font = app.getAssetManager().loadFont("Interface/Fonts/Default.fnt");
labelText = new BitmapText(font);
labelText.setSize(font.getCharSet().getRenderedSize() * 1.2f);
labelText.setSize(font.getCharSet().getRenderedSize() * 1.2f * 2f);
labelText.setLocalScale(0.5f, 0.5f, 1f);
labelText.setColor(new ColorRGBA(1f, 0.95f, 0.6f, 1f));
labelText.setCullHint(Spatial.CullHint.Always);
guiNode.attachChild(labelText);

View File

@@ -6,63 +6,107 @@ import com.jme3.app.state.BaseAppState;
import com.jme3.asset.AssetManager;
import com.jme3.audio.AudioData;
import com.jme3.audio.AudioNode;
import com.jme3.audio.AudioSource;
import com.jme3.font.BitmapFont;
import com.jme3.font.BitmapText;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector3f;
import com.jme3.scene.Node;
import de.blight.common.AreaDefinition;
import de.blight.common.AreaDefinitionIO;
import de.blight.common.AreaIO;
import de.blight.common.PlacedArea;
import de.blight.lang.TextResolver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Proximity-based ambient music per polygon area.
* Three parallel tracks (day / night / combat) play when the player is inside.
* All three are attached + played on entry and detached on exit.
* Which one is actually audible is controlled via volume (wiring to day/night deferred).
* Area-based music system.
*
* Track priority: combat (not yet implemented) > night > day (fallback).
* On area entry the appropriate track fades in.
* On day/night change the running track finishes its current loop,
* then the next track starts (no abrupt cut).
* Day track is always the fallback when no other track is configured.
*/
public class MusicSystem extends BaseAppState {
private static final Logger log = LoggerFactory.getLogger(MusicSystem.class);
private static final float CHECK_INTERVAL = 0.25f;
private static final float FADE_DURATION = 3f;
private static final float MUSIC_VOLUME = 0.7f;
private static final Logger log = LoggerFactory.getLogger(MusicSystem.class);
private static final float CHECK_INTERVAL = 0.25f;
private static final float FADE_DURATION = 3f;
private static final float AREA_NAME_SHOW = 10f;
private static final float AREA_NAME_FADE = 1.5f;
private static final float AREA_NAME_SIZE = 24f;
private enum FadeState { INACTIVE, FADING_IN, ACTIVE, FADING_OUT }
private static final int SLOT_DAY = 0;
private static final int SLOT_NIGHT = 1;
private SimpleApplication app;
private AssetManager assets;
private Node rootNode;
private enum Phase { INACTIVE, FADING_IN, ACTIVE, FINISHING, FADING_OUT }
private final List<PlacedArea> data = new ArrayList<>();
// three nodes per area: [0]=day, [1]=night, [2]=combat; element may be null
private final List<AudioNode[]> tracks = new ArrayList<>();
private final List<FadeState> fadeStates = new ArrayList<>();
private SimpleApplication app;
private AssetManager assets;
private Node rootNode;
private AudioSettingsState audioSettings;
private final List<PlacedArea> data = new ArrayList<>();
private final List<String> areaNameKeys = new ArrayList<>();
private final List<String> areaShortIds = new ArrayList<>();
// index [area][slot]: slot 0 = day, slot 1 = night
private final List<AudioNode[]> nodes = new ArrayList<>();
private final List<Phase> phases = new ArrayList<>();
private final List<Integer> activeSlot = new ArrayList<>();
private final List<Integer> pendingSlot = new ArrayList<>();
private Vector3f playerPos = new Vector3f();
private float checkTimer = 0f;
private boolean isDaytime = true;
private BitmapText areaNameText;
private float areaNameTimer = 0f;
// ── Lifecycle ─────────────────────────────────────────────────────────────
@Override
protected void initialize(Application application) {
app = (SimpleApplication) application;
assets = app.getAssetManager();
rootNode = app.getRootNode();
audioSettings = app.getStateManager().getState(AudioSettingsState.class);
BitmapFont font = assets.loadFont("Interface/Fonts/Default.fnt");
areaNameText = new BitmapText(font);
areaNameText.setSize(AREA_NAME_SIZE * 2f);
areaNameText.setLocalScale(0.5f, 0.5f, 1f);
areaNameText.setColor(new ColorRGBA(1f, 0.95f, 0.8f, 0f));
app.getGuiNode().attachChild(areaNameText);
try {
Map<String, AreaDefinition> defs = new java.util.HashMap<>();
try {
for (AreaDefinition d : AreaDefinitionIO.load()) defs.put(d.id(), d);
} catch (IOException e) {
log.warn("[MusicSystem] Area-Definitions nicht ladbar: {}", e.getMessage());
}
for (PlacedArea area : AreaIO.load()) {
AudioNode[] arr = {
loadAmbient(area.dayTrack()),
loadAmbient(area.nightTrack()),
loadAmbient(area.combatTrack())
};
boolean hasAny = false;
for (AudioNode n : arr) if (n != null) { hasAny = true; break; }
if (!hasAny) continue;
AreaDefinition def = defs.get(area.areaId());
if (def == null) continue;
AudioNode day = loadTrack(def.dayTrack());
AudioNode night = loadTrack(def.nightTrack());
if (day == null && night == null) continue;
String rawId = def.id();
String shortId = rawId.startsWith("area.") ? rawId.substring("area.".length()) : rawId;
data.add(area);
tracks.add(arr);
fadeStates.add(FadeState.INACTIVE);
areaNameKeys.add(def.nameKey());
areaShortIds.add(shortId);
nodes.add(new AudioNode[]{day, night});
phases.add(Phase.INACTIVE);
activeSlot.add(-1);
pendingSlot.add(-1);
}
if (!data.isEmpty()) log.info("[MusicSystem] {} Musik-Bereiche geladen.", data.size());
} catch (IOException e) {
@@ -72,88 +116,212 @@ public class MusicSystem extends BaseAppState {
@Override
protected void cleanup(Application application) {
for (int i = 0; i < tracks.size(); i++) {
if (fadeStates.get(i) != FadeState.INACTIVE) {
for (AudioNode n : tracks.get(i)) {
if (n != null) { n.stop(); rootNode.detachChild(n); }
}
}
for (int i = 0; i < data.size(); i++) {
if (phases.get(i) != Phase.INACTIVE) stopAll(i);
}
data.clear();
tracks.clear();
fadeStates.clear();
areaNameKeys.clear();
areaShortIds.clear();
nodes.clear();
phases.clear();
activeSlot.clear();
pendingSlot.clear();
if (areaNameText != null) app.getGuiNode().detachChild(areaNameText);
}
@Override protected void onEnable() {}
@Override protected void onDisable() {}
public void setPlayerPosition(Vector3f pos) {
playerPos.set(pos);
public void setPlayerPosition(Vector3f pos) { playerPos.set(pos); }
public void setDaytime(boolean day) {
if (isDaytime == day) return;
isDaytime = day;
// trigger track switch for all active areas
for (int i = 0; i < data.size(); i++) {
Phase ph = phases.get(i);
if (ph == Phase.INACTIVE || ph == Phase.FADING_OUT) continue;
int desired = desiredSlot(i);
if (desired != activeSlot.get(i)) {
startFinishing(i, desired);
}
}
}
// ── Update ────────────────────────────────────────────────────────────────
@Override
public void update(float tpf) {
if (data.isEmpty()) return;
for (int i = 0; i < tracks.size(); i++) {
FadeState fs = fadeStates.get(i);
if (fs == FadeState.INACTIVE || fs == FadeState.ACTIVE) continue;
AudioNode[] arr = tracks.get(i);
if (fs == FadeState.FADING_IN) {
boolean done = true;
for (AudioNode n : arr) {
if (n == null) continue;
float nv = Math.min(n.getVolume() + MUSIC_VOLUME * tpf / FADE_DURATION, MUSIC_VOLUME);
n.setVolume(nv);
if (nv < MUSIC_VOLUME) done = false;
}
if (done) fadeStates.set(i, FadeState.ACTIVE);
} else { // FADING_OUT
boolean done = true;
for (AudioNode n : arr) {
if (n == null) continue;
float nv = Math.max(n.getVolume() - MUSIC_VOLUME * tpf / FADE_DURATION, 0f);
n.setVolume(nv);
if (nv > 0f) done = false;
}
if (done) {
for (AudioNode n : arr) {
if (n != null) { n.stop(); rootNode.detachChild(n); }
}
fadeStates.set(i, FadeState.INACTIVE);
}
// area name display timer
if (areaNameTimer > 0f) {
areaNameTimer -= tpf;
if (areaNameTimer <= 0f) {
areaNameText.setColor(new ColorRGBA(1f, 0.95f, 0.8f, 0f));
areaNameTimer = 0f;
} else if (areaNameTimer < AREA_NAME_FADE) {
float alpha = areaNameTimer / AREA_NAME_FADE;
areaNameText.setColor(new ColorRGBA(1f, 0.95f, 0.8f, alpha));
}
}
if (data.isEmpty()) return;
// per-area fade/finish handling + live volume update for active tracks
float vol = musicVol();
for (int i = 0; i < data.size(); i++) {
Phase ph = phases.get(i);
switch (ph) {
case FADING_IN -> tickFadeIn(i, tpf, vol);
case FINISHING -> tickFinishing(i);
case FADING_OUT -> tickFadeOut(i, tpf, vol);
case ACTIVE -> {
AudioNode n = activeNode(i);
if (n != null) n.setVolume(vol);
}
default -> {}
}
}
// proximity check
checkTimer += tpf;
if (checkTimer < CHECK_INTERVAL) return;
checkTimer = 0f;
for (int i = 0; i < data.size(); i++) {
PlacedArea area = data.get(i);
boolean inside = pointInPolygon(playerPos.x, playerPos.z, area.pointsX(), area.pointsZ());
FadeState fs = fadeStates.get(i);
boolean inside = pointInPolygon(playerPos.x, playerPos.z, area.pointsX(), area.pointsZ());
Phase ph = phases.get(i);
if (inside && (fs == FadeState.INACTIVE || fs == FadeState.FADING_OUT)) {
if (fs == FadeState.INACTIVE) {
log.info("[MusicSystem] Bereich {} betreten → starte Tracks", i);
for (AudioNode n : tracks.get(i)) {
if (n != null) { n.setVolume(0f); rootNode.attachChild(n); n.play(); }
}
}
fadeStates.set(i, FadeState.FADING_IN);
} else if (!inside && (fs == FadeState.ACTIVE || fs == FadeState.FADING_IN)) {
log.info("[MusicSystem] Bereich {} verlassen → fade out", i);
fadeStates.set(i, FadeState.FADING_OUT);
if (inside && (ph == Phase.INACTIVE || ph == Phase.FADING_OUT)) {
startArea(i);
} else if (!inside && (ph == Phase.ACTIVE || ph == Phase.FADING_IN || ph == Phase.FINISHING)) {
beginFadeOut(i);
}
}
}
private AudioNode loadAmbient(String path) {
// ── State transitions ─────────────────────────────────────────────────────
private void startArea(int i) {
int slot = desiredSlot(i);
AudioNode n = nodes.get(i)[slot];
if (n == null) return;
n.setVolume(0f);
n.setLooping(true);
rootNode.attachChild(n);
n.play();
activeSlot.set(i, slot);
pendingSlot.set(i, -1);
phases.set(i, Phase.FADING_IN);
log.info("[MusicSystem] Area {} → slot {} gestartet", i, slot == SLOT_DAY ? "Tag" : "Nacht");
showAreaName(i);
}
private void startFinishing(int i, int desired) {
// let current track play to end, then switch
AudioNode cur = activeNode(i);
if (cur != null) cur.setLooping(false);
pendingSlot.set(i, desired);
phases.set(i, Phase.FINISHING);
log.info("[MusicSystem] Area {} → Slot wechsel nach {}, warte auf Loop-Ende", i, desired == SLOT_DAY ? "Tag" : "Nacht");
}
private void beginFadeOut(int i) {
phases.set(i, Phase.FADING_OUT);
}
private void stopAll(int i) {
for (AudioNode n : nodes.get(i)) {
if (n != null && n.getParent() != null) { n.stop(); rootNode.detachChild(n); }
}
activeSlot.set(i, -1);
pendingSlot.set(i, -1);
phases.set(i, Phase.INACTIVE);
}
// ── Tick helpers ──────────────────────────────────────────────────────────
private void tickFadeIn(int i, float tpf, float vol) {
AudioNode n = activeNode(i);
if (n == null) { phases.set(i, Phase.ACTIVE); return; }
float nv = Math.min(n.getVolume() + vol * tpf / FADE_DURATION, vol);
n.setVolume(nv);
if (nv >= vol) phases.set(i, Phase.ACTIVE);
}
private void tickFinishing(int i) {
AudioNode cur = activeNode(i);
// wait until the current (non-looping) track finishes
if (cur != null && cur.getStatus() != AudioSource.Status.Stopped) return;
// detach old
if (cur != null && cur.getParent() != null) { cur.stop(); rootNode.detachChild(cur); }
activeSlot.set(i, -1);
int next = pendingSlot.get(i);
pendingSlot.set(i, -1);
if (next < 0 || nodes.get(i)[next] == null) {
// fallback to day
next = SLOT_DAY;
}
AudioNode n = nodes.get(i)[next];
if (n == null) { phases.set(i, Phase.INACTIVE); return; }
n.setVolume(0f);
n.setLooping(true);
rootNode.attachChild(n);
n.play();
activeSlot.set(i, next);
phases.set(i, Phase.FADING_IN);
log.info("[MusicSystem] Area {} → nächster Slot {} gestartet", i, next == SLOT_DAY ? "Tag" : "Nacht");
}
private void tickFadeOut(int i, float tpf, float vol) {
AudioNode n = activeNode(i);
if (n == null) { stopAll(i); return; }
float nv = Math.max(n.getVolume() - vol * tpf / FADE_DURATION, 0f);
n.setVolume(nv);
if (nv <= 0f) stopAll(i);
}
// ── Area-Name-Anzeige ─────────────────────────────────────────────────────
private void showAreaName(int i) {
String key = i < areaNameKeys.size() ? areaNameKeys.get(i) : "";
String shortId = i < areaShortIds.size() ? areaShortIds.get(i) : key;
String resolved = TextResolver.get().resolveOrId(key);
String name = resolved.equals(key) ? shortId : resolved;
if (name.isBlank()) return;
areaNameText.setText(name);
float tw = areaNameText.getLineWidth() * 0.5f;
float sw = app.getCamera().getWidth();
float sh = app.getCamera().getHeight();
areaNameText.setLocalTranslation((sw - tw) / 2f, sh - 40f, 0f);
areaNameText.setColor(new ColorRGBA(1f, 0.95f, 0.8f, 1f));
areaNameTimer = AREA_NAME_SHOW;
}
// ── Helpers ───────────────────────────────────────────────────────────────
private float musicVol() {
return audioSettings != null ? audioSettings.effectiveMusic() : 0.7f;
}
private int desiredSlot(int i) {
AudioNode[] arr = nodes.get(i);
if (!isDaytime && arr[SLOT_NIGHT] != null) return SLOT_NIGHT;
return SLOT_DAY;
}
private AudioNode activeNode(int i) {
int slot = activeSlot.get(i);
if (slot < 0) return null;
return nodes.get(i)[slot];
}
private AudioNode loadTrack(String path) {
if (path == null || path.isEmpty()) return null;
try {
// Use Stream for music (files are large), Buffer for short sfx
AudioNode n = new AudioNode(assets, path, AudioData.DataType.Stream);
n.setLooping(true);
n.setPositional(false);