Soundsystem weiter ausgebaut
BIN
assets/imported/animations/sitting_cycle.glb
Normal file
18
blight-assets/src/main/resources/MatDefs/GaussianBlur.j3md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
MaterialDef GaussianBlur {
|
||||||
|
|
||||||
|
MaterialParameters {
|
||||||
|
Texture2D Texture
|
||||||
|
Float BlurScale : 6.0
|
||||||
|
Int NumSamples
|
||||||
|
Int NumSamplesDepth
|
||||||
|
Texture2D DepthTexture
|
||||||
|
}
|
||||||
|
|
||||||
|
Technique {
|
||||||
|
VertexShader GLSL150: Common/MatDefs/Post/Post15.vert
|
||||||
|
FragmentShader GLSL150: Shaders/GaussianBlur.frag
|
||||||
|
|
||||||
|
WorldParameters {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
45
blight-assets/src/main/resources/Shaders/GaussianBlur.frag
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
#import "Common/ShaderLib/GLSLCompat.glsllib"
|
||||||
|
|
||||||
|
uniform sampler2D m_Texture;
|
||||||
|
uniform float m_BlurScale;
|
||||||
|
|
||||||
|
in vec2 texCoord;
|
||||||
|
|
||||||
|
// 5x5 Gauß-Kernel (σ≈1.2, vollständig normiert)
|
||||||
|
void main() {
|
||||||
|
vec2 ts = m_BlurScale / vec2(textureSize(m_Texture, 0));
|
||||||
|
|
||||||
|
vec4 c = vec4(0.0);
|
||||||
|
|
||||||
|
c += texture2D(m_Texture, texCoord + vec2(-2.0, -2.0) * ts) * 0.00731;
|
||||||
|
c += texture2D(m_Texture, texCoord + vec2(-1.0, -2.0) * ts) * 0.02075;
|
||||||
|
c += texture2D(m_Texture, texCoord + vec2( 0.0, -2.0) * ts) * 0.02936;
|
||||||
|
c += texture2D(m_Texture, texCoord + vec2( 1.0, -2.0) * ts) * 0.02075;
|
||||||
|
c += texture2D(m_Texture, texCoord + vec2( 2.0, -2.0) * ts) * 0.00731;
|
||||||
|
|
||||||
|
c += texture2D(m_Texture, texCoord + vec2(-2.0, -1.0) * ts) * 0.02075;
|
||||||
|
c += texture2D(m_Texture, texCoord + vec2(-1.0, -1.0) * ts) * 0.05890;
|
||||||
|
c += texture2D(m_Texture, texCoord + vec2( 0.0, -1.0) * ts) * 0.08334;
|
||||||
|
c += texture2D(m_Texture, texCoord + vec2( 1.0, -1.0) * ts) * 0.05890;
|
||||||
|
c += texture2D(m_Texture, texCoord + vec2( 2.0, -1.0) * ts) * 0.02075;
|
||||||
|
|
||||||
|
c += texture2D(m_Texture, texCoord + vec2(-2.0, 0.0) * ts) * 0.02936;
|
||||||
|
c += texture2D(m_Texture, texCoord + vec2(-1.0, 0.0) * ts) * 0.08334;
|
||||||
|
c += texture2D(m_Texture, texCoord + vec2( 0.0, 0.0) * ts) * 0.11792;
|
||||||
|
c += texture2D(m_Texture, texCoord + vec2( 1.0, 0.0) * ts) * 0.08334;
|
||||||
|
c += texture2D(m_Texture, texCoord + vec2( 2.0, 0.0) * ts) * 0.02936;
|
||||||
|
|
||||||
|
c += texture2D(m_Texture, texCoord + vec2(-2.0, 1.0) * ts) * 0.02075;
|
||||||
|
c += texture2D(m_Texture, texCoord + vec2(-1.0, 1.0) * ts) * 0.05890;
|
||||||
|
c += texture2D(m_Texture, texCoord + vec2( 0.0, 1.0) * ts) * 0.08334;
|
||||||
|
c += texture2D(m_Texture, texCoord + vec2( 1.0, 1.0) * ts) * 0.05890;
|
||||||
|
c += texture2D(m_Texture, texCoord + vec2( 2.0, 1.0) * ts) * 0.02075;
|
||||||
|
|
||||||
|
c += texture2D(m_Texture, texCoord + vec2(-2.0, 2.0) * ts) * 0.00731;
|
||||||
|
c += texture2D(m_Texture, texCoord + vec2(-1.0, 2.0) * ts) * 0.02075;
|
||||||
|
c += texture2D(m_Texture, texCoord + vec2( 0.0, 2.0) * ts) * 0.02936;
|
||||||
|
c += texture2D(m_Texture, texCoord + vec2( 1.0, 2.0) * ts) * 0.02075;
|
||||||
|
c += texture2D(m_Texture, texCoord + vec2( 2.0, 2.0) * ts) * 0.00731;
|
||||||
|
|
||||||
|
gl_FragColor = c; // Gewichte summieren sich auf 1.0
|
||||||
|
}
|
||||||
BIN
blight-assets/src/main/resources/Textures/menu/background.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
blight-assets/src/main/resources/Textures/menu/button.png
Normal file
|
After Width: | Height: | Size: 333 B |
BIN
blight-assets/src/main/resources/Textures/menu/button_arrow.png
Normal file
|
After Width: | Height: | Size: 250 B |
|
After Width: | Height: | Size: 335 B |
BIN
blight-assets/src/main/resources/Textures/menu/button_quit.png
Normal file
|
After Width: | Height: | Size: 333 B |
BIN
blight-assets/src/main/resources/Textures/menu/button_save.png
Normal file
|
After Width: | Height: | Size: 337 B |
BIN
blight-assets/src/main/resources/Textures/menu/item_cell.png
Normal file
|
After Width: | Height: | Size: 410 B |
BIN
blight-assets/src/main/resources/Textures/menu/panel.png
Normal file
|
After Width: | Height: | Size: 428 B |
BIN
blight-assets/src/main/resources/Textures/menu/tab_active.png
Normal file
|
After Width: | Height: | Size: 222 B |
BIN
blight-assets/src/main/resources/Textures/menu/tab_inactive.png
Normal file
|
After Width: | Height: | Size: 226 B |
BIN
blight-assets/src/main/resources/audio/footsteps/dirt/dirt1.ogg
Normal file
BIN
blight-assets/src/main/resources/audio/footsteps/dirt/dirt2.ogg
Normal file
BIN
blight-assets/src/main/resources/audio/footsteps/dirt/dirt3.ogg
Normal file
BIN
blight-assets/src/main/resources/audio/footsteps/dirt/dirt4.ogg
Normal file
BIN
blight-assets/src/main/resources/audio/footsteps/grass/gras1.ogg
Normal file
BIN
blight-assets/src/main/resources/audio/footsteps/grass/gras2.ogg
Normal file
BIN
blight-assets/src/main/resources/audio/footsteps/grass/gras3.ogg
Normal file
BIN
blight-assets/src/main/resources/audio/footsteps/grass/gras4.ogg
Normal file
BIN
blight-assets/src/main/resources/audio/footsteps/rock/rock1.ogg
Normal file
BIN
blight-assets/src/main/resources/audio/footsteps/rock/rock2.ogg
Normal file
BIN
blight-assets/src/main/resources/audio/footsteps/rock/rock3.ogg
Normal file
BIN
blight-assets/src/main/resources/audio/footsteps/rock/rock4.ogg
Normal file
BIN
blight-assets/src/main/resources/audio/footsteps/sand/sand1.ogg
Normal file
BIN
blight-assets/src/main/resources/audio/footsteps/sand/sand2.ogg
Normal file
BIN
blight-assets/src/main/resources/audio/footsteps/sand/sand3.ogg
Normal file
BIN
blight-assets/src/main/resources/audio/footsteps/sand/sand4.ogg
Normal file
BIN
blight-assets/src/main/resources/audio/footsteps/sand/sand5.ogg
Normal file
BIN
blight-assets/src/main/resources/audio/footsteps/steps1.ogg
Normal file
BIN
blight-assets/src/main/resources/audio/footsteps/steps2.ogg
Normal file
BIN
blight-assets/src/main/resources/audio/footsteps/steps3.ogg
Normal file
BIN
blight-assets/src/main/resources/audio/footsteps/steps4.ogg
Normal file
BIN
blight-assets/src/main/resources/audio/footsteps/wood/wood1.ogg
Normal file
BIN
blight-assets/src/main/resources/audio/footsteps/wood/wood2.ogg
Normal file
BIN
blight-assets/src/main/resources/audio/footsteps/wood/wood3.ogg
Normal file
BIN
blight-assets/src/main/resources/audio/footsteps/wood/wood4.ogg
Normal file
@@ -1,5 +1,54 @@
|
|||||||
{
|
{
|
||||||
|
"status": "NEUTRAL",
|
||||||
"trader": false,
|
"trader": false,
|
||||||
|
"currentOptions": [
|
||||||
|
{
|
||||||
|
"id": "5a9821c8-f0f3-49c2-a570-9b68e819b46a",
|
||||||
|
"label": "First Contact",
|
||||||
|
"requiresChapter": 0,
|
||||||
|
"requiresStatus": "NEUTRAL",
|
||||||
|
"textHero": {
|
||||||
|
"id": "Hi! Ich suche einen Weg von diesem Strand."
|
||||||
|
},
|
||||||
|
"textNpc": {
|
||||||
|
"id": "Woher kommst du?"
|
||||||
|
},
|
||||||
|
"nextOptions": [
|
||||||
|
{
|
||||||
|
"id": "f78aa61f-8532-4b64-8f0e-c90ff0e92760",
|
||||||
|
"label": "Test1",
|
||||||
|
"requiresChapter": 0,
|
||||||
|
"textHero": {
|
||||||
|
"id": "test"
|
||||||
|
},
|
||||||
|
"textNpc": {
|
||||||
|
"id": "test"
|
||||||
|
},
|
||||||
|
"nextOptions": [
|
||||||
|
{
|
||||||
|
"id": "0114af85-986f-4923-b77e-88529b868c0f",
|
||||||
|
"label": "test2",
|
||||||
|
"requiresChapter": 0,
|
||||||
|
"textHero": {
|
||||||
|
"id": "test"
|
||||||
|
},
|
||||||
|
"textNpc": {
|
||||||
|
"id": "test"
|
||||||
|
},
|
||||||
|
"abortsQuests": [],
|
||||||
|
"enablesTrade": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"abortsQuests": [],
|
||||||
|
"enablesTrade": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"abortsQuests": [],
|
||||||
|
"enablesTrade": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"defaultMessages": {},
|
||||||
|
"routines": [],
|
||||||
"characterId": "silas",
|
"characterId": "silas",
|
||||||
"name": {
|
"name": {
|
||||||
"id": "silas.name"
|
"id": "silas.name"
|
||||||
|
|||||||
1
blight-assets/src/main/resources/localization/de.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
15
blight-assets/src/main/resources/quests/strandgut.quest
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"xp": 100,
|
||||||
|
"questId": "strandgut",
|
||||||
|
"text": {
|
||||||
|
"id": "strandgut.name"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"id": "strandgut.description"
|
||||||
|
},
|
||||||
|
"successText": {
|
||||||
|
"id": "strandgut.successmassage"
|
||||||
|
},
|
||||||
|
"type": "ITEM"
|
||||||
|
}
|
||||||
@@ -129,6 +129,9 @@ public final class MapData {
|
|||||||
/** Spawnpunkt Z-Koordinate in Welteinheiten (default: 0 = Kartenmitte). */
|
/** Spawnpunkt Z-Koordinate in Welteinheiten (default: 0 = Kartenmitte). */
|
||||||
public float spawnZ = 0f;
|
public float spawnZ = 0f;
|
||||||
|
|
||||||
|
/** Blickrichtung am permanenten Spawnpunkt in Grad (0 = +Z, 90 = +X, Uhrzeigersinn von oben). */
|
||||||
|
public float spawnYaw = 0f;
|
||||||
|
|
||||||
/** Terrain-Slot (0-7) für flache Voxel-Flächen (TexFlat), -1 = nicht gesetzt. */
|
/** Terrain-Slot (0-7) für flache Voxel-Flächen (TexFlat), -1 = nicht gesetzt. */
|
||||||
public int voxelFlatSlot = -1;
|
public int voxelFlatSlot = -1;
|
||||||
/** Terrain-Slot (0-7) für steile Wände (TexSteep), -1 = nicht gesetzt. */
|
/** Terrain-Slot (0-7) für steile Wände (TexSteep), -1 = nicht gesetzt. */
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ public final class MapIO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static final int MAGIC = 0x424C4947; // "BLIG"
|
private static final int MAGIC = 0x424C4947; // "BLIG"
|
||||||
private static final int VERSION = 14;
|
private static final int VERSION = 15;
|
||||||
|
|
||||||
// Größen älterer Saves (v≤9) – für Migrations-Upsampling
|
// Größen älterer Saves (v≤9) – für Migrations-Upsampling
|
||||||
private static final int OLD_TERRAIN_VERTS = 4097;
|
private static final int OLD_TERRAIN_VERTS = 4097;
|
||||||
@@ -124,6 +124,8 @@ public final class MapIO {
|
|||||||
// v4: spawnpunkt
|
// v4: spawnpunkt
|
||||||
out.writeFloat(data.spawnX);
|
out.writeFloat(data.spawnX);
|
||||||
out.writeFloat(data.spawnZ);
|
out.writeFloat(data.spawnZ);
|
||||||
|
// v15: spawn-blickrichtung
|
||||||
|
out.writeFloat(data.spawnYaw);
|
||||||
// v5: splatA + texturpfade + gebirge-splatmap
|
// v5: splatA + texturpfade + gebirge-splatmap
|
||||||
out.write(data.splatA);
|
out.write(data.splatA);
|
||||||
writeStrings(out, data.terrainTextures);
|
writeStrings(out, data.terrainTextures);
|
||||||
@@ -232,6 +234,9 @@ public final class MapIO {
|
|||||||
data.spawnX = in.readFloat();
|
data.spawnX = in.readFloat();
|
||||||
data.spawnZ = in.readFloat();
|
data.spawnZ = in.readFloat();
|
||||||
}
|
}
|
||||||
|
if (version >= 15) {
|
||||||
|
data.spawnYaw = in.readFloat();
|
||||||
|
}
|
||||||
if (version >= 5) {
|
if (version >= 5) {
|
||||||
if (version >= 10) {
|
if (version >= 10) {
|
||||||
in.readFully(data.splatA);
|
in.readFully(data.splatA);
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ public record ModelMeta(
|
|||||||
float interactableOffsetX,
|
float interactableOffsetX,
|
||||||
float interactableOffsetY,
|
float interactableOffsetY,
|
||||||
float interactableOffsetZ,
|
float interactableOffsetZ,
|
||||||
float interactableRotY
|
float interactableRotY,
|
||||||
|
String footstepSurface
|
||||||
) {
|
) {
|
||||||
/** Lichtquelle relativ zum Modell-Ursprung. */
|
/** Lichtquelle relativ zum Modell-Ursprung. */
|
||||||
public record AttachedLight(
|
public record AttachedLight(
|
||||||
@@ -52,6 +53,6 @@ public record ModelMeta(
|
|||||||
"", "", 30f, 80f, 120f,
|
"", "", 30f, 80f, 120f,
|
||||||
List.of(), List.of(),
|
List.of(), List.of(),
|
||||||
de.blight.common.model.InteractableType.NONE,
|
de.blight.common.model.InteractableType.NONE,
|
||||||
0f, 0.5f, 0f, 0f);
|
0f, 0.5f, 0f, 0f, "");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ public final class ModelMetaIO {
|
|||||||
p.setProperty("interactableOffsetY", String.valueOf(m.interactableOffsetY()));
|
p.setProperty("interactableOffsetY", String.valueOf(m.interactableOffsetY()));
|
||||||
p.setProperty("interactableOffsetZ", String.valueOf(m.interactableOffsetZ()));
|
p.setProperty("interactableOffsetZ", String.valueOf(m.interactableOffsetZ()));
|
||||||
p.setProperty("interactableRotY", String.valueOf(m.interactableRotY()));
|
p.setProperty("interactableRotY", String.valueOf(m.interactableRotY()));
|
||||||
|
p.setProperty("footstepSurface", m.footstepSurface() != null ? m.footstepSurface() : "");
|
||||||
|
|
||||||
// Anhänge: Lichter
|
// Anhänge: Lichter
|
||||||
List<ModelMeta.AttachedLight> lights = m.attachedLights();
|
List<ModelMeta.AttachedLight> lights = m.attachedLights();
|
||||||
@@ -138,7 +139,8 @@ public final class ModelMetaIO {
|
|||||||
parseFloat(p, "interactableOffsetX", 0f),
|
parseFloat(p, "interactableOffsetX", 0f),
|
||||||
parseFloat(p, "interactableOffsetY", 0.5f),
|
parseFloat(p, "interactableOffsetY", 0.5f),
|
||||||
parseFloat(p, "interactableOffsetZ", 0f),
|
parseFloat(p, "interactableOffsetZ", 0f),
|
||||||
parseFloat(p, "interactableRotY", 0f)
|
parseFloat(p, "interactableRotY", 0f),
|
||||||
|
p.getProperty("footstepSurface", "")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,4 +40,10 @@ public class Bed implements Interactable {
|
|||||||
public String getDisplayText() {
|
public String getDisplayText() {
|
||||||
return TextRegistry.resolve(name, id != null ? id : "Bett");
|
return TextRegistry.resolve(name, id != null ? id : "Bett");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getLabelKey() {
|
||||||
|
if (name != null && name.id() != null && !name.id().isBlank()) return name.id();
|
||||||
|
return "interactable.bed.name";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,4 +41,10 @@ public class Bench implements Interactable {
|
|||||||
public String getDisplayText() {
|
public String getDisplayText() {
|
||||||
return TextRegistry.resolve(name, id != null ? id : "Bank");
|
return TextRegistry.resolve(name, id != null ? id : "Bank");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getLabelKey() {
|
||||||
|
if (name != null && name.id() != null && !name.id().isBlank()) return name.id();
|
||||||
|
return "interactable.bench.name";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,8 @@ package de.blight.common.model;
|
|||||||
|
|
||||||
public interface Interactable {
|
public interface Interactable {
|
||||||
|
|
||||||
public String getDisplayText();
|
String getDisplayText();
|
||||||
|
|
||||||
|
/** Gibt den Lokalisierungsschlüssel für das HUD-Label zurück (z.B. "item.driftwood.name"). */
|
||||||
|
default String getLabelKey() { return ""; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,4 +30,10 @@ public class Item implements Interactable {
|
|||||||
public String getDisplayText() {
|
public String getDisplayText() {
|
||||||
return TextRegistry.resolve(name, itemId != null ? itemId : "?");
|
return TextRegistry.resolve(name, itemId != null ? itemId : "?");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getLabelKey() {
|
||||||
|
if (name != null && name.id() != null && !name.id().isBlank()) return name.id();
|
||||||
|
return itemId != null ? "item." + itemId + ".name" : "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package de.blight.common.model;
|
package de.blight.common.model;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.EnumMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@@ -11,7 +13,7 @@ import lombok.Setter;
|
|||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
public class NPC extends GameCharacter {
|
public class NPC extends GameCharacter implements Interactable {
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(NPC.class);
|
private static final Logger LOG = LoggerFactory.getLogger(NPC.class);
|
||||||
|
|
||||||
@@ -23,6 +25,15 @@ public class NPC extends GameCharacter {
|
|||||||
|
|
||||||
private List<DialogOption> currentOptions;
|
private List<DialogOption> currentOptions;
|
||||||
|
|
||||||
|
/** Nur im Editor relevant: Optionen die nicht mit currentOptions verbunden sind, aber erhalten werden sollen. */
|
||||||
|
private List<DialogOption> editorOnlyOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standard-Nachrichten je Status, die angezeigt werden wenn keine Dialog-Optionen
|
||||||
|
* verfügbar sind. Schlüssel = Status-Wert des NPCs zum Gesprächszeitpunkt.
|
||||||
|
*/
|
||||||
|
private Map<Status, TextReference> defaultMessages = new EnumMap<>(Status.class);
|
||||||
|
|
||||||
/** Tagesabläufe dieses NPCs. Die erste Routine gilt standardmäßig als aktiv. */
|
/** Tagesabläufe dieses NPCs. Die erste Routine gilt standardmäßig als aktiv. */
|
||||||
private List<NpcRoutine> routines = new ArrayList<>();
|
private List<NpcRoutine> routines = new ArrayList<>();
|
||||||
|
|
||||||
@@ -58,6 +69,35 @@ public class NPC extends GameCharacter {
|
|||||||
this.activeRoutineName = routineName;
|
this.activeRoutineName = routineName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Interactable ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDisplayText() {
|
||||||
|
return TextRegistry.resolve(getName(), getCharacterId() != null ? getCharacterId() : "NPC");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getLabelKey() {
|
||||||
|
TextReference nameRef = getName();
|
||||||
|
if (nameRef != null && nameRef.id() != null && !nameRef.id().isBlank()) return nameRef.id();
|
||||||
|
return getCharacterId() != null ? "npc." + getCharacterId() + ".name" : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt die Default-Nachricht für den aktuellen Status zurück.
|
||||||
|
* Fällt auf den nächst-freundlicheren Status zurück wenn kein Eintrag gefunden wird.
|
||||||
|
*/
|
||||||
|
public TextReference getDefaultMessage() {
|
||||||
|
if (defaultMessages == null || defaultMessages.isEmpty()) return null;
|
||||||
|
Status s = status != null ? status : Status.NEUTRAL;
|
||||||
|
Status[] fallback = {s, Status.NEUTRAL, Status.FRIENDLY, Status.ENRAGED, Status.ENEMY};
|
||||||
|
for (Status candidate : fallback) {
|
||||||
|
TextReference ref = defaultMessages.get(candidate);
|
||||||
|
if (ref != null) return ref;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Dialog-Methoden ──────────────────────────────────────────────────────
|
// ── Dialog-Methoden ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
public List<DialogOption> getAvailableOptions(MainCharacter character) {
|
public List<DialogOption> getAvailableOptions(MainCharacter character) {
|
||||||
|
|||||||
@@ -0,0 +1,257 @@
|
|||||||
|
package de.blight.editor;
|
||||||
|
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.scene.control.Button;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.ToggleButton;
|
||||||
|
import javafx.scene.control.Tooltip;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
import javafx.stage.StageStyle;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import javax.sound.sampled.AudioFormat;
|
||||||
|
import javax.sound.sampled.AudioInputStream;
|
||||||
|
import javax.sound.sampled.AudioSystem;
|
||||||
|
import javax.sound.sampled.Clip;
|
||||||
|
import javax.sound.sampled.LineEvent;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nicht-modales Popup zum Vorhören von OGG-Dateien.
|
||||||
|
* Nutzt javax.sound.sampled + j-ogg-vorbis SPI (bereits transitiv über jme3-jogg
|
||||||
|
* im Classpath). Kein GStreamer, kein javafx.media nötig.
|
||||||
|
*/
|
||||||
|
public class AudioPreviewPopup {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(AudioPreviewPopup.class);
|
||||||
|
|
||||||
|
private Stage stage;
|
||||||
|
private Clip clip;
|
||||||
|
private boolean paused = false;
|
||||||
|
private boolean repeat = false;
|
||||||
|
|
||||||
|
private List<Path> playlist = Collections.emptyList();
|
||||||
|
private int idx = 0;
|
||||||
|
|
||||||
|
private Label fileLabel;
|
||||||
|
private Label folderLabel;
|
||||||
|
private Button playPauseBtn;
|
||||||
|
private ToggleButton repeatBtn;
|
||||||
|
|
||||||
|
// ── API ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Öffnet das Popup (oder bringt es in den Vordergrund) und lädt die Datei. */
|
||||||
|
public void open(Path file) {
|
||||||
|
if (stage == null) buildStage();
|
||||||
|
loadFile(file);
|
||||||
|
if (!stage.isShowing()) stage.show();
|
||||||
|
stage.toFront();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Intern ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void loadFile(Path file) {
|
||||||
|
buildPlaylist(file.getParent());
|
||||||
|
idx = playlist.indexOf(file);
|
||||||
|
if (idx < 0) idx = 0;
|
||||||
|
playIndex(idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildPlaylist(Path folder) {
|
||||||
|
if (folder == null) { playlist = Collections.emptyList(); return; }
|
||||||
|
try (var s = Files.list(folder)) {
|
||||||
|
playlist = s.filter(p -> Files.isRegularFile(p)
|
||||||
|
&& p.getFileName().toString().toLowerCase().endsWith(".ogg"))
|
||||||
|
.sorted()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("[AudioPreview] Ordner nicht lesbar: {}", folder);
|
||||||
|
playlist = Collections.emptyList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void navigate(int delta) {
|
||||||
|
if (playlist.isEmpty()) return;
|
||||||
|
idx = (idx + delta + playlist.size()) % playlist.size();
|
||||||
|
playIndex(idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void playIndex(int i) {
|
||||||
|
if (playlist.isEmpty()) return;
|
||||||
|
Path file = playlist.get(i);
|
||||||
|
|
||||||
|
stopClip();
|
||||||
|
fileLabel.setText(file.getFileName().toString());
|
||||||
|
folderLabel.setText(file.getParent() != null ? shortenPath(file.getParent()) : "");
|
||||||
|
playPauseBtn.setText("⏳");
|
||||||
|
playPauseBtn.setDisable(true);
|
||||||
|
|
||||||
|
Thread t = new Thread(() -> {
|
||||||
|
try {
|
||||||
|
AudioInputStream raw = AudioSystem.getAudioInputStream(file.toFile());
|
||||||
|
AudioFormat src = raw.getFormat();
|
||||||
|
float rate = src.getSampleRate() < 0 ? 44100f : src.getSampleRate();
|
||||||
|
int ch = src.getChannels() < 0 ? 1 : src.getChannels();
|
||||||
|
AudioFormat pcm = new AudioFormat(
|
||||||
|
AudioFormat.Encoding.PCM_SIGNED,
|
||||||
|
rate, 16, ch, ch * 2, rate, false);
|
||||||
|
AudioInputStream pcmStream = AudioSystem.getAudioInputStream(pcm, raw);
|
||||||
|
Clip newClip = AudioSystem.getClip();
|
||||||
|
newClip.open(pcmStream);
|
||||||
|
|
||||||
|
newClip.addLineListener(ev -> {
|
||||||
|
if (ev.getType() == LineEvent.Type.STOP && !paused && !repeat) {
|
||||||
|
Platform.runLater(() -> playPauseBtn.setText("▶"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
clip = newClip;
|
||||||
|
paused = false;
|
||||||
|
playPauseBtn.setDisable(false);
|
||||||
|
if (repeat) {
|
||||||
|
clip.loop(Clip.LOOP_CONTINUOUSLY);
|
||||||
|
} else {
|
||||||
|
clip.start();
|
||||||
|
}
|
||||||
|
playPauseBtn.setText("⏸");
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[AudioPreview] Nicht ladbar '{}': {}", file.getFileName(), e.getMessage());
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
fileLabel.setText("⚠ " + file.getFileName());
|
||||||
|
playPauseBtn.setText("▶");
|
||||||
|
playPauseBtn.setDisable(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, "audio-preview-load");
|
||||||
|
t.setDaemon(true);
|
||||||
|
t.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void togglePlay() {
|
||||||
|
if (clip == null) {
|
||||||
|
playIndex(idx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (clip.isRunning()) {
|
||||||
|
clip.stop();
|
||||||
|
paused = true;
|
||||||
|
playPauseBtn.setText("▶");
|
||||||
|
} else {
|
||||||
|
clip.start();
|
||||||
|
paused = false;
|
||||||
|
playPauseBtn.setText("⏸");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stopClip() {
|
||||||
|
if (clip != null) {
|
||||||
|
clip.stop();
|
||||||
|
clip.close();
|
||||||
|
clip = null;
|
||||||
|
}
|
||||||
|
paused = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onRepeatChanged(boolean on) {
|
||||||
|
repeat = on;
|
||||||
|
if (clip == null) return;
|
||||||
|
if (on) {
|
||||||
|
clip.loop(Clip.LOOP_CONTINUOUSLY);
|
||||||
|
} else {
|
||||||
|
clip.loop(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── UI-Aufbau ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void buildStage() {
|
||||||
|
stage = new Stage(StageStyle.UTILITY);
|
||||||
|
stage.setTitle("Audio-Vorschau");
|
||||||
|
stage.setAlwaysOnTop(true);
|
||||||
|
stage.setResizable(false);
|
||||||
|
stage.setOnCloseRequest(e -> stopClip());
|
||||||
|
|
||||||
|
fileLabel = new Label("–");
|
||||||
|
fileLabel.setStyle("-fx-font-weight:bold; -fx-text-fill:#eee; -fx-font-size:13;");
|
||||||
|
fileLabel.setWrapText(true);
|
||||||
|
fileLabel.setMaxWidth(340);
|
||||||
|
|
||||||
|
folderLabel = new Label("");
|
||||||
|
folderLabel.setStyle("-fx-text-fill:#777; -fx-font-size:10;");
|
||||||
|
folderLabel.setMaxWidth(340);
|
||||||
|
folderLabel.setWrapText(true);
|
||||||
|
|
||||||
|
Button prevBtn = makeBtn("◀", "Vorherige Datei", () -> navigate(-1));
|
||||||
|
playPauseBtn = makeBtn("▶", "Abspielen / Pausieren", this::togglePlay);
|
||||||
|
Button nextBtn = makeBtn("▶▶", "Nächste Datei", () -> navigate(+1));
|
||||||
|
|
||||||
|
repeatBtn = new ToggleButton("🔁");
|
||||||
|
repeatBtn.setTooltip(new Tooltip("Wiederholen"));
|
||||||
|
styleToggle(repeatBtn, false);
|
||||||
|
repeatBtn.selectedProperty().addListener((o, ov, nv) -> {
|
||||||
|
styleToggle(repeatBtn, nv);
|
||||||
|
onRepeatChanged(nv);
|
||||||
|
});
|
||||||
|
|
||||||
|
HBox controls = new HBox(6, prevBtn, playPauseBtn, nextBtn, repeatBtn);
|
||||||
|
controls.setAlignment(Pos.CENTER);
|
||||||
|
controls.setPadding(new Insets(8, 0, 4, 0));
|
||||||
|
|
||||||
|
VBox root = new VBox(6, fileLabel, folderLabel, controls);
|
||||||
|
root.setPadding(new Insets(12));
|
||||||
|
root.setStyle("-fx-background-color:#2a2a2a;");
|
||||||
|
root.setPrefWidth(360);
|
||||||
|
|
||||||
|
stage.setScene(new Scene(root));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Button makeBtn(String text, String tip, Runnable action) {
|
||||||
|
Button b = new Button(text);
|
||||||
|
b.setTooltip(new Tooltip(tip));
|
||||||
|
styleBtn(b, false, false);
|
||||||
|
b.setOnMouseEntered(e -> styleBtn(b, true, false));
|
||||||
|
b.setOnMouseExited(e -> styleBtn(b, false, false));
|
||||||
|
b.setOnMousePressed(e -> styleBtn(b, true, true));
|
||||||
|
b.setOnMouseReleased(e -> styleBtn(b, true, false));
|
||||||
|
b.setOnAction(e -> action.run());
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void styleBtn(Button b, boolean hover, boolean pressed) {
|
||||||
|
String bg = pressed ? "#606060" : hover ? "#4d4d4d" : "#3a3a3a";
|
||||||
|
b.setStyle("-fx-background-color:" + bg + ";" +
|
||||||
|
"-fx-text-fill:#eee;" +
|
||||||
|
"-fx-min-width:36; -fx-min-height:30;" +
|
||||||
|
"-fx-border-color:#555; -fx-border-width:1; -fx-border-radius:3;" +
|
||||||
|
"-fx-background-radius:3; -fx-cursor:hand;");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void styleToggle(ToggleButton b, boolean active) {
|
||||||
|
String bg = active ? "#1565c0" : "#3a3a3a";
|
||||||
|
String border = active ? "#1976d2" : "#555";
|
||||||
|
b.setStyle("-fx-background-color:" + bg + ";" +
|
||||||
|
"-fx-text-fill:#eee;" +
|
||||||
|
"-fx-min-width:36; -fx-min-height:30;" +
|
||||||
|
"-fx-border-color:" + border + "; -fx-border-width:1; -fx-border-radius:3;" +
|
||||||
|
"-fx-background-radius:3; -fx-cursor:hand;");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String shortenPath(Path p) {
|
||||||
|
String s = p.toString();
|
||||||
|
int audio = s.indexOf("/audio/");
|
||||||
|
return audio >= 0 ? s.substring(audio) : s;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -88,6 +88,7 @@ public class EditorApp extends Application {
|
|||||||
private StackPane plantPreviewPanel; // einmaliges Vorschau-Panel für alle Generatoren
|
private StackPane plantPreviewPanel; // einmaliges Vorschau-Panel für alle Generatoren
|
||||||
private Stage primaryStage;
|
private Stage primaryStage;
|
||||||
private JmeEditorApp jmeApp;
|
private JmeEditorApp jmeApp;
|
||||||
|
private AudioPreviewPopup audioPreviewPopup;
|
||||||
|
|
||||||
// Baum-Generator-Zustand (wird beim Preset-Wechsel neu gesetzt)
|
// Baum-Generator-Zustand (wird beim Preset-Wechsel neu gesetzt)
|
||||||
private TreeParams treeParams = TreeParams.oak();
|
private TreeParams treeParams = TreeParams.oak();
|
||||||
@@ -155,6 +156,7 @@ public class EditorApp extends Application {
|
|||||||
private String animSetPendingPlayClip = null;
|
private String animSetPendingPlayClip = null;
|
||||||
private ComboBox<String> animSetModelCombo;
|
private ComboBox<String> animSetModelCombo;
|
||||||
private boolean animSetDirty = false;
|
private boolean animSetDirty = false;
|
||||||
|
private boolean charDirty = false;
|
||||||
private String animSetCurrentName = null;
|
private String animSetCurrentName = null;
|
||||||
private Path animSetCurrentDir = null;
|
private Path animSetCurrentDir = null;
|
||||||
// Anim-Offset-Editor (innerhalb AnimSet-Editor)
|
// Anim-Offset-Editor (innerhalb AnimSet-Editor)
|
||||||
@@ -163,6 +165,10 @@ public class EditorApp extends Application {
|
|||||||
javafx.collections.FXCollections.observableArrayList();
|
javafx.collections.FXCollections.observableArrayList();
|
||||||
private java.util.Map<String, de.blight.game.animation.AnimOffset>
|
private java.util.Map<String, de.blight.game.animation.AnimOffset>
|
||||||
animSetOffsets = new java.util.LinkedHashMap<>();
|
animSetOffsets = new java.util.LinkedHashMap<>();
|
||||||
|
// Sub-Clip-Editor (innerhalb AnimSet-Editor)
|
||||||
|
private ListView<String> animSetPartsListView;
|
||||||
|
private java.util.Map<String, de.blight.game.animation.AnimSet.SubClipDef>
|
||||||
|
editableSubClips = new java.util.LinkedHashMap<>();
|
||||||
|
|
||||||
// Character-Editor-Zustand
|
// Character-Editor-Zustand
|
||||||
private de.blight.editor.ui.DialogEditorView dialogEditorView;
|
private de.blight.editor.ui.DialogEditorView dialogEditorView;
|
||||||
@@ -173,6 +179,11 @@ public class EditorApp extends Application {
|
|||||||
private javafx.scene.control.ComboBox<String> charStatusCombo;
|
private javafx.scene.control.ComboBox<String> charStatusCombo;
|
||||||
private javafx.scene.control.CheckBox charTraderCheck;
|
private javafx.scene.control.CheckBox charTraderCheck;
|
||||||
private javafx.scene.control.ListView<String> charTraderItemsView;
|
private javafx.scene.control.ListView<String> charTraderItemsView;
|
||||||
|
/** Default-Nachrichten je NPC-Status (TextReference-Schlüssel). */
|
||||||
|
private javafx.scene.control.TextField charMsgFriendly;
|
||||||
|
private javafx.scene.control.TextField charMsgNeutral;
|
||||||
|
private javafx.scene.control.TextField charMsgEnraged;
|
||||||
|
private javafx.scene.control.TextField charMsgEnemy;
|
||||||
// Abilities (MainCharacter)
|
// Abilities (MainCharacter)
|
||||||
private javafx.scene.control.Spinner<Integer> abMagicSpin, abStaffSpin, abSwordSpin,
|
private javafx.scene.control.Spinner<Integer> abMagicSpin, abStaffSpin, abSwordSpin,
|
||||||
abArcherySpin, abHeavySpin, abCrossbowSpin, abThieverySpin,
|
abArcherySpin, abHeavySpin, abCrossbowSpin, abThieverySpin,
|
||||||
@@ -211,6 +222,11 @@ public class EditorApp extends Application {
|
|||||||
// Spiel-Starten-Werkzeug-Zustand
|
// Spiel-Starten-Werkzeug-Zustand
|
||||||
private TextField spawnXField;
|
private TextField spawnXField;
|
||||||
private TextField spawnZField;
|
private TextField spawnZField;
|
||||||
|
private Label tempSpawnCoordsLabel;
|
||||||
|
private Label permSpawnCoordsLabel;
|
||||||
|
private Button gameNewBtn;
|
||||||
|
private boolean launchNewGameAfterSave = false;
|
||||||
|
private boolean pendingNewGame = false;
|
||||||
|
|
||||||
// Baum-Ordner-Modus
|
// Baum-Ordner-Modus
|
||||||
private Label randomTreeStatusLabel;
|
private Label randomTreeStatusLabel;
|
||||||
@@ -249,6 +265,7 @@ public class EditorApp extends Application {
|
|||||||
private Spinner<Double> modelEditorInteractableXSpin = null;
|
private Spinner<Double> modelEditorInteractableXSpin = null;
|
||||||
private Spinner<Double> modelEditorInteractableYSpin = null;
|
private Spinner<Double> modelEditorInteractableYSpin = null;
|
||||||
private Spinner<Double> modelEditorInteractableZSpin = null;
|
private Spinner<Double> modelEditorInteractableZSpin = null;
|
||||||
|
private ComboBox<String> modelEditorFootstepSurfaceCB = null;
|
||||||
private boolean updatingInteractableSpinnersFromJme = false;
|
private boolean updatingInteractableSpinnersFromJme = false;
|
||||||
|
|
||||||
// Modell-Import-Zustand
|
// Modell-Import-Zustand
|
||||||
@@ -396,6 +413,16 @@ public class EditorApp extends Application {
|
|||||||
stage.setMinWidth(900);
|
stage.setMinWidth(900);
|
||||||
stage.setMinHeight(600);
|
stage.setMinHeight(600);
|
||||||
stage.setOnCloseRequest(e -> {
|
stage.setOnCloseRequest(e -> {
|
||||||
|
if (animSetDirty || charDirty) {
|
||||||
|
Alert confirm = new Alert(Alert.AlertType.CONFIRMATION,
|
||||||
|
"Es gibt ungespeicherte Änderungen.\nTrotzdem beenden?",
|
||||||
|
ButtonType.YES, ButtonType.NO);
|
||||||
|
confirm.setHeaderText("Ungespeicherte Änderungen");
|
||||||
|
confirm.showAndWait().ifPresent(btn -> {
|
||||||
|
if (btn != ButtonType.YES) e.consume();
|
||||||
|
});
|
||||||
|
if (e.isConsumed()) return;
|
||||||
|
}
|
||||||
saveCameraPrefs();
|
saveCameraPrefs();
|
||||||
if (jmeApp != null) jmeApp.stop();
|
if (jmeApp != null) jmeApp.stop();
|
||||||
Platform.exit();
|
Platform.exit();
|
||||||
@@ -603,6 +630,11 @@ public class EditorApp extends Application {
|
|||||||
updateSpawnFields(input.pickedSpawnInfo);
|
updateSpawnFields(input.pickedSpawnInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (input.permSpawnChanged) {
|
||||||
|
input.permSpawnChanged = false;
|
||||||
|
if (permSpawnCoordsLabel != null) permSpawnCoordsLabel.setText(permSpawnCoordsText());
|
||||||
|
}
|
||||||
|
|
||||||
// Modell-Editor: gebakte Scale aus j3o erkannt → Spinner aktualisieren
|
// Modell-Editor: gebakte Scale aus j3o erkannt → Spinner aktualisieren
|
||||||
if (input.modelEditorBakedScaleDetected) {
|
if (input.modelEditorBakedScaleDetected) {
|
||||||
input.modelEditorBakedScaleDetected = false;
|
input.modelEditorBakedScaleDetected = false;
|
||||||
@@ -3972,6 +4004,10 @@ public class EditorApp extends Application {
|
|||||||
switchToAnimPreview();
|
switchToAnimPreview();
|
||||||
input.animPreviewLoadPath = relPath;
|
input.animPreviewLoadPath = relPath;
|
||||||
if (animPreviewStatusLabel != null) animPreviewStatusLabel.setText("Lade…");
|
if (animPreviewStatusLabel != null) animPreviewStatusLabel.setText("Lade…");
|
||||||
|
} else if (cat == audioNode && relPath.endsWith(".ogg")) {
|
||||||
|
if (audioPreviewPopup == null) audioPreviewPopup = new AudioPreviewPopup();
|
||||||
|
audioPreviewPopup.open(p);
|
||||||
|
setStatus("▶ " + relPath);
|
||||||
} else if (relPath.endsWith(".animset.json")) {
|
} else if (relPath.endsWith(".animset.json")) {
|
||||||
openAnimSetEditor(relPath, p);
|
openAnimSetEditor(relPath, p);
|
||||||
} else if (relPath.endsWith(".character")) {
|
} else if (relPath.endsWith(".character")) {
|
||||||
@@ -4812,7 +4848,12 @@ public class EditorApp extends Application {
|
|||||||
/** Liest die AnimClip-Namen aus einer J3O-Datei, ohne den JME3-Thread zu benötigen. */
|
/** Liest die AnimClip-Namen aus einer J3O-Datei, ohne den JME3-Thread zu benötigen. */
|
||||||
private List<String> readAnimClipNames(Path j3oPath) {
|
private List<String> readAnimClipNames(Path j3oPath) {
|
||||||
try {
|
try {
|
||||||
|
com.jme3.asset.DesktopAssetManager assetManager = new com.jme3.asset.DesktopAssetManager(true);
|
||||||
|
assetManager.registerLocator(
|
||||||
|
de.blight.game.animation.AnimationLibrary.findAssetRoot().toAbsolutePath().toString(),
|
||||||
|
com.jme3.asset.plugins.FileLocator.class);
|
||||||
com.jme3.export.binary.BinaryImporter imp = new com.jme3.export.binary.BinaryImporter();
|
com.jme3.export.binary.BinaryImporter imp = new com.jme3.export.binary.BinaryImporter();
|
||||||
|
imp.setAssetManager(assetManager);
|
||||||
com.jme3.scene.Spatial s = (com.jme3.scene.Spatial) imp.load(j3oPath.toFile());
|
com.jme3.scene.Spatial s = (com.jme3.scene.Spatial) imp.load(j3oPath.toFile());
|
||||||
com.jme3.anim.AnimComposer ac =
|
com.jme3.anim.AnimComposer ac =
|
||||||
de.blight.game.animation.RetargetingSystem.findAnimComposer(s);
|
de.blight.game.animation.RetargetingSystem.findAnimComposer(s);
|
||||||
@@ -6134,6 +6175,26 @@ public class EditorApp extends Application {
|
|||||||
input.modelInteractableOffsetChanged = true;
|
input.modelInteractableOffsetChanged = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Fußgeräusch-Untergrund ────────────────────────────────────────────
|
||||||
|
Label footstepTitle = new Label("Fußgeräusch-Untergrund:");
|
||||||
|
footstepTitle.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;");
|
||||||
|
|
||||||
|
modelEditorFootstepSurfaceCB = new ComboBox<>();
|
||||||
|
modelEditorFootstepSurfaceCB.getItems().add("");
|
||||||
|
for (de.blight.game.audio.SurfaceType st : de.blight.game.audio.SurfaceType.values()) {
|
||||||
|
modelEditorFootstepSurfaceCB.getItems().add(st.name());
|
||||||
|
}
|
||||||
|
String currentSurface = meta.footstepSurface() != null ? meta.footstepSurface() : "";
|
||||||
|
modelEditorFootstepSurfaceCB.setValue(
|
||||||
|
modelEditorFootstepSurfaceCB.getItems().contains(currentSurface) ? currentSurface : "");
|
||||||
|
modelEditorFootstepSurfaceCB.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
modelEditorFootstepSurfaceCB.setConverter(new javafx.util.StringConverter<>() {
|
||||||
|
@Override public String toString(String s) {
|
||||||
|
return (s == null || s.isEmpty()) ? "(aus Textur)" : s;
|
||||||
|
}
|
||||||
|
@Override public String fromString(String s) { return s; }
|
||||||
|
});
|
||||||
|
|
||||||
// ── Buttons ───────────────────────────────────────────────────────────
|
// ── Buttons ───────────────────────────────────────────────────────────
|
||||||
Button saveBtn = new Button("💾 Speichern");
|
Button saveBtn = new Button("💾 Speichern");
|
||||||
saveBtn.setMaxWidth(Double.MAX_VALUE);
|
saveBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
@@ -6172,7 +6233,10 @@ public class EditorApp extends Application {
|
|||||||
input.modelInteractableOffsetX,
|
input.modelInteractableOffsetX,
|
||||||
input.modelInteractableOffsetY,
|
input.modelInteractableOffsetY,
|
||||||
input.modelInteractableOffsetZ,
|
input.modelInteractableOffsetZ,
|
||||||
input.modelInteractableRotY));
|
input.modelInteractableRotY,
|
||||||
|
modelEditorFootstepSurfaceCB != null
|
||||||
|
? modelEditorFootstepSurfaceCB.getValue()
|
||||||
|
: ""));
|
||||||
|
|
||||||
placeBtn.setOnAction(e -> {
|
placeBtn.setOnAction(e -> {
|
||||||
input.modelEditorCloseRequest = true;
|
input.modelEditorCloseRequest = true;
|
||||||
@@ -6215,6 +6279,8 @@ public class EditorApp extends Application {
|
|||||||
new Separator(),
|
new Separator(),
|
||||||
interactTitle, modelEditorInteractableCB, restPointBox,
|
interactTitle, modelEditorInteractableCB, restPointBox,
|
||||||
new Separator(),
|
new Separator(),
|
||||||
|
footstepTitle, modelEditorFootstepSurfaceCB,
|
||||||
|
new Separator(),
|
||||||
saveBtn, placeBtn, closeBtn
|
saveBtn, placeBtn, closeBtn
|
||||||
);
|
);
|
||||||
return panel;
|
return panel;
|
||||||
@@ -6667,7 +6733,8 @@ public class EditorApp extends Application {
|
|||||||
java.util.List<de.blight.common.ModelMeta.AttachedEmitter> emitters,
|
java.util.List<de.blight.common.ModelMeta.AttachedEmitter> emitters,
|
||||||
de.blight.common.model.InteractableType interactableType,
|
de.blight.common.model.InteractableType interactableType,
|
||||||
float interactableOffsetX, float interactableOffsetY,
|
float interactableOffsetX, float interactableOffsetY,
|
||||||
float interactableOffsetZ, float interactableRotY) {
|
float interactableOffsetZ, float interactableRotY,
|
||||||
|
String footstepSurface) {
|
||||||
// Scale wird in j3o eingebrannt → Meta bekommt immer 1.0 (kein doppelter Scale beim Laden)
|
// Scale wird in j3o eingebrannt → Meta bekommt immer 1.0 (kein doppelter Scale beim Laden)
|
||||||
de.blight.common.ModelMeta meta = new de.blight.common.ModelMeta(
|
de.blight.common.ModelMeta meta = new de.blight.common.ModelMeta(
|
||||||
name, category, tags, 1f, 1f, 1f, uniform,
|
name, category, tags, 1f, 1f, 1f, uniform,
|
||||||
@@ -6675,7 +6742,8 @@ public class EditorApp extends Application {
|
|||||||
lod1Path, lod2Path, 30f, 80f, 120f,
|
lod1Path, lod2Path, 30f, 80f, 120f,
|
||||||
lights, emitters,
|
lights, emitters,
|
||||||
interactableType != null ? interactableType : de.blight.common.model.InteractableType.NONE,
|
interactableType != null ? interactableType : de.blight.common.model.InteractableType.NONE,
|
||||||
interactableOffsetX, interactableOffsetY, interactableOffsetZ, interactableRotY);
|
interactableOffsetX, interactableOffsetY, interactableOffsetZ, interactableRotY,
|
||||||
|
footstepSurface != null ? footstepSurface : "");
|
||||||
|
|
||||||
if (absolutePath == null || !absolutePath.toFile().exists()) {
|
if (absolutePath == null || !absolutePath.toFile().exists()) {
|
||||||
setStatus("Fehler: Modell-Datei nicht gefunden – Meta nicht gespeichert");
|
setStatus("Fehler: Modell-Datei nicht gefunden – Meta nicht gespeichert");
|
||||||
@@ -7008,9 +7076,15 @@ public class EditorApp extends Application {
|
|||||||
if (bothDown) {
|
if (bothDown) {
|
||||||
stopEditTimer();
|
stopEditTimer();
|
||||||
} else if (e.getButton() == MouseButton.PRIMARY) {
|
} else if (e.getButton() == MouseButton.PRIMARY) {
|
||||||
editPressX = e.getX(); editPressY = e.getY(); editPressAction = +1;
|
if (input.activeLayer == SharedInput.LAYER_PLAY_TOOL) {
|
||||||
submitEdit(editPressX, editPressY, editPressAction);
|
// Einzel-Klick ohne Edit-Timer (verhindert Dauer-Spam)
|
||||||
startEditTimer();
|
input.playToolClickQueue.offer(
|
||||||
|
new SharedInput.PlayToolClick((float) e.getX(), (float) e.getY()));
|
||||||
|
} else {
|
||||||
|
editPressX = e.getX(); editPressY = e.getY(); editPressAction = +1;
|
||||||
|
submitEdit(editPressX, editPressY, editPressAction);
|
||||||
|
startEditTimer();
|
||||||
|
}
|
||||||
} else if (e.getButton() == MouseButton.SECONDARY) {
|
} else if (e.getButton() == MouseButton.SECONDARY) {
|
||||||
editPressX = e.getX(); editPressY = e.getY(); editPressAction = -1;
|
editPressX = e.getX(); editPressY = e.getY(); editPressAction = -1;
|
||||||
submitEdit(editPressX, editPressY, editPressAction);
|
submitEdit(editPressX, editPressY, editPressAction);
|
||||||
@@ -7030,6 +7104,15 @@ public class EditorApp extends Application {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Play-Tool EDIT: Drag-Events an JME3 weiterleiten
|
||||||
|
if (input.activeLayer == SharedInput.LAYER_PLAY_TOOL
|
||||||
|
&& input.playToolMode == SharedInput.PlayToolMode.EDIT
|
||||||
|
&& e.isPrimaryButtonDown()) {
|
||||||
|
input.playToolDragQueue.offer(
|
||||||
|
new SharedInput.PlayToolDrag((float) e.getX(), (float) e.getY()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (e.isPrimaryButtonDown() || e.isSecondaryButtonDown()) {
|
if (e.isPrimaryButtonDown() || e.isSecondaryButtonDown()) {
|
||||||
editPressX = e.getX();
|
editPressX = e.getX();
|
||||||
editPressY = e.getY();
|
editPressY = e.getY();
|
||||||
@@ -7041,6 +7124,9 @@ public class EditorApp extends Application {
|
|||||||
viewport.setOnMouseReleased(e -> {
|
viewport.setOnMouseReleased(e -> {
|
||||||
objDragging = false;
|
objDragging = false;
|
||||||
if (!isObjectMode()) stopEditTimer();
|
if (!isObjectMode()) stopEditTimer();
|
||||||
|
if (input.activeLayer == SharedInput.LAYER_PLAY_TOOL) {
|
||||||
|
input.playToolMouseUp = true;
|
||||||
|
}
|
||||||
if (input.vertexSnapEnabled
|
if (input.vertexSnapEnabled
|
||||||
&& input.objectSelectionMode == SharedInput.SEL_MODE_VERTEX) {
|
&& input.objectSelectionMode == SharedInput.SEL_MODE_VERTEX) {
|
||||||
input.vertexSnapTrigger = true;
|
input.vertexSnapTrigger = true;
|
||||||
@@ -7141,17 +7227,28 @@ public class EditorApp extends Application {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void launchGame() {
|
private void launchGame() {
|
||||||
if (launchGameAfterSave) return; // bereits ausstehend
|
if (launchGameAfterSave) return;
|
||||||
launchGameAfterSave = true;
|
launchGameAfterSave = true;
|
||||||
if (gamePlayBtn != null) {
|
pendingNewGame = false;
|
||||||
gamePlayBtn.setDisable(true);
|
if (gamePlayBtn != null) { gamePlayBtn.setDisable(true); gamePlayBtn.setText("⏳ Startet…"); }
|
||||||
gamePlayBtn.setText("⏳ Startet…");
|
if (gameNewBtn != null) gameNewBtn.setDisable(true);
|
||||||
}
|
|
||||||
input.saveRequested = true;
|
input.saveRequested = true;
|
||||||
setStatus("Karte wird gespeichert, Spiel startet…");
|
setStatus("Karte wird gespeichert, Spiel startet…");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void launchNewGame() {
|
||||||
|
if (launchGameAfterSave) return;
|
||||||
|
launchGameAfterSave = true;
|
||||||
|
pendingNewGame = true;
|
||||||
|
if (gameNewBtn != null) { gameNewBtn.setDisable(true); gameNewBtn.setText("⏳ Startet…"); }
|
||||||
|
if (gamePlayBtn != null) gamePlayBtn.setDisable(true);
|
||||||
|
input.saveRequested = true;
|
||||||
|
setStatus("Karte wird gespeichert, Neues Spiel startet…");
|
||||||
|
}
|
||||||
|
|
||||||
private void startGameProcess() {
|
private void startGameProcess() {
|
||||||
|
final boolean isNewGame = pendingNewGame;
|
||||||
|
pendingNewGame = false;
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
try {
|
try {
|
||||||
String javaExe = Paths.get(System.getProperty("java.home"), "bin", "java").toString();
|
String javaExe = Paths.get(System.getProperty("java.home"), "bin", "java").toString();
|
||||||
@@ -7178,9 +7275,12 @@ public class EditorApp extends Application {
|
|||||||
// Vom Editor gestartet: Hauptmenü überspringen, letzten Stand fortsetzen
|
// Vom Editor gestartet: Hauptmenü überspringen, letzten Stand fortsetzen
|
||||||
"-Dblight.autostart=true"));
|
"-Dblight.autostart=true"));
|
||||||
|
|
||||||
if (!Float.isNaN(input.tempSpawnX) && !Float.isNaN(input.tempSpawnZ)) {
|
if (isNewGame) {
|
||||||
|
cmd.add("-Dblight.new.game=true");
|
||||||
|
} else if (!Float.isNaN(input.tempSpawnX) && !Float.isNaN(input.tempSpawnZ)) {
|
||||||
cmd.add("-Dblight.temp.spawn.x=" + input.tempSpawnX);
|
cmd.add("-Dblight.temp.spawn.x=" + input.tempSpawnX);
|
||||||
cmd.add("-Dblight.temp.spawn.z=" + input.tempSpawnZ);
|
cmd.add("-Dblight.temp.spawn.z=" + input.tempSpawnZ);
|
||||||
|
cmd.add("-Dblight.temp.spawn.yaw=" + input.tempSpawnYaw);
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.addAll(List.of("-cp", classpath, "de.blight.game.BlightGame"));
|
cmd.addAll(List.of("-cp", classpath, "de.blight.game.BlightGame"));
|
||||||
@@ -7191,8 +7291,9 @@ public class EditorApp extends Application {
|
|||||||
.start();
|
.start();
|
||||||
|
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
setStatus("Spiel gestartet");
|
setStatus(isNewGame ? "Neues Spiel gestartet" : "Spiel gestartet");
|
||||||
if (gamePlayBtn != null) gamePlayBtn.setText("🎮 Läuft…");
|
if (gamePlayBtn != null) gamePlayBtn.setText("🎮 Läuft…");
|
||||||
|
if (gameNewBtn != null) gameNewBtn.setText("🎮 Läuft…");
|
||||||
openGameConsole();
|
openGameConsole();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -7205,22 +7306,18 @@ public class EditorApp extends Application {
|
|||||||
consoleBuffer.offer(line);
|
consoleBuffer.offer(line);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Spiel beendet → Button freigeben
|
// Spiel beendet → Buttons freigeben
|
||||||
consoleBuffer.offer("--- Spiel beendet ---");
|
consoleBuffer.offer("--- Spiel beendet ---");
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
if (gamePlayBtn != null) {
|
if (gamePlayBtn != null) { gamePlayBtn.setText("▶ Spielen"); gamePlayBtn.setDisable(false); }
|
||||||
gamePlayBtn.setText("▶ Spielen");
|
if (gameNewBtn != null) { gameNewBtn.setText("🆕 Neues Spiel"); gameNewBtn.setDisable(false); }
|
||||||
gamePlayBtn.setDisable(false);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (IOException ex) {
|
} catch (IOException ex) {
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
setStatus("Spielstart fehlgeschlagen: " + ex.getMessage());
|
setStatus("Spielstart fehlgeschlagen: " + ex.getMessage());
|
||||||
if (gamePlayBtn != null) {
|
if (gamePlayBtn != null) { gamePlayBtn.setText("▶ Spielen"); gamePlayBtn.setDisable(false); }
|
||||||
gamePlayBtn.setText("▶ Spielen");
|
if (gameNewBtn != null) { gameNewBtn.setText("🆕 Neues Spiel"); gameNewBtn.setDisable(false); }
|
||||||
gamePlayBtn.setDisable(false);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, "game-launcher").start();
|
}, "game-launcher").start();
|
||||||
@@ -7809,47 +7906,63 @@ public class EditorApp extends Application {
|
|||||||
private VBox buildPlayToolPanel() {
|
private VBox buildPlayToolPanel() {
|
||||||
VBox inner = new VBox(8);
|
VBox inner = new VBox(8);
|
||||||
inner.setPadding(new Insets(10));
|
inner.setPadding(new Insets(10));
|
||||||
inner.getChildren().addAll(
|
|
||||||
sectionTitle("Spiel starten"),
|
|
||||||
new Separator(),
|
|
||||||
bold("Temporärer Spawnpunkt:"),
|
|
||||||
styledHint("L-Klick im Viewport → Spawnpunkt setzen"));
|
|
||||||
|
|
||||||
Label coordHint = new Label("oder manuell eingeben:");
|
// ── Abschnitt: Spawnpunkte setzen ────────────────────────────────────
|
||||||
coordHint.setStyle("-fx-text-fill: #444;");
|
inner.getChildren().addAll(sectionTitle("Spawnpunkte"), new Separator());
|
||||||
inner.getChildren().add(coordHint);
|
|
||||||
|
|
||||||
spawnXField = new TextField(Float.isNaN(input.tempSpawnX) ? "" : String.valueOf(input.tempSpawnX));
|
Button setTempBtn = new Button("📍 Temp. Spawn hier setzen");
|
||||||
spawnZField = new TextField(Float.isNaN(input.tempSpawnZ) ? "" : String.valueOf(input.tempSpawnZ));
|
setTempBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
spawnXField.setPromptText("X");
|
setTempBtn.setTooltip(new javafx.scene.control.Tooltip(
|
||||||
spawnZField.setPromptText("Z");
|
"Setzt Yaw = Kamera-Blickrichtung und aktiviert Klick-Modus im Viewport"));
|
||||||
|
setTempBtn.setOnAction(e -> {
|
||||||
Runnable applyFields = () -> {
|
input.tempSpawnYaw = camYawToSpawnYaw(input.camYaw);
|
||||||
try {
|
input.playToolMode = SharedInput.PlayToolMode.SET_TEMP;
|
||||||
input.tempSpawnX = Float.parseFloat(spawnXField.getText().trim());
|
setStatus("L-Klick im Viewport → temporären Spawnpunkt setzen");
|
||||||
input.tempSpawnZ = Float.parseFloat(spawnZField.getText().trim());
|
|
||||||
} catch (NumberFormatException ignored2) {}
|
|
||||||
};
|
|
||||||
spawnXField.setOnAction(e -> applyFields.run());
|
|
||||||
spawnZField.setOnAction(e -> applyFields.run());
|
|
||||||
spawnXField.focusedProperty().addListener((o, ov, nv) -> { if (!nv) applyFields.run(); });
|
|
||||||
spawnZField.focusedProperty().addListener((o, ov, nv) -> { if (!nv) applyFields.run(); });
|
|
||||||
|
|
||||||
HBox coordRow = new HBox(6, new Label("X:"), spawnXField, new Label("Z:"), spawnZField);
|
|
||||||
coordRow.setAlignment(Pos.CENTER_LEFT);
|
|
||||||
HBox.setHgrow(spawnXField, Priority.ALWAYS);
|
|
||||||
HBox.setHgrow(spawnZField, Priority.ALWAYS);
|
|
||||||
|
|
||||||
Button clearSpawn = new Button("✕ Spawnpunkt löschen");
|
|
||||||
clearSpawn.setMaxWidth(Double.MAX_VALUE);
|
|
||||||
clearSpawn.setOnAction(e -> {
|
|
||||||
input.tempSpawnX = Float.NaN;
|
|
||||||
input.tempSpawnZ = Float.NaN;
|
|
||||||
spawnXField.setText("");
|
|
||||||
spawnZField.setText("");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
inner.getChildren().addAll(coordRow, clearSpawn, new Separator());
|
Button setPermBtn = new Button("🏁 Perm. Spawn hier setzen");
|
||||||
|
setPermBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
setPermBtn.setTooltip(new javafx.scene.control.Tooltip(
|
||||||
|
"Setzt Yaw = Kamera-Blickrichtung und aktiviert Klick-Modus im Viewport"));
|
||||||
|
setPermBtn.setOnAction(e -> {
|
||||||
|
input.permSpawnYaw = camYawToSpawnYaw(input.camYaw);
|
||||||
|
input.playToolMode = SharedInput.PlayToolMode.SET_PERM;
|
||||||
|
setStatus("L-Klick im Viewport → permanenten Spawnpunkt setzen");
|
||||||
|
});
|
||||||
|
|
||||||
|
ToggleButton editModeBtn = new ToggleButton("✎ Bearbeiten");
|
||||||
|
editModeBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
editModeBtn.setTooltip(new javafx.scene.control.Tooltip(
|
||||||
|
"Drag auf Marker → Position; Drag auf Pfeilspitze → Richtung drehen"));
|
||||||
|
editModeBtn.setOnAction(e -> {
|
||||||
|
input.playToolMode = editModeBtn.isSelected()
|
||||||
|
? SharedInput.PlayToolMode.EDIT
|
||||||
|
: SharedInput.PlayToolMode.NONE;
|
||||||
|
});
|
||||||
|
|
||||||
|
inner.getChildren().addAll(setTempBtn, setPermBtn, editModeBtn, new Separator());
|
||||||
|
|
||||||
|
// ── Koordinatenanzeige ────────────────────────────────────────────────
|
||||||
|
tempSpawnCoordsLabel = new Label(tempSpawnCoordsText());
|
||||||
|
tempSpawnCoordsLabel.setStyle("-fx-font-family: monospace; -fx-font-size: 11; -fx-text-fill: #333;");
|
||||||
|
permSpawnCoordsLabel = new Label(permSpawnCoordsText());
|
||||||
|
permSpawnCoordsLabel.setStyle("-fx-font-family: monospace; -fx-font-size: 11; -fx-text-fill: #333;");
|
||||||
|
|
||||||
|
Button clearTempBtn = new Button("✕ Temp. Spawn löschen");
|
||||||
|
clearTempBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
clearTempBtn.setOnAction(e -> {
|
||||||
|
input.tempSpawnX = Float.NaN;
|
||||||
|
input.tempSpawnZ = Float.NaN;
|
||||||
|
if (tempSpawnCoordsLabel != null) tempSpawnCoordsLabel.setText("(nicht gesetzt)");
|
||||||
|
});
|
||||||
|
|
||||||
|
inner.getChildren().addAll(
|
||||||
|
bold("Temp. Spawn:"), tempSpawnCoordsLabel, clearTempBtn,
|
||||||
|
bold("Perm. Spawn:"), permSpawnCoordsLabel,
|
||||||
|
new Separator());
|
||||||
|
|
||||||
|
// ── Abschnitt: Spielen ────────────────────────────────────────────────
|
||||||
|
inner.getChildren().addAll(sectionTitle("Spiel starten"), new Separator());
|
||||||
|
|
||||||
Button playBtn = new Button("▶ Spielen");
|
Button playBtn = new Button("▶ Spielen");
|
||||||
gamePlayBtn = playBtn;
|
gamePlayBtn = playBtn;
|
||||||
@@ -7857,8 +7970,21 @@ public class EditorApp extends Application {
|
|||||||
playBtn.setStyle(
|
playBtn.setStyle(
|
||||||
"-fx-background-color: #2d8a3e; -fx-text-fill: white; " +
|
"-fx-background-color: #2d8a3e; -fx-text-fill: white; " +
|
||||||
"-fx-font-weight: bold; -fx-padding: 6 12 6 12;");
|
"-fx-font-weight: bold; -fx-padding: 6 12 6 12;");
|
||||||
|
playBtn.setTooltip(new javafx.scene.control.Tooltip(
|
||||||
|
"Startet das Spiel mit dem temp. Spawnpunkt (bzw. gespeicherter Position)"));
|
||||||
playBtn.setOnAction(e -> launchGame());
|
playBtn.setOnAction(e -> launchGame());
|
||||||
inner.getChildren().add(playBtn);
|
|
||||||
|
Button newGameBtn = new Button("🆕 Neues Spiel");
|
||||||
|
gameNewBtn = newGameBtn;
|
||||||
|
newGameBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
newGameBtn.setStyle(
|
||||||
|
"-fx-background-color: #7b3ea4; -fx-text-fill: white; " +
|
||||||
|
"-fx-font-weight: bold; -fx-padding: 6 12 6 12;");
|
||||||
|
newGameBtn.setTooltip(new javafx.scene.control.Tooltip(
|
||||||
|
"Startet ein neues Spiel am perm. Spawnpunkt mit Intro-Sequenz"));
|
||||||
|
newGameBtn.setOnAction(e -> launchNewGame());
|
||||||
|
|
||||||
|
inner.getChildren().addAll(playBtn, newGameBtn);
|
||||||
|
|
||||||
ScrollPane scroll = new ScrollPane(inner);
|
ScrollPane scroll = new ScrollPane(inner);
|
||||||
scroll.setFitToWidth(true);
|
scroll.setFitToWidth(true);
|
||||||
@@ -7871,12 +7997,28 @@ public class EditorApp extends Application {
|
|||||||
return panel;
|
return panel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Konvertiert den Editor-Kamera-Yaw in den Spawnpunkt-Yaw (0=+Z, 90=+X, UZS von oben). */
|
||||||
|
private static float camYawToSpawnYaw(float camYaw) {
|
||||||
|
return ((180f + camYaw) % 360f + 360f) % 360f;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String tempSpawnCoordsText() {
|
||||||
|
if (Float.isNaN(input.tempSpawnX) || Float.isNaN(input.tempSpawnZ)) return "(nicht gesetzt)";
|
||||||
|
return String.format("X=%.1f Z=%.1f Yaw=%.0f°", input.tempSpawnX, input.tempSpawnZ, input.tempSpawnYaw);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String permSpawnCoordsText() {
|
||||||
|
if (Float.isNaN(input.permSpawnX) || Float.isNaN(input.permSpawnZ)) return "(nicht gesetzt)";
|
||||||
|
return String.format("X=%.1f Z=%.1f Yaw=%.0f°", input.permSpawnX, input.permSpawnZ, input.permSpawnYaw);
|
||||||
|
}
|
||||||
|
|
||||||
private void updateSpawnFields(String info) {
|
private void updateSpawnFields(String info) {
|
||||||
if (spawnXField == null || info == null) return;
|
if (info == null) return;
|
||||||
String[] p = info.split("\\|", -1);
|
String[] p = info.split("\\|", -1);
|
||||||
if (p.length < 2) return;
|
if (p.length < 2) return;
|
||||||
spawnXField.setText(p[0]);
|
if (spawnXField != null) spawnXField.setText(p[0]);
|
||||||
spawnZField.setText(p[1]);
|
if (spawnZField != null) spawnZField.setText(p[1]);
|
||||||
|
if (tempSpawnCoordsLabel != null) tempSpawnCoordsLabel.setText(tempSpawnCoordsText());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Tripo3D-Generator ────────────────────────────────────────────────────
|
// ── Tripo3D-Generator ────────────────────────────────────────────────────
|
||||||
@@ -8280,6 +8422,7 @@ public class EditorApp extends Application {
|
|||||||
animSetClipListView = new ListView<>();
|
animSetClipListView = new ListView<>();
|
||||||
animSetClipListView.getItems().addAll(animSet.getClips());
|
animSetClipListView.getItems().addAll(animSet.getClips());
|
||||||
animSetClipListView.setPrefHeight(180);
|
animSetClipListView.setPrefHeight(180);
|
||||||
|
editableSubClips = new java.util.LinkedHashMap<>(animSet.getSubClips());
|
||||||
|
|
||||||
Path animRootDir = ASSET_ROOT.resolve("animations");
|
Path animRootDir = ASSET_ROOT.resolve("animations");
|
||||||
|
|
||||||
@@ -8390,6 +8533,8 @@ public class EditorApp extends Application {
|
|||||||
animSetClipListView.getItems().remove(sel);
|
animSetClipListView.getItems().remove(sel);
|
||||||
if (animSetActionListView != null)
|
if (animSetActionListView != null)
|
||||||
animSetActionListView.getItems().removeIf(it -> it.endsWith(" → " + sel));
|
animSetActionListView.getItems().removeIf(it -> it.endsWith(" → " + sel));
|
||||||
|
editableSubClips.entrySet().removeIf(en -> sel.equals(en.getValue().source));
|
||||||
|
if (animSetPartsListView != null) animSetPartsListView.getItems().clear();
|
||||||
animSetDirty = true;
|
animSetDirty = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -8398,6 +8543,75 @@ public class EditorApp extends Application {
|
|||||||
HBox.setHgrow(removeClipBtn, Priority.ALWAYS);
|
HBox.setHgrow(removeClipBtn, Priority.ALWAYS);
|
||||||
inner.getChildren().addAll(animSetClipListView, clipBtns);
|
inner.getChildren().addAll(animSetClipListView, clipBtns);
|
||||||
|
|
||||||
|
// ── Clip-Teile (Sub-Clips) ────────────────────────────────────────────
|
||||||
|
inner.getChildren().addAll(new Separator(), sectionTitle("Clip-Teile"), new Separator());
|
||||||
|
|
||||||
|
Label partsHint = new Label("Gewählten Clip in benannte Teile aufteilen. Jeder Teil erhält einen Namen und Zeitgrenzen (Sub-Clips).");
|
||||||
|
partsHint.setStyle("-fx-font-size: 10; -fx-text-fill: #888;");
|
||||||
|
partsHint.setWrapText(true);
|
||||||
|
|
||||||
|
animSetPartsListView = new ListView<>();
|
||||||
|
animSetPartsListView.setPrefHeight(120);
|
||||||
|
animSetPartsListView.setPlaceholder(new Label("Clip auswählen oder noch keine Teile definiert"));
|
||||||
|
animSetPartsListView.setDisable(true);
|
||||||
|
|
||||||
|
Button addPartBtn = new Button("+ Teil hinzufügen…");
|
||||||
|
Button removePartBtn = new Button("- Entfernen");
|
||||||
|
addPartBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
removePartBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
addPartBtn.setDisable(true);
|
||||||
|
removePartBtn.setDisable(true);
|
||||||
|
|
||||||
|
animSetPartsListView.getSelectionModel().selectedItemProperty()
|
||||||
|
.addListener((obs, ov, nv) -> removePartBtn.setDisable(nv == null));
|
||||||
|
|
||||||
|
animSetPartsListView.setOnMouseClicked(ev -> {
|
||||||
|
if (ev.getClickCount() == 2) {
|
||||||
|
String sel = animSetPartsListView.getSelectionModel().getSelectedItem();
|
||||||
|
if (sel == null) return;
|
||||||
|
String selName = sel.substring(0, sel.indexOf(" ["));
|
||||||
|
de.blight.game.animation.AnimSet.SubClipDef def = editableSubClips.get(selName);
|
||||||
|
String source = animSetClipListView.getSelectionModel().getSelectedItem();
|
||||||
|
if (def != null && source != null) showPartDialog(source, selName, def);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Teile-Liste aktualisieren wenn Clip-Auswahl ändert
|
||||||
|
animSetClipListView.getSelectionModel().selectedItemProperty()
|
||||||
|
.addListener((obs, ov, nv) -> {
|
||||||
|
animSetPartsListView.getItems().clear();
|
||||||
|
boolean hasClip = nv != null;
|
||||||
|
animSetPartsListView.setDisable(!hasClip);
|
||||||
|
addPartBtn.setDisable(!hasClip);
|
||||||
|
if (hasClip) {
|
||||||
|
for (var en : editableSubClips.entrySet()) {
|
||||||
|
if (nv.equals(en.getValue().source)) {
|
||||||
|
animSetPartsListView.getItems().add(
|
||||||
|
formatPartEntry(en.getKey(), en.getValue()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
addPartBtn.setOnAction(e -> {
|
||||||
|
String source = animSetClipListView.getSelectionModel().getSelectedItem();
|
||||||
|
if (source != null) showPartDialog(source, null, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
removePartBtn.setOnAction(e -> {
|
||||||
|
String sel = animSetPartsListView.getSelectionModel().getSelectedItem();
|
||||||
|
if (sel == null) return;
|
||||||
|
String name = sel.substring(0, sel.indexOf(" ["));
|
||||||
|
editableSubClips.remove(name);
|
||||||
|
animSetPartsListView.getItems().remove(sel);
|
||||||
|
animSetDirty = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
HBox partsBtns = new HBox(6, addPartBtn, removePartBtn);
|
||||||
|
HBox.setHgrow(addPartBtn, Priority.ALWAYS);
|
||||||
|
HBox.setHgrow(removePartBtn, Priority.ALWAYS);
|
||||||
|
inner.getChildren().addAll(partsHint, animSetPartsListView, partsBtns);
|
||||||
|
|
||||||
// ── Aktions-Zuordnung ─────────────────────────────────────────────────
|
// ── Aktions-Zuordnung ─────────────────────────────────────────────────
|
||||||
inner.getChildren().addAll(new Separator(), sectionTitle("Aktions-Zuordnung"), new Separator());
|
inner.getChildren().addAll(new Separator(), sectionTitle("Aktions-Zuordnung"), new Separator());
|
||||||
|
|
||||||
@@ -8701,7 +8915,11 @@ public class EditorApp extends Application {
|
|||||||
actionCombo.getSelectionModel().selectFirst();
|
actionCombo.getSelectionModel().selectFirst();
|
||||||
|
|
||||||
ComboBox<String> clipCombo = new ComboBox<>();
|
ComboBox<String> clipCombo = new ComboBox<>();
|
||||||
clipCombo.getItems().addAll(animSetClipListView.getItems());
|
java.util.List<String> allClipNames = new java.util.ArrayList<>(animSetClipListView.getItems());
|
||||||
|
for (String subName : editableSubClips.keySet()) {
|
||||||
|
if (!allClipNames.contains(subName)) allClipNames.add(subName);
|
||||||
|
}
|
||||||
|
clipCombo.getItems().addAll(allClipNames);
|
||||||
clipCombo.setMaxWidth(Double.MAX_VALUE);
|
clipCombo.setMaxWidth(Double.MAX_VALUE);
|
||||||
clipCombo.getSelectionModel().selectFirst();
|
clipCombo.getSelectionModel().selectFirst();
|
||||||
|
|
||||||
@@ -8732,6 +8950,78 @@ public class EditorApp extends Application {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String formatPartEntry(String name,
|
||||||
|
de.blight.game.animation.AnimSet.SubClipDef def) {
|
||||||
|
return String.format("%s [%.3fs – %.3fs]", name, def.start, def.end);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showPartDialog(String source, String existingName,
|
||||||
|
de.blight.game.animation.AnimSet.SubClipDef existing) {
|
||||||
|
boolean isEdit = existingName != null && existing != null;
|
||||||
|
|
||||||
|
javafx.scene.control.TextField nameField = new javafx.scene.control.TextField(
|
||||||
|
isEdit ? existingName : "");
|
||||||
|
nameField.setPromptText("Teil-Name (z. B. sit_down_bench)");
|
||||||
|
nameField.setDisable(isEdit); // Name beim Bearbeiten nicht änderbar
|
||||||
|
|
||||||
|
Spinner<Double> startSpinner = new Spinner<>(0.0, 9999.0,
|
||||||
|
isEdit ? existing.start : 0.0, 0.033);
|
||||||
|
startSpinner.setEditable(true);
|
||||||
|
startSpinner.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
|
||||||
|
Spinner<Double> endSpinner = new Spinner<>(0.0, 9999.0,
|
||||||
|
isEdit ? existing.end : 1.0, 0.033);
|
||||||
|
endSpinner.setEditable(true);
|
||||||
|
endSpinner.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
|
||||||
|
javafx.scene.layout.GridPane grid = new javafx.scene.layout.GridPane();
|
||||||
|
grid.setHgap(8); grid.setVgap(6);
|
||||||
|
grid.add(new Label("Name:"), 0, 0); grid.add(nameField, 1, 0);
|
||||||
|
grid.add(new Label("Start:"), 0, 1); grid.add(startSpinner, 1, 1);
|
||||||
|
grid.add(new Label("Ende:"), 0, 2); grid.add(endSpinner, 1, 2);
|
||||||
|
javafx.scene.layout.ColumnConstraints cc = new javafx.scene.layout.ColumnConstraints();
|
||||||
|
cc.setHgrow(Priority.ALWAYS);
|
||||||
|
grid.getColumnConstraints().addAll(new javafx.scene.layout.ColumnConstraints(), cc);
|
||||||
|
|
||||||
|
javafx.scene.control.Dialog<javafx.scene.control.ButtonType> dlg =
|
||||||
|
new javafx.scene.control.Dialog<>();
|
||||||
|
dlg.setTitle(isEdit ? "Clip-Teil bearbeiten" : "Clip-Teil hinzufügen");
|
||||||
|
dlg.setHeaderText((isEdit ? "Bearbeiten: " : "Neuer Teil für: ") + source);
|
||||||
|
javafx.scene.control.ButtonType ok = new javafx.scene.control.ButtonType(
|
||||||
|
isEdit ? "Übernehmen" : "Hinzufügen",
|
||||||
|
javafx.scene.control.ButtonBar.ButtonData.OK_DONE);
|
||||||
|
dlg.getDialogPane().getButtonTypes().addAll(ok, javafx.scene.control.ButtonType.CANCEL);
|
||||||
|
dlg.getDialogPane().setContent(grid);
|
||||||
|
|
||||||
|
javafx.scene.Node okNode = dlg.getDialogPane().lookupButton(ok);
|
||||||
|
Runnable validate = () -> {
|
||||||
|
boolean valid = !nameField.getText().isBlank()
|
||||||
|
&& endSpinner.getValue() > startSpinner.getValue();
|
||||||
|
okNode.setDisable(!valid);
|
||||||
|
};
|
||||||
|
validate.run();
|
||||||
|
nameField.textProperty().addListener((obs, ov, nv) -> validate.run());
|
||||||
|
startSpinner.valueProperty().addListener((obs, ov, nv) -> validate.run());
|
||||||
|
endSpinner.valueProperty().addListener((obs, ov, nv) -> validate.run());
|
||||||
|
|
||||||
|
dlg.showAndWait().ifPresent(bt -> {
|
||||||
|
if (bt != ok) return;
|
||||||
|
String name = isEdit ? existingName : nameField.getText().trim();
|
||||||
|
if (name.isBlank()) return;
|
||||||
|
de.blight.game.animation.AnimSet.SubClipDef def =
|
||||||
|
new de.blight.game.animation.AnimSet.SubClipDef();
|
||||||
|
def.source = source;
|
||||||
|
def.start = startSpinner.getValue().floatValue();
|
||||||
|
def.end = endSpinner.getValue().floatValue();
|
||||||
|
editableSubClips.put(name, def);
|
||||||
|
if (animSetPartsListView != null) {
|
||||||
|
animSetPartsListView.getItems().removeIf(s -> s.startsWith(name + " ["));
|
||||||
|
animSetPartsListView.getItems().add(formatPartEntry(name, def));
|
||||||
|
}
|
||||||
|
animSetDirty = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private void saveCurrentAnimSet(String setName, Path setDir) {
|
private void saveCurrentAnimSet(String setName, Path setDir) {
|
||||||
if (animSetClipListView == null) {
|
if (animSetClipListView == null) {
|
||||||
return;
|
return;
|
||||||
@@ -8767,6 +9057,7 @@ public class EditorApp extends Application {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
animSet.setAnimOffsets(offsetFinal);
|
animSet.setAnimOffsets(offsetFinal);
|
||||||
|
animSet.setSubClips(new java.util.LinkedHashMap<>(editableSubClips));
|
||||||
// Vorschau-Modell-Pfad beibehalten
|
// Vorschau-Modell-Pfad beibehalten
|
||||||
if (animSetModelCombo != null && animSetModelCombo.getValue() != null && !animSetModelCombo.getValue().isBlank()) {
|
if (animSetModelCombo != null && animSetModelCombo.getValue() != null && !animSetModelCombo.getValue().isBlank()) {
|
||||||
animSet.setPreviewModelPath(animSetModelCombo.getValue());
|
animSet.setPreviewModelPath(animSetModelCombo.getValue());
|
||||||
@@ -9511,10 +9802,24 @@ public class EditorApp extends Application {
|
|||||||
Label npcTraderLbl = new Label("Handel:");
|
Label npcTraderLbl = new Label("Handel:");
|
||||||
Label npcItemsLbl = new Label("Waren:");
|
Label npcItemsLbl = new Label("Waren:");
|
||||||
|
|
||||||
|
charMsgFriendly = new javafx.scene.control.TextField();
|
||||||
|
charMsgFriendly.setPromptText("TextReference-Schlüssel (z.B. silas.msg.friendly)");
|
||||||
|
charMsgNeutral = new javafx.scene.control.TextField();
|
||||||
|
charMsgNeutral.setPromptText("TextReference-Schlüssel");
|
||||||
|
charMsgEnraged = new javafx.scene.control.TextField();
|
||||||
|
charMsgEnraged.setPromptText("TextReference-Schlüssel");
|
||||||
|
charMsgEnemy = new javafx.scene.control.TextField();
|
||||||
|
charMsgEnemy.setPromptText("TextReference-Schlüssel");
|
||||||
|
|
||||||
charNpcSection = new VBox(4,
|
charNpcSection = new VBox(4,
|
||||||
npcStatusLbl, charStatusCombo,
|
npcStatusLbl, charStatusCombo,
|
||||||
npcTraderLbl, charTraderCheck,
|
npcTraderLbl, charTraderCheck,
|
||||||
npcItemsLbl, charTraderItemsView, traderBtns);
|
npcItemsLbl, charTraderItemsView, traderBtns,
|
||||||
|
sectionTitle("Standard-Nachrichten je Status"),
|
||||||
|
new Label("Friendly:"), charMsgFriendly,
|
||||||
|
new Label("Neutral:"), charMsgNeutral,
|
||||||
|
new Label("Enraged:"), charMsgEnraged,
|
||||||
|
new Label("Enemy:"), charMsgEnemy);
|
||||||
boolean npcInitial = "NPC".equals(charTypeCombo.getValue());
|
boolean npcInitial = "NPC".equals(charTypeCombo.getValue());
|
||||||
charNpcSection.setVisible(npcInitial);
|
charNpcSection.setVisible(npcInitial);
|
||||||
charNpcSection.setManaged(npcInitial);
|
charNpcSection.setManaged(npcInitial);
|
||||||
@@ -9666,6 +9971,10 @@ public class EditorApp extends Application {
|
|||||||
if (charStatusCombo != null) charStatusCombo.setValue("NEUTRAL");
|
if (charStatusCombo != null) charStatusCombo.setValue("NEUTRAL");
|
||||||
if (charTraderCheck != null) charTraderCheck.setSelected(false);
|
if (charTraderCheck != null) charTraderCheck.setSelected(false);
|
||||||
if (charTraderItemsView != null) charTraderItemsView.getItems().clear();
|
if (charTraderItemsView != null) charTraderItemsView.getItems().clear();
|
||||||
|
if (charMsgFriendly != null) charMsgFriendly.clear();
|
||||||
|
if (charMsgNeutral != null) charMsgNeutral.clear();
|
||||||
|
if (charMsgEnraged != null) charMsgEnraged.clear();
|
||||||
|
if (charMsgEnemy != null) charMsgEnemy.clear();
|
||||||
if (abMagicSpin != null) resetAbilities();
|
if (abMagicSpin != null) resetAbilities();
|
||||||
updateCharActionCombosFromSet();
|
updateCharActionCombosFromSet();
|
||||||
if (charEditContainer != null) charEditContainer.setDisable(false);
|
if (charEditContainer != null) charEditContainer.setDisable(false);
|
||||||
@@ -9710,6 +10019,12 @@ public class EditorApp extends Application {
|
|||||||
if (npc.getItems() != null)
|
if (npc.getItems() != null)
|
||||||
npc.getItems().forEach(it -> charTraderItemsView.getItems().add(it.getItemId()));
|
npc.getItems().forEach(it -> charTraderItemsView.getItems().add(it.getItemId()));
|
||||||
}
|
}
|
||||||
|
java.util.Map<de.blight.common.model.Status, de.blight.common.model.TextReference> msgs =
|
||||||
|
npc.getDefaultMessages();
|
||||||
|
if (charMsgFriendly != null) charMsgFriendly.setText(msgKey(msgs, de.blight.common.model.Status.FRIENDLY));
|
||||||
|
if (charMsgNeutral != null) charMsgNeutral.setText(msgKey(msgs, de.blight.common.model.Status.NEUTRAL));
|
||||||
|
if (charMsgEnraged != null) charMsgEnraged.setText(msgKey(msgs, de.blight.common.model.Status.ENRAGED));
|
||||||
|
if (charMsgEnemy != null) charMsgEnemy.setText(msgKey(msgs, de.blight.common.model.Status.ENEMY));
|
||||||
}
|
}
|
||||||
if (c instanceof de.blight.common.model.MainCharacter mc) {
|
if (c instanceof de.blight.common.model.MainCharacter mc) {
|
||||||
loadAbilities(mc.getAbilities());
|
loadAbilities(mc.getAbilities());
|
||||||
@@ -9721,6 +10036,7 @@ public class EditorApp extends Application {
|
|||||||
else dialogEditorView.clear();
|
else dialogEditorView.clear();
|
||||||
}
|
}
|
||||||
if (charEditorStatusLabel != null) charEditorStatusLabel.setText("Geladen: " + id);
|
if (charEditorStatusLabel != null) charEditorStatusLabel.setText("Geladen: " + id);
|
||||||
|
charDirty = true;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
if (charEditorStatusLabel != null) charEditorStatusLabel.setText("Fehler: " + e.getMessage());
|
if (charEditorStatusLabel != null) charEditorStatusLabel.setText("Fehler: " + e.getMessage());
|
||||||
}
|
}
|
||||||
@@ -9757,6 +10073,14 @@ public class EditorApp extends Application {
|
|||||||
catch (IllegalArgumentException ignored) {}
|
catch (IllegalArgumentException ignored) {}
|
||||||
}
|
}
|
||||||
if (charTraderCheck != null) npc.setTrader(charTraderCheck.isSelected());
|
if (charTraderCheck != null) npc.setTrader(charTraderCheck.isSelected());
|
||||||
|
// Default-Nachrichten je Status
|
||||||
|
java.util.Map<de.blight.common.model.Status, de.blight.common.model.TextReference> msgs =
|
||||||
|
new java.util.EnumMap<>(de.blight.common.model.Status.class);
|
||||||
|
putMsg(msgs, de.blight.common.model.Status.FRIENDLY, charMsgFriendly);
|
||||||
|
putMsg(msgs, de.blight.common.model.Status.NEUTRAL, charMsgNeutral);
|
||||||
|
putMsg(msgs, de.blight.common.model.Status.ENRAGED, charMsgEnraged);
|
||||||
|
putMsg(msgs, de.blight.common.model.Status.ENEMY, charMsgEnemy);
|
||||||
|
npc.setDefaultMessages(msgs);
|
||||||
if (charTraderItemsView != null && !charTraderItemsView.getItems().isEmpty()) {
|
if (charTraderItemsView != null && !charTraderItemsView.getItems().isEmpty()) {
|
||||||
java.util.List<de.blight.common.model.Item> items = new java.util.ArrayList<>();
|
java.util.List<de.blight.common.model.Item> items = new java.util.ArrayList<>();
|
||||||
charTraderItemsView.getItems().forEach(id2 -> {
|
charTraderItemsView.getItems().forEach(id2 -> {
|
||||||
@@ -9775,6 +10099,7 @@ public class EditorApp extends Application {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
de.blight.common.model.CharacterIO.save(c, charDir);
|
de.blight.common.model.CharacterIO.save(c, charDir);
|
||||||
|
charDirty = false;
|
||||||
refreshCharacterList();
|
refreshCharacterList();
|
||||||
if (charEditorStatusLabel != null) charEditorStatusLabel.setText("Gespeichert: " + id);
|
if (charEditorStatusLabel != null) charEditorStatusLabel.setText("Gespeichert: " + id);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -9899,6 +10224,23 @@ public class EditorApp extends Application {
|
|||||||
return ab;
|
return ab;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String msgKey(
|
||||||
|
java.util.Map<de.blight.common.model.Status, de.blight.common.model.TextReference> msgs,
|
||||||
|
de.blight.common.model.Status status) {
|
||||||
|
if (msgs == null) return "";
|
||||||
|
de.blight.common.model.TextReference ref = msgs.get(status);
|
||||||
|
return (ref != null && ref.id() != null) ? ref.id() : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void putMsg(
|
||||||
|
java.util.Map<de.blight.common.model.Status, de.blight.common.model.TextReference> msgs,
|
||||||
|
de.blight.common.model.Status status,
|
||||||
|
javafx.scene.control.TextField field) {
|
||||||
|
if (field == null) return;
|
||||||
|
String s = field.getText().trim();
|
||||||
|
if (!s.isBlank()) msgs.put(status, new de.blight.common.model.TextReference(s));
|
||||||
|
}
|
||||||
|
|
||||||
private void switchToLocationEditor() {
|
private void switchToLocationEditor() {
|
||||||
onF5 = null;
|
onF5 = null;
|
||||||
currentTool = "locationEditor";
|
currentTool = "locationEditor";
|
||||||
|
|||||||
@@ -521,9 +521,17 @@ public class SharedInput {
|
|||||||
public volatile boolean cancelZoneDrawing = false;
|
public volatile boolean cancelZoneDrawing = false;
|
||||||
|
|
||||||
// ── Spiel-Starten-Werkzeug ────────────────────────────────────────────────
|
// ── Spiel-Starten-Werkzeug ────────────────────────────────────────────────
|
||||||
/** Klick im Viewport zum Setzen des temporären Spawnpunkts. */
|
/** Klick/Drag-Ereignisse im Viewport für das Play-Tool. */
|
||||||
public record PlayToolClick(float screenX, float screenY) {}
|
public record PlayToolClick(float screenX, float screenY) {}
|
||||||
|
public record PlayToolDrag(float screenX, float screenY) {}
|
||||||
public final ConcurrentLinkedQueue<PlayToolClick> playToolClickQueue = new ConcurrentLinkedQueue<>();
|
public final ConcurrentLinkedQueue<PlayToolClick> playToolClickQueue = new ConcurrentLinkedQueue<>();
|
||||||
|
public final ConcurrentLinkedQueue<PlayToolDrag> playToolDragQueue = new ConcurrentLinkedQueue<>();
|
||||||
|
/** JME3 → JavaFX: Maus-Taste wurde losgelassen (EDIT-Modus). */
|
||||||
|
public volatile boolean playToolMouseUp = false;
|
||||||
|
|
||||||
|
/** Sub-Modus des Play-Tools. */
|
||||||
|
public enum PlayToolMode { NONE, SET_TEMP, SET_PERM, EDIT }
|
||||||
|
public volatile PlayToolMode playToolMode = PlayToolMode.NONE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JME → JavaFX: Terrain-Treffpunkt nach Spawn-Klick.
|
* JME → JavaFX: Terrain-Treffpunkt nach Spawn-Klick.
|
||||||
@@ -531,10 +539,21 @@ public class SharedInput {
|
|||||||
*/
|
*/
|
||||||
public volatile String pickedSpawnInfo = null;
|
public volatile String pickedSpawnInfo = null;
|
||||||
public volatile boolean spawnPickChanged = false;
|
public volatile boolean spawnPickChanged = false;
|
||||||
|
public volatile String pickedPermSpawnInfo = null;
|
||||||
|
public volatile boolean permSpawnChanged = false;
|
||||||
|
|
||||||
/** Temporärer Spawnpunkt (NaN = nicht gesetzt). Wird beim Spielstart als System-Property übergeben. */
|
/** Temporärer Spawnpunkt (NaN = nicht gesetzt). Wird beim Spielstart als System-Property übergeben. */
|
||||||
public volatile float tempSpawnX = Float.NaN;
|
public volatile float tempSpawnX = Float.NaN;
|
||||||
public volatile float tempSpawnZ = Float.NaN;
|
public volatile float tempSpawnZ = Float.NaN;
|
||||||
|
public volatile float tempSpawnYaw = 0f;
|
||||||
|
|
||||||
|
/** Permanenter Spawnpunkt (NaN = nicht gesetzt). Wird in MapData.spawnX/Z/Yaw gespeichert. */
|
||||||
|
public volatile float permSpawnX = Float.NaN;
|
||||||
|
public volatile float permSpawnZ = Float.NaN;
|
||||||
|
public volatile float permSpawnYaw = 0f;
|
||||||
|
|
||||||
|
/** Master-Lautstärke (0=stumm, 1=voll) – wird von der Intro-Sequenz gesetzt. */
|
||||||
|
public volatile float masterAudioVolume = 1.0f;
|
||||||
|
|
||||||
// ── Animations-Vorschau ──────────────────────────────────────────────────
|
// ── Animations-Vorschau ──────────────────────────────────────────────────
|
||||||
public volatile float animPreviewRotY = 0f;
|
public volatile float animPreviewRotY = 0f;
|
||||||
|
|||||||
@@ -9,12 +9,30 @@ import com.jme3.material.Material;
|
|||||||
import com.jme3.math.*;
|
import com.jme3.math.*;
|
||||||
import com.jme3.renderer.Camera;
|
import com.jme3.renderer.Camera;
|
||||||
import com.jme3.scene.*;
|
import com.jme3.scene.*;
|
||||||
import com.jme3.scene.shape.Cylinder;
|
import com.jme3.scene.shape.*;
|
||||||
import com.jme3.terrain.geomipmap.TerrainQuad;
|
import com.jme3.terrain.geomipmap.TerrainQuad;
|
||||||
import de.blight.editor.SharedInput;
|
import de.blight.editor.SharedInput;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rendert Spawn-Marker im Editor (temp=grün, perm=blau) mit Richtungspfeilen.
|
||||||
|
*
|
||||||
|
* Modi:
|
||||||
|
* NONE – Marker sichtbar, aber keine Interaktion
|
||||||
|
* SET_TEMP – Nächster Viewport-Klick setzt temp. Spawnpunkt
|
||||||
|
* SET_PERM – Nächster Viewport-Klick setzt perm. Spawnpunkt
|
||||||
|
* EDIT – Drag auf Marker-Körper → Positionieren;
|
||||||
|
* Drag auf Pfeilspitze → Richtung drehen (via XZ-Projektion)
|
||||||
|
*/
|
||||||
public class PlayToolState extends BaseAppState {
|
public class PlayToolState extends BaseAppState {
|
||||||
|
|
||||||
|
private static final float MARKER_RADIUS = 0.5f;
|
||||||
|
private static final float SHAFT_LEN = 2.0f;
|
||||||
|
private static final float SHAFT_RADIUS = 0.06f;
|
||||||
|
private static final float TIP_LEN = 0.5f;
|
||||||
|
private static final float TIP_RADIUS = 0.18f;
|
||||||
|
|
||||||
|
private enum DragTarget { NONE, POS_TEMP, POS_PERM, ROT_TEMP, ROT_PERM }
|
||||||
|
|
||||||
private final SharedInput input;
|
private final SharedInput input;
|
||||||
private SimpleApplication app;
|
private SimpleApplication app;
|
||||||
private Camera cam;
|
private Camera cam;
|
||||||
@@ -22,7 +40,21 @@ public class PlayToolState extends BaseAppState {
|
|||||||
private Node rootNode;
|
private Node rootNode;
|
||||||
private TerrainQuad terrain;
|
private TerrainQuad terrain;
|
||||||
|
|
||||||
private Geometry spawnMarker;
|
// Marker-Nodes
|
||||||
|
private Node tempMarkerNode;
|
||||||
|
private Node permMarkerNode;
|
||||||
|
private Node tempArrowNode;
|
||||||
|
private Node permArrowNode;
|
||||||
|
|
||||||
|
// Separate Geometrien für Ray-Cast-Erkennung
|
||||||
|
private Geometry tempBodyGeom;
|
||||||
|
private Geometry permBodyGeom;
|
||||||
|
private Geometry tempTipGeom;
|
||||||
|
private Geometry permTipGeom;
|
||||||
|
|
||||||
|
private DragTarget dragTarget = DragTarget.NONE;
|
||||||
|
private float lastDragX;
|
||||||
|
private float lastDragY;
|
||||||
|
|
||||||
public PlayToolState(SharedInput input) {
|
public PlayToolState(SharedInput input) {
|
||||||
this.input = input;
|
this.input = input;
|
||||||
@@ -34,10 +66,25 @@ public class PlayToolState extends BaseAppState {
|
|||||||
cam = app.getCamera();
|
cam = app.getCamera();
|
||||||
assets = app.getAssetManager();
|
assets = app.getAssetManager();
|
||||||
rootNode = app.getRootNode();
|
rootNode = app.getRootNode();
|
||||||
|
|
||||||
|
tempMarkerNode = buildMarkerNode(new ColorRGBA(0f, 1f, 0.2f, 1f), "temp");
|
||||||
|
permMarkerNode = buildMarkerNode(new ColorRGBA(0.2f, 0.5f, 1f, 1f), "perm");
|
||||||
|
|
||||||
|
tempArrowNode = (Node) tempMarkerNode.getChild("arrow");
|
||||||
|
permArrowNode = (Node) permMarkerNode.getChild("arrow");
|
||||||
|
tempBodyGeom = (Geometry) tempMarkerNode.getChild("body_temp");
|
||||||
|
permBodyGeom = (Geometry) permMarkerNode.getChild("body_perm");
|
||||||
|
tempTipGeom = (Geometry) tempArrowNode.getChild("tip_temp");
|
||||||
|
permTipGeom = (Geometry) permArrowNode.getChild("tip_perm");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override protected void cleanup(Application application) { removeMarker(); }
|
@Override
|
||||||
@Override protected void onEnable() {}
|
protected void cleanup(Application application) {
|
||||||
|
detachMarker(tempMarkerNode);
|
||||||
|
detachMarker(permMarkerNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override protected void onEnable() {}
|
||||||
@Override protected void onDisable() {}
|
@Override protected void onDisable() {}
|
||||||
|
|
||||||
public void setTerrain(TerrainQuad terrain) { this.terrain = terrain; }
|
public void setTerrain(TerrainQuad terrain) { this.terrain = terrain; }
|
||||||
@@ -46,58 +93,281 @@ public class PlayToolState extends BaseAppState {
|
|||||||
public void update(float tpf) {
|
public void update(float tpf) {
|
||||||
if (input.activeLayer != SharedInput.LAYER_PLAY_TOOL) return;
|
if (input.activeLayer != SharedInput.LAYER_PLAY_TOOL) return;
|
||||||
|
|
||||||
|
SharedInput.PlayToolMode mode = input.playToolMode;
|
||||||
|
|
||||||
|
// --- Klick-Auswertung ---
|
||||||
SharedInput.PlayToolClick click;
|
SharedInput.PlayToolClick click;
|
||||||
while ((click = input.playToolClickQueue.poll()) != null) {
|
while ((click = input.playToolClickQueue.poll()) != null) {
|
||||||
handleClick(click);
|
handleClick(click, mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update marker position if spawn changed from text fields
|
// --- Drag ---
|
||||||
if (!Float.isNaN(input.tempSpawnX) && !Float.isNaN(input.tempSpawnZ)) {
|
if (mode == SharedInput.PlayToolMode.EDIT) {
|
||||||
placeMarkerAt(input.tempSpawnX, input.tempSpawnZ);
|
SharedInput.PlayToolDrag drag;
|
||||||
|
while ((drag = input.playToolDragQueue.poll()) != null) {
|
||||||
|
handleDrag(drag);
|
||||||
|
}
|
||||||
|
if (input.playToolMouseUp) {
|
||||||
|
input.playToolMouseUp = false;
|
||||||
|
dragTarget = DragTarget.NONE;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
input.playToolDragQueue.clear();
|
||||||
|
if (input.playToolMouseUp) input.playToolMouseUp = false;
|
||||||
|
dragTarget = DragTarget.NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Marker aktualisieren ---
|
||||||
|
updateMarker(tempMarkerNode, tempArrowNode, input.tempSpawnX, input.tempSpawnZ, input.tempSpawnYaw);
|
||||||
|
updateMarker(permMarkerNode, permArrowNode, input.permSpawnX, input.permSpawnZ, input.permSpawnYaw);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleClick(SharedInput.PlayToolClick click) {
|
// ── Click-Handler ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void handleClick(SharedInput.PlayToolClick click, SharedInput.PlayToolMode mode) {
|
||||||
float jmeX = click.screenX() * (float) input.viewportScaleX;
|
float jmeX = click.screenX() * (float) input.viewportScaleX;
|
||||||
float jmeY = cam.getHeight() - click.screenY() * (float) input.viewportScaleY;
|
float jmeY = cam.getHeight() - click.screenY() * (float) input.viewportScaleY;
|
||||||
|
Vector2f screen = new Vector2f(jmeX, jmeY);
|
||||||
|
|
||||||
Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f);
|
Ray ray = new Ray(cam.getWorldCoordinates(screen, 0f),
|
||||||
Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f);
|
cam.getWorldCoordinates(screen, 1f).subtractLocal(
|
||||||
Ray ray = new Ray(near, far.subtract(near).normalizeLocal());
|
cam.getWorldCoordinates(screen, 0f)).normalizeLocal());
|
||||||
|
|
||||||
if (terrain == null) return;
|
if (mode == SharedInput.PlayToolMode.SET_TEMP) {
|
||||||
|
Vector3f pt = hitTerrain(ray);
|
||||||
|
if (pt != null) {
|
||||||
|
input.tempSpawnX = pt.x;
|
||||||
|
input.tempSpawnZ = pt.z;
|
||||||
|
input.pickedSpawnInfo = pt.x + "|" + pt.z;
|
||||||
|
input.spawnPickChanged = true;
|
||||||
|
input.playToolMode = SharedInput.PlayToolMode.NONE;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode == SharedInput.PlayToolMode.SET_PERM) {
|
||||||
|
Vector3f pt = hitTerrain(ray);
|
||||||
|
if (pt != null) {
|
||||||
|
input.permSpawnX = pt.x;
|
||||||
|
input.permSpawnZ = pt.z;
|
||||||
|
input.pickedPermSpawnInfo = pt.x + "|" + pt.z;
|
||||||
|
input.permSpawnChanged = true;
|
||||||
|
input.playToolMode = SharedInput.PlayToolMode.NONE;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode == SharedInput.PlayToolMode.EDIT) {
|
||||||
|
lastDragX = click.screenX();
|
||||||
|
lastDragY = click.screenY();
|
||||||
|
dragTarget = detectDragTarget(ray);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private DragTarget detectDragTarget(Ray ray) {
|
||||||
|
// Pfeilspitzen zuerst prüfen (kleineres Ziel, höhere Priorität)
|
||||||
|
if (tempTipGeom != null && !Float.isNaN(input.tempSpawnX)) {
|
||||||
|
CollisionResults res = new CollisionResults();
|
||||||
|
tempTipGeom.collideWith(ray, res);
|
||||||
|
if (res.size() > 0) return DragTarget.ROT_TEMP;
|
||||||
|
}
|
||||||
|
if (permTipGeom != null && !Float.isNaN(input.permSpawnX)) {
|
||||||
|
CollisionResults res = new CollisionResults();
|
||||||
|
permTipGeom.collideWith(ray, res);
|
||||||
|
if (res.size() > 0) return DragTarget.ROT_PERM;
|
||||||
|
}
|
||||||
|
if (tempBodyGeom != null && !Float.isNaN(input.tempSpawnX)) {
|
||||||
|
CollisionResults res = new CollisionResults();
|
||||||
|
tempBodyGeom.collideWith(ray, res);
|
||||||
|
if (res.size() > 0) return DragTarget.POS_TEMP;
|
||||||
|
}
|
||||||
|
if (permBodyGeom != null && !Float.isNaN(input.permSpawnX)) {
|
||||||
|
CollisionResults res = new CollisionResults();
|
||||||
|
permBodyGeom.collideWith(ray, res);
|
||||||
|
if (res.size() > 0) return DragTarget.POS_PERM;
|
||||||
|
}
|
||||||
|
return DragTarget.NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Drag-Handler ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void handleDrag(SharedInput.PlayToolDrag drag) {
|
||||||
|
if (dragTarget == DragTarget.NONE) return;
|
||||||
|
|
||||||
|
float jmeX = drag.screenX() * (float) input.viewportScaleX;
|
||||||
|
float jmeY = cam.getHeight() - drag.screenY() * (float) input.viewportScaleY;
|
||||||
|
Vector2f screen = new Vector2f(jmeX, jmeY);
|
||||||
|
Ray ray = new Ray(cam.getWorldCoordinates(screen, 0f),
|
||||||
|
cam.getWorldCoordinates(screen, 1f).subtractLocal(
|
||||||
|
cam.getWorldCoordinates(screen, 0f)).normalizeLocal());
|
||||||
|
|
||||||
|
switch (dragTarget) {
|
||||||
|
case POS_TEMP -> {
|
||||||
|
Vector3f pt = hitTerrain(ray);
|
||||||
|
if (pt != null) {
|
||||||
|
input.tempSpawnX = pt.x;
|
||||||
|
input.tempSpawnZ = pt.z;
|
||||||
|
input.pickedSpawnInfo = pt.x + "|" + pt.z;
|
||||||
|
input.spawnPickChanged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case POS_PERM -> {
|
||||||
|
Vector3f pt = hitTerrain(ray);
|
||||||
|
if (pt != null) {
|
||||||
|
input.permSpawnX = pt.x;
|
||||||
|
input.permSpawnZ = pt.z;
|
||||||
|
input.pickedPermSpawnInfo = pt.x + "|" + pt.z;
|
||||||
|
input.permSpawnChanged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case ROT_TEMP -> {
|
||||||
|
float markerY = terrainY(input.tempSpawnX, input.tempSpawnZ);
|
||||||
|
Vector3f proj = projectOnHorizontalPlane(ray, markerY);
|
||||||
|
if (proj != null) {
|
||||||
|
float dx = proj.x - input.tempSpawnX;
|
||||||
|
float dz = proj.z - input.tempSpawnZ;
|
||||||
|
if (dx * dx + dz * dz > 0.01f) {
|
||||||
|
// spawnYaw: 0=+Z, 90=+X → atan2(dx, dz)
|
||||||
|
float yaw = (float) Math.toDegrees(Math.atan2(dx, dz));
|
||||||
|
input.tempSpawnYaw = ((yaw % 360f) + 360f) % 360f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case ROT_PERM -> {
|
||||||
|
float markerY = terrainY(input.permSpawnX, input.permSpawnZ);
|
||||||
|
Vector3f proj = projectOnHorizontalPlane(ray, markerY);
|
||||||
|
if (proj != null) {
|
||||||
|
float dx = proj.x - input.permSpawnX;
|
||||||
|
float dz = proj.z - input.permSpawnZ;
|
||||||
|
if (dx * dx + dz * dz > 0.01f) {
|
||||||
|
float yaw = (float) Math.toDegrees(Math.atan2(dx, dz));
|
||||||
|
input.permSpawnYaw = ((yaw % 360f) + 360f) % 360f;
|
||||||
|
input.permSpawnChanged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default -> {}
|
||||||
|
}
|
||||||
|
|
||||||
|
lastDragX = drag.screenX();
|
||||||
|
lastDragY = drag.screenY();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Marker aufbauen ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private Node buildMarkerNode(ColorRGBA color, String id) {
|
||||||
|
Node markerNode = new Node("marker_" + id);
|
||||||
|
|
||||||
|
// Basis-Scheibe: JME3-Cylinder liegt entlang Z → rotate(-90°, 0, 0) dreht Z→Y (flach in XZ-Ebene)
|
||||||
|
Cylinder disc = new Cylinder(8, 20, MARKER_RADIUS, 0.05f, true);
|
||||||
|
Geometry body = new Geometry("body_" + id, disc);
|
||||||
|
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||||
|
mat.setColor("Color", color);
|
||||||
|
body.setMaterial(mat);
|
||||||
|
body.rotate(-FastMath.HALF_PI, 0f, 0f);
|
||||||
|
markerNode.attachChild(body);
|
||||||
|
|
||||||
|
// Pfeil-Node (wird nach Yaw rotiert; local Y → Yaw-Richtung, local Z → Terrain-Normal)
|
||||||
|
Node arrowNode = new Node("arrow");
|
||||||
|
markerNode.attachChild(arrowNode);
|
||||||
|
|
||||||
|
// Schaft: JME3-Cylinder entlang Z → rotate(-90°, 0, 0) dreht Z→arrowNode-Y (= Yaw-Richtung)
|
||||||
|
Cylinder shaft = new Cylinder(4, 8, SHAFT_RADIUS, SHAFT_LEN, true);
|
||||||
|
Geometry shaftGeom = new Geometry("shaft_" + id, shaft);
|
||||||
|
shaftGeom.setMaterial(mat);
|
||||||
|
shaftGeom.rotate(-FastMath.HALF_PI, 0f, 0f);
|
||||||
|
shaftGeom.setLocalTranslation(0f, SHAFT_LEN * 0.5f, 0f);
|
||||||
|
arrowNode.attachChild(shaftGeom);
|
||||||
|
|
||||||
|
// Pfeilspitze: Kegel mit breiter Basis bei z=-TIP_LEN/2 → nach rotate: Basis bei y=SHAFT_LEN (Schaft-Ende)
|
||||||
|
Cylinder tip = new Cylinder(4, 8, TIP_RADIUS, 0.001f, TIP_LEN, true, false);
|
||||||
|
Geometry tipGeom = new Geometry("tip_" + id, tip);
|
||||||
|
tipGeom.setMaterial(mat);
|
||||||
|
tipGeom.rotate(-FastMath.HALF_PI, 0f, 0f);
|
||||||
|
tipGeom.setLocalTranslation(0f, SHAFT_LEN + TIP_LEN * 0.5f, 0f);
|
||||||
|
arrowNode.attachChild(tipGeom);
|
||||||
|
|
||||||
|
return markerNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Marker-Positions- / Rotations-Update ─────────────────────────────────
|
||||||
|
|
||||||
|
private void updateMarker(Node markerNode, Node arrowNode, float x, float z, float yawDeg) {
|
||||||
|
if (Float.isNaN(x) || Float.isNaN(z)) {
|
||||||
|
if (markerNode.getParent() != null) rootNode.detachChild(markerNode);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (markerNode.getParent() == null) rootNode.attachChild(markerNode);
|
||||||
|
|
||||||
|
float y = terrainY(x, z);
|
||||||
|
markerNode.setLocalTranslation(x, y + 0.06f, z);
|
||||||
|
|
||||||
|
if (arrowNode == null) return;
|
||||||
|
|
||||||
|
// Terrain-Normale als lokale Oben-Richtung
|
||||||
|
Vector3f up = terrainNormal(x, z);
|
||||||
|
|
||||||
|
// Yaw-Richtung als horizontaler Richtungsvektor (0=+Z, 90=+X)
|
||||||
|
float rad = yawDeg * FastMath.DEG_TO_RAD;
|
||||||
|
Vector3f flatForward = new Vector3f(FastMath.sin(rad), 0f, FastMath.cos(rad));
|
||||||
|
|
||||||
|
// right = up × flatForward (liegt auf der Terrain-Oberfläche, senkrecht zur Richtung)
|
||||||
|
Vector3f right = up.cross(flatForward);
|
||||||
|
if (right.lengthSquared() < 1e-6f) {
|
||||||
|
// Sonderfall: Terrain fast senkrecht → horizontale Rotation als Fallback
|
||||||
|
arrowNode.setLocalRotation(new Quaternion().fromAngles(FastMath.HALF_PI, rad, 0f));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
right.normalizeLocal();
|
||||||
|
// forward = right × up (auf Terrain-Fläche projiziertes Forward in Yaw-Richtung)
|
||||||
|
Vector3f forward = right.cross(up).normalizeLocal();
|
||||||
|
|
||||||
|
// Rotationsmatrix: local X → right, local Y → forward (Pfeil), local Z → up (Terrain-Normal)
|
||||||
|
Matrix3f rot = new Matrix3f();
|
||||||
|
rot.setColumn(0, right);
|
||||||
|
rot.setColumn(1, forward);
|
||||||
|
rot.setColumn(2, up);
|
||||||
|
arrowNode.setLocalRotation(new Quaternion().fromRotationMatrix(rot));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Terrain-Hilfsmethoden ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private Vector3f hitTerrain(Ray ray) {
|
||||||
|
if (terrain == null) return null;
|
||||||
CollisionResults hits = new CollisionResults();
|
CollisionResults hits = new CollisionResults();
|
||||||
terrain.collideWith(ray, hits);
|
terrain.collideWith(ray, hits);
|
||||||
if (hits.size() == 0) return;
|
if (hits.size() == 0) return null;
|
||||||
|
return hits.getClosestCollision().getContactPoint();
|
||||||
Vector3f pt = hits.getClosestCollision().getContactPoint();
|
|
||||||
input.tempSpawnX = pt.x;
|
|
||||||
input.tempSpawnZ = pt.z;
|
|
||||||
input.pickedSpawnInfo = pt.x + "|" + pt.z;
|
|
||||||
input.spawnPickChanged = true;
|
|
||||||
placeMarkerAt(pt.x, pt.z);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void placeMarkerAt(float x, float z) {
|
private float terrainY(float x, float z) {
|
||||||
float y = terrain != null ? terrain.getHeight(new Vector2f(x, z)) : 0f;
|
if (terrain == null) return 0f;
|
||||||
if (Float.isNaN(y)) y = 0f;
|
float h = terrain.getHeight(new Vector2f(x, z));
|
||||||
|
return Float.isNaN(h) ? 0f : h;
|
||||||
if (spawnMarker == null) {
|
|
||||||
Cylinder cyl = new Cylinder(8, 16, 0.4f, 0.1f, true);
|
|
||||||
spawnMarker = new Geometry("spawn_marker", cyl);
|
|
||||||
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
|
||||||
mat.setColor("Color", new ColorRGBA(0f, 1f, 0f, 1f));
|
|
||||||
spawnMarker.setMaterial(mat);
|
|
||||||
spawnMarker.rotate(FastMath.HALF_PI, 0, 0);
|
|
||||||
rootNode.attachChild(spawnMarker);
|
|
||||||
}
|
|
||||||
spawnMarker.setLocalTranslation(x, y + 0.05f, z);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void removeMarker() {
|
/** Terrain-Normale an (x, z) durch Kreuzprodukt zweier Tangenten. */
|
||||||
if (spawnMarker != null) {
|
private Vector3f terrainNormal(float x, float z) {
|
||||||
rootNode.detachChild(spawnMarker);
|
if (terrain == null) return Vector3f.UNIT_Y.clone();
|
||||||
spawnMarker = null;
|
float d = 0.5f;
|
||||||
|
float h00 = terrainY(x, z);
|
||||||
|
Vector3f tx = new Vector3f(d, terrainY(x + d, z) - h00, 0f);
|
||||||
|
Vector3f tz = new Vector3f(0f, terrainY(x, z + d) - h00, d);
|
||||||
|
return tz.cross(tx).normalizeLocal();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Schneidet den Kamera-Ray mit der horizontalen Ebene Y=planeY. Null wenn kein Treffer. */
|
||||||
|
private static Vector3f projectOnHorizontalPlane(Ray ray, float planeY) {
|
||||||
|
float dY = ray.getDirection().y;
|
||||||
|
if (Math.abs(dY) < 1e-6f) return null;
|
||||||
|
float t = (planeY - ray.getOrigin().y) / dY;
|
||||||
|
if (t < 0f) return null;
|
||||||
|
return ray.getOrigin().add(ray.getDirection().mult(t));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void detachMarker(Node markerNode) {
|
||||||
|
if (markerNode != null && markerNode.getParent() != null) {
|
||||||
|
rootNode.detachChild(markerNode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -458,6 +458,10 @@ public class TerrainEditorState extends BaseAppState {
|
|||||||
input.voxelFlatSlot = loadedMapData.voxelFlatSlot;
|
input.voxelFlatSlot = loadedMapData.voxelFlatSlot;
|
||||||
input.voxelSteepSlot = loadedMapData.voxelSteepSlot;
|
input.voxelSteepSlot = loadedMapData.voxelSteepSlot;
|
||||||
input.voxelCeilSlot = loadedMapData.voxelCeilSlot;
|
input.voxelCeilSlot = loadedMapData.voxelCeilSlot;
|
||||||
|
input.permSpawnX = loadedMapData.spawnX;
|
||||||
|
input.permSpawnZ = loadedMapData.spawnZ;
|
||||||
|
input.permSpawnYaw = loadedMapData.spawnYaw;
|
||||||
|
input.permSpawnChanged = true;
|
||||||
input.voxelTexturesChanged = true;
|
input.voxelTexturesChanged = true;
|
||||||
// Alte Gebirge-Splatmap-Migration: R=255 überall war der Gebirge-Standard.
|
// Alte Gebirge-Splatmap-Migration: R=255 überall war der Gebirge-Standard.
|
||||||
// Im neuen 1-Terrain-System bedeutet das: Slot-5-Textur deckt alles ab → auf 0 setzen.
|
// Im neuen 1-Terrain-System bedeutet das: Slot-5-Textur deckt alles ab → auf 0 setzen.
|
||||||
@@ -1210,6 +1214,16 @@ public class TerrainEditorState extends BaseAppState {
|
|||||||
data.voxelFlatSlot = voxelFlatSlot;
|
data.voxelFlatSlot = voxelFlatSlot;
|
||||||
data.voxelSteepSlot = voxelSteepSlot;
|
data.voxelSteepSlot = voxelSteepSlot;
|
||||||
data.voxelCeilSlot = voxelCeilSlot;
|
data.voxelCeilSlot = voxelCeilSlot;
|
||||||
|
// Permanenten Spawnpunkt übernehmen (falls gesetzt), sonst aus geladenem MapData
|
||||||
|
if (!Float.isNaN(input.permSpawnX) && !Float.isNaN(input.permSpawnZ)) {
|
||||||
|
data.spawnX = input.permSpawnX;
|
||||||
|
data.spawnZ = input.permSpawnZ;
|
||||||
|
data.spawnYaw = input.permSpawnYaw;
|
||||||
|
} else if (loadedMapData != null) {
|
||||||
|
data.spawnX = loadedMapData.spawnX;
|
||||||
|
data.spawnZ = loadedMapData.spawnZ;
|
||||||
|
data.spawnYaw = loadedMapData.spawnYaw;
|
||||||
|
}
|
||||||
|
|
||||||
if (grassData != null) {
|
if (grassData != null) {
|
||||||
try { GrassTuftIO.save(grassData); }
|
try { GrassTuftIO.save(grassData); }
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ import java.nio.file.Path;
|
|||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Crafting-Table-Verwaltung: zwei identische TablePanel-Instanzen nebeneinander.
|
* Crafting-Table-Verwaltung: Liste links, Formular rechts.
|
||||||
* Pro CraftingTableType kann genau ein Eintrag existieren — die Liste zeigt
|
* Pro CraftingTableType kann genau ein Eintrag existieren — die Liste zeigt
|
||||||
* immer alle 5 Typen; nicht konfigurierte Einträge erscheinen grau.
|
* immer alle Typen; nicht konfigurierte Einträge erscheinen grau.
|
||||||
*/
|
*/
|
||||||
public class CraftingTableEditorView extends BorderPane {
|
public class CraftingTableEditorView extends BorderPane {
|
||||||
|
|
||||||
@@ -22,21 +22,188 @@ public class CraftingTableEditorView extends BorderPane {
|
|||||||
new EnumMap<>(CraftingTable.CraftingTableType.class);
|
new EnumMap<>(CraftingTable.CraftingTableType.class);
|
||||||
private final Path tableDir;
|
private final Path tableDir;
|
||||||
|
|
||||||
private TablePanel left;
|
// ── List ──────────────────────────────────────────────────────────────────
|
||||||
private TablePanel right;
|
|
||||||
|
private ListView<CraftingTable.CraftingTableType> listView;
|
||||||
|
private Button deleteBtn;
|
||||||
|
private CraftingTable.CraftingTableType currentType = null;
|
||||||
|
|
||||||
|
// ── Form fields ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private Label formTypeLabel;
|
||||||
|
private TextField nameIdField;
|
||||||
|
private TextField objectPathField;
|
||||||
|
private VBox formContainer;
|
||||||
|
|
||||||
public CraftingTableEditorView(Path tableDir) {
|
public CraftingTableEditorView(Path tableDir) {
|
||||||
this.tableDir = tableDir;
|
this.tableDir = tableDir;
|
||||||
setStyle("-fx-background-color: #1e1e2e;");
|
setStyle("-fx-background-color: #1e1e2e;");
|
||||||
reloadMap();
|
reloadMap();
|
||||||
|
|
||||||
left = new TablePanel("Liste 1", shared, tableDir, this::onSaved);
|
SplitPane split = new SplitPane(buildListPanel(), buildFormPanel());
|
||||||
right = new TablePanel("Liste 2", shared, tableDir, this::onSaved);
|
split.setDividerPositions(0.28);
|
||||||
|
setCenter(split);
|
||||||
|
}
|
||||||
|
|
||||||
HBox panels = new HBox(1, left, right);
|
// ── List panel ────────────────────────────────────────────────────────────
|
||||||
HBox.setHgrow(left, Priority.ALWAYS);
|
|
||||||
HBox.setHgrow(right, Priority.ALWAYS);
|
private VBox buildListPanel() {
|
||||||
setCenter(panels);
|
listView = new ListView<>();
|
||||||
|
listView.getItems().setAll(CraftingTable.CraftingTableType.values());
|
||||||
|
listView.setStyle("-fx-background-color: #1a1a2a; -fx-control-inner-background: #1a1a2a;");
|
||||||
|
listView.setCellFactory(lv -> new ListCell<>() {
|
||||||
|
@Override protected void updateItem(CraftingTable.CraftingTableType type, boolean empty) {
|
||||||
|
super.updateItem(type, empty);
|
||||||
|
if (empty || type == null) { setText(null); setStyle(""); return; }
|
||||||
|
boolean configured = shared.containsKey(type);
|
||||||
|
String color = typeColor(type);
|
||||||
|
String suffix = configured ? " ✓" : " —";
|
||||||
|
setText(type.name() + suffix);
|
||||||
|
String fillColor = configured ? "#dddddd" : "#666666";
|
||||||
|
setStyle("-fx-text-fill: " + fillColor + ";"
|
||||||
|
+ " -fx-border-color: transparent transparent transparent " + color + ";"
|
||||||
|
+ " -fx-border-width: 0 0 0 3; -fx-padding: 3 6 3 8;");
|
||||||
|
setTooltip(new Tooltip(type.name() + (configured ? " – konfiguriert" : " – nicht konfiguriert")));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
listView.getSelectionModel().selectedItemProperty()
|
||||||
|
.addListener((obs, old, nw) -> onTypeSelected(old, nw));
|
||||||
|
VBox.setVgrow(listView, Priority.ALWAYS);
|
||||||
|
|
||||||
|
deleteBtn = new Button("Konfiguration löschen");
|
||||||
|
deleteBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
deleteBtn.setStyle("-fx-background-color: #6a2a2a; -fx-text-fill: white;");
|
||||||
|
deleteBtn.setDisable(true);
|
||||||
|
deleteBtn.setOnAction(e -> deleteSelected());
|
||||||
|
|
||||||
|
Button refreshBtn = new Button("↺ Neu laden");
|
||||||
|
refreshBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
refreshBtn.setOnAction(e -> { reloadMap(); refresh(); });
|
||||||
|
|
||||||
|
VBox panel = new VBox(6, listView, deleteBtn, refreshBtn);
|
||||||
|
VBox.setVgrow(listView, Priority.ALWAYS);
|
||||||
|
panel.setPadding(new Insets(8));
|
||||||
|
panel.setStyle("-fx-background-color: #1a1a2a;");
|
||||||
|
return panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Form panel ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private ScrollPane buildFormPanel() {
|
||||||
|
formContainer = buildForm();
|
||||||
|
formContainer.setDisable(true);
|
||||||
|
ScrollPane scroll = new ScrollPane(formContainer);
|
||||||
|
scroll.setFitToWidth(true);
|
||||||
|
scroll.setStyle("-fx-background-color: #252535; -fx-background: #252535;");
|
||||||
|
return scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
private VBox buildForm() {
|
||||||
|
VBox form = new VBox(6);
|
||||||
|
form.setPadding(new Insets(12));
|
||||||
|
form.setStyle("-fx-background-color: #252535;");
|
||||||
|
|
||||||
|
formTypeLabel = new Label("—");
|
||||||
|
formTypeLabel.setStyle("-fx-font-weight: bold; -fx-font-size: 13; -fx-text-fill: #ccddff;");
|
||||||
|
|
||||||
|
nameIdField = new TextField();
|
||||||
|
nameIdField.setPromptText("Text-Referenz ID (z. B. ui.crafting.alchemy_table)");
|
||||||
|
|
||||||
|
objectPathField = new TextField();
|
||||||
|
objectPathField.setPromptText("Asset-Pfad zum 3D-Objekt (z. B. Models/crafting/alchemy_table.j3o)");
|
||||||
|
|
||||||
|
Button saveBtn = new Button("Crafting Table speichern");
|
||||||
|
saveBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
saveBtn.setStyle("-fx-font-weight: bold; -fx-background-color: #3a5a8a; -fx-text-fill: white;");
|
||||||
|
saveBtn.setOnAction(e -> saveCurrentTable());
|
||||||
|
|
||||||
|
form.getChildren().addAll(
|
||||||
|
formTypeLabel,
|
||||||
|
new Separator(),
|
||||||
|
sectionTitle("Bezeichnung"),
|
||||||
|
row("Name-ID:", nameIdField),
|
||||||
|
new Separator(),
|
||||||
|
sectionTitle("3D-Objekt"),
|
||||||
|
row("Pfad:", objectPathField),
|
||||||
|
new Separator(),
|
||||||
|
saveBtn
|
||||||
|
);
|
||||||
|
return form;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Form load / save ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void onTypeSelected(CraftingTable.CraftingTableType old, CraftingTable.CraftingTableType nw) {
|
||||||
|
currentType = nw;
|
||||||
|
if (nw == null) {
|
||||||
|
formContainer.setDisable(true);
|
||||||
|
deleteBtn.setDisable(true);
|
||||||
|
clearForm();
|
||||||
|
} else {
|
||||||
|
formContainer.setDisable(false);
|
||||||
|
loadFormFromType(nw);
|
||||||
|
deleteBtn.setDisable(!shared.containsKey(nw));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadFormFromType(CraftingTable.CraftingTableType type) {
|
||||||
|
String color = typeColor(type);
|
||||||
|
formTypeLabel.setText(type.name());
|
||||||
|
formTypeLabel.setStyle("-fx-font-weight: bold; -fx-font-size: 13; -fx-text-fill: " + color + ";");
|
||||||
|
|
||||||
|
CraftingTable t = shared.get(type);
|
||||||
|
if (t != null) {
|
||||||
|
nameIdField.setText(t.getName() != null ? t.getName().id() : "");
|
||||||
|
objectPathField.setText(t.getObject() != null ? safe(t.getObject().getPath()) : "");
|
||||||
|
} else {
|
||||||
|
clearFormFields();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveCurrentTable() {
|
||||||
|
if (currentType == null) return;
|
||||||
|
CraftingTable t = shared.getOrDefault(currentType, new CraftingTable());
|
||||||
|
t.setType(currentType);
|
||||||
|
|
||||||
|
String nameId = nameIdField.getText().trim();
|
||||||
|
t.setName(nameId.isBlank() ? null : new TextReference(nameId));
|
||||||
|
|
||||||
|
String objPath = objectPathField.getText().trim();
|
||||||
|
t.setObject(objPath.isBlank() ? null : new ObjectReference(objPath));
|
||||||
|
|
||||||
|
try {
|
||||||
|
CraftingTableIO.save(t, tableDir);
|
||||||
|
} catch (IOException e) {
|
||||||
|
new Alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reloadMap();
|
||||||
|
refresh();
|
||||||
|
listView.getSelectionModel().select(currentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteSelected() {
|
||||||
|
if (currentType == null) return;
|
||||||
|
try {
|
||||||
|
CraftingTableIO.delete(currentType, tableDir);
|
||||||
|
} catch (IOException e) {
|
||||||
|
new Alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reloadMap();
|
||||||
|
refresh();
|
||||||
|
listView.getSelectionModel().select(currentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearForm() {
|
||||||
|
formTypeLabel.setText("—");
|
||||||
|
formTypeLabel.setStyle("-fx-font-weight: bold; -fx-font-size: 13; -fx-text-fill: #ccddff;");
|
||||||
|
clearFormFields();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearFormFields() {
|
||||||
|
nameIdField.clear();
|
||||||
|
objectPathField.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void reloadMap() {
|
private void reloadMap() {
|
||||||
@@ -44,251 +211,44 @@ public class CraftingTableEditorView extends BorderPane {
|
|||||||
shared.putAll(CraftingTableIO.loadAll(tableDir));
|
shared.putAll(CraftingTableIO.loadAll(tableDir));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onSaved() {
|
private void refresh() {
|
||||||
reloadMap();
|
listView.refresh();
|
||||||
left.refresh();
|
if (currentType != null) {
|
||||||
right.refresh();
|
deleteBtn.setDisable(!shared.containsKey(currentType));
|
||||||
|
if (!formContainer.isDisable()) loadFormFromType(currentType);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Single panel ──────────────────────────────────────────────────────────
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
static class TablePanel extends VBox {
|
static String typeColor(CraftingTable.CraftingTableType type) {
|
||||||
|
if (type == null) return "#666666";
|
||||||
private final Map<CraftingTable.CraftingTableType, CraftingTable> shared;
|
return switch (type) {
|
||||||
private final Path tableDir;
|
case AlchemyTable -> "#44bb88";
|
||||||
private final Runnable onSaved;
|
case EnchantmentTable -> "#aa55ee";
|
||||||
|
case Smithy -> "#cc8833";
|
||||||
private final ListView<CraftingTable.CraftingTableType> listView;
|
case Goldsmiths -> "#ddbb22";
|
||||||
|
case Workshop -> "#4488cc";
|
||||||
private CraftingTable.CraftingTableType currentType = null;
|
case Fireplace -> "#ee6633";
|
||||||
|
case Kitchen -> "#88aa44";
|
||||||
// Form fields
|
};
|
||||||
private TextField nameIdField;
|
|
||||||
private TextField objectPathField;
|
|
||||||
|
|
||||||
private VBox formContainer;
|
|
||||||
private Button deleteBtn;
|
|
||||||
private Label formTypeLabel;
|
|
||||||
|
|
||||||
TablePanel(String title,
|
|
||||||
Map<CraftingTable.CraftingTableType, CraftingTable> shared,
|
|
||||||
Path tableDir, Runnable onSaved) {
|
|
||||||
this.shared = shared;
|
|
||||||
this.tableDir = tableDir;
|
|
||||||
this.onSaved = onSaved;
|
|
||||||
|
|
||||||
setStyle("-fx-background-color: #252535; -fx-border-color: #3a3a4a; -fx-border-width: 0 1 0 0;");
|
|
||||||
|
|
||||||
// ── Header ────────────────────────────────────────────────────────
|
|
||||||
Label titleLbl = new Label(title);
|
|
||||||
titleLbl.setStyle("-fx-font-weight: bold; -fx-font-size: 13; -fx-text-fill: #aaccee;");
|
|
||||||
Button refreshBtn = new Button("↺");
|
|
||||||
refreshBtn.setStyle("-fx-background-color: transparent; -fx-text-fill: #888; -fx-cursor: hand;");
|
|
||||||
refreshBtn.setOnAction(e -> onSaved.run());
|
|
||||||
HBox header = new HBox(8, titleLbl, refreshBtn);
|
|
||||||
header.setPadding(new Insets(8, 10, 8, 10));
|
|
||||||
header.setAlignment(Pos.CENTER_LEFT);
|
|
||||||
header.setStyle("-fx-background-color: #1a1a2a; -fx-border-color: #444;"
|
|
||||||
+ " -fx-border-width: 0 0 1 0;");
|
|
||||||
|
|
||||||
// ── Type list (always 5 fixed entries) ───────────────────────────
|
|
||||||
listView = new ListView<>();
|
|
||||||
listView.getItems().setAll(CraftingTable.CraftingTableType.values());
|
|
||||||
listView.setPrefHeight(160);
|
|
||||||
listView.setStyle("-fx-background-color: #1a1a2a; -fx-control-inner-background: #1a1a2a;");
|
|
||||||
listView.setCellFactory(lv -> new ListCell<>() {
|
|
||||||
@Override
|
|
||||||
protected void updateItem(CraftingTable.CraftingTableType type, boolean empty) {
|
|
||||||
super.updateItem(type, empty);
|
|
||||||
if (empty || type == null) { setText(null); setStyle(""); return; }
|
|
||||||
boolean configured = shared.containsKey(type);
|
|
||||||
String color = typeColor(type);
|
|
||||||
String suffix = configured ? " ✓" : " —";
|
|
||||||
setText(type.name() + suffix);
|
|
||||||
String fillColor = configured ? "#dddddd" : "#666666";
|
|
||||||
setStyle("-fx-text-fill: " + fillColor + ";"
|
|
||||||
+ " -fx-border-color: transparent transparent transparent " + color + ";"
|
|
||||||
+ " -fx-border-width: 0 0 0 3; -fx-padding: 3 6 3 8;");
|
|
||||||
setTooltip(new Tooltip(type.name() + (configured ? " – konfiguriert" : " – nicht konfiguriert")));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
listView.getSelectionModel().selectedItemProperty()
|
|
||||||
.addListener((obs, old, nw) -> onTypeSelected(old, nw));
|
|
||||||
|
|
||||||
deleteBtn = new Button("Konfiguration löschen");
|
|
||||||
deleteBtn.setMaxWidth(Double.MAX_VALUE);
|
|
||||||
deleteBtn.setStyle("-fx-background-color: #6a2a2a; -fx-text-fill: white;");
|
|
||||||
deleteBtn.setDisable(true);
|
|
||||||
deleteBtn.setOnAction(e -> deleteSelected());
|
|
||||||
|
|
||||||
VBox listSection = new VBox(listView, deleteBtn);
|
|
||||||
listSection.setPadding(new Insets(0, 0, 4, 0));
|
|
||||||
listSection.setStyle("-fx-background-color: #1a1a2a;");
|
|
||||||
VBox.setMargin(deleteBtn, new Insets(4, 8, 4, 8));
|
|
||||||
|
|
||||||
// ── Form ──────────────────────────────────────────────────────────
|
|
||||||
formContainer = buildForm();
|
|
||||||
formContainer.setDisable(true);
|
|
||||||
|
|
||||||
ScrollPane formScroll = new ScrollPane(formContainer);
|
|
||||||
formScroll.setFitToWidth(true);
|
|
||||||
formScroll.setStyle("-fx-background-color: #252535; -fx-background: #252535;");
|
|
||||||
VBox.setVgrow(formScroll, Priority.ALWAYS);
|
|
||||||
|
|
||||||
getChildren().addAll(header, listSection, new Separator(), formScroll);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Form construction ─────────────────────────────────────────────────
|
|
||||||
|
|
||||||
private VBox buildForm() {
|
|
||||||
VBox form = new VBox(6);
|
|
||||||
form.setPadding(new Insets(10));
|
|
||||||
form.setStyle("-fx-background-color: #252535;");
|
|
||||||
|
|
||||||
formTypeLabel = new Label("—");
|
|
||||||
formTypeLabel.setStyle("-fx-font-weight: bold; -fx-font-size: 13; -fx-text-fill: #ccddff;");
|
|
||||||
|
|
||||||
nameIdField = new TextField();
|
|
||||||
nameIdField.setPromptText("Text-Referenz ID (z. B. ui.crafting.alchemy_table)");
|
|
||||||
|
|
||||||
objectPathField = new TextField();
|
|
||||||
objectPathField.setPromptText("Asset-Pfad zum 3D-Objekt (z. B. Models/crafting/alchemy_table.j3o)");
|
|
||||||
|
|
||||||
Button saveBtn = new Button("Crafting Table speichern");
|
|
||||||
saveBtn.setMaxWidth(Double.MAX_VALUE);
|
|
||||||
saveBtn.setStyle("-fx-font-weight: bold; -fx-background-color: #3a5a8a; -fx-text-fill: white;");
|
|
||||||
saveBtn.setOnAction(e -> saveCurrentTable());
|
|
||||||
|
|
||||||
form.getChildren().addAll(
|
|
||||||
formTypeLabel,
|
|
||||||
new Separator(),
|
|
||||||
sectionTitle("Bezeichnung"),
|
|
||||||
row("Name-ID:", nameIdField),
|
|
||||||
new Separator(),
|
|
||||||
sectionTitle("3D-Objekt"),
|
|
||||||
row("Pfad:", objectPathField),
|
|
||||||
new Separator(),
|
|
||||||
saveBtn
|
|
||||||
);
|
|
||||||
return form;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Form load / save ──────────────────────────────────────────────────
|
|
||||||
|
|
||||||
private void onTypeSelected(CraftingTable.CraftingTableType old, CraftingTable.CraftingTableType nw) {
|
|
||||||
currentType = nw;
|
|
||||||
if (nw == null) {
|
|
||||||
formContainer.setDisable(true);
|
|
||||||
deleteBtn.setDisable(true);
|
|
||||||
clearForm();
|
|
||||||
} else {
|
|
||||||
formContainer.setDisable(false);
|
|
||||||
loadFormFromType(nw);
|
|
||||||
deleteBtn.setDisable(!shared.containsKey(nw));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void loadFormFromType(CraftingTable.CraftingTableType type) {
|
|
||||||
String color = typeColor(type);
|
|
||||||
formTypeLabel.setText(type.name());
|
|
||||||
formTypeLabel.setStyle("-fx-font-weight: bold; -fx-font-size: 13;"
|
|
||||||
+ " -fx-text-fill: " + color + ";");
|
|
||||||
|
|
||||||
CraftingTable t = shared.get(type);
|
|
||||||
if (t != null) {
|
|
||||||
nameIdField.setText(t.getName() != null ? t.getName().id() : "");
|
|
||||||
objectPathField.setText(t.getObject() != null ? safe(t.getObject().getPath()) : "");
|
|
||||||
} else {
|
|
||||||
clearFormFields();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void saveCurrentTable() {
|
|
||||||
if (currentType == null) return;
|
|
||||||
|
|
||||||
CraftingTable t = shared.getOrDefault(currentType, new CraftingTable());
|
|
||||||
t.setType(currentType);
|
|
||||||
|
|
||||||
String nameId = nameIdField.getText().trim();
|
|
||||||
t.setName(nameId.isBlank() ? null : new TextReference(nameId));
|
|
||||||
|
|
||||||
String objPath = objectPathField.getText().trim();
|
|
||||||
t.setObject(objPath.isBlank() ? null : new ObjectReference(objPath));
|
|
||||||
|
|
||||||
try {
|
|
||||||
CraftingTableIO.save(t, tableDir);
|
|
||||||
} catch (IOException e) {
|
|
||||||
new Alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
onSaved.run();
|
|
||||||
listView.getSelectionModel().select(currentType);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void deleteSelected() {
|
|
||||||
if (currentType == null) return;
|
|
||||||
try {
|
|
||||||
CraftingTableIO.delete(currentType, tableDir);
|
|
||||||
} catch (IOException e) {
|
|
||||||
new Alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
onSaved.run();
|
|
||||||
listView.getSelectionModel().select(currentType);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Called by the parent view when shared data has been reloaded. */
|
|
||||||
void refresh() {
|
|
||||||
listView.refresh();
|
|
||||||
if (currentType != null) {
|
|
||||||
deleteBtn.setDisable(!shared.containsKey(currentType));
|
|
||||||
if (formContainer.isDisable()) return;
|
|
||||||
loadFormFromType(currentType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void clearForm() {
|
|
||||||
formTypeLabel.setText("—");
|
|
||||||
formTypeLabel.setStyle("-fx-font-weight: bold; -fx-font-size: 13; -fx-text-fill: #ccddff;");
|
|
||||||
clearFormFields();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void clearFormFields() {
|
|
||||||
nameIdField.clear();
|
|
||||||
objectPathField.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Helpers ───────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
static String typeColor(CraftingTable.CraftingTableType type) {
|
|
||||||
if (type == null) return "#666666";
|
|
||||||
return switch (type) {
|
|
||||||
case AlchemyTable -> "#44bb88";
|
|
||||||
case EnchantmentTable -> "#aa55ee";
|
|
||||||
case Smithy -> "#cc8833";
|
|
||||||
case Goldsmiths -> "#ddbb22";
|
|
||||||
case Workshop -> "#4488cc";
|
|
||||||
case Fireplace -> "#ee6633";
|
|
||||||
case Kitchen -> "#88aa44";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Label sectionTitle(String text) {
|
|
||||||
Label l = new Label(text);
|
|
||||||
l.setStyle("-fx-font-weight: bold; -fx-font-size: 12; -fx-text-fill: #88aacc;");
|
|
||||||
return l;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static HBox row(String labelText, Node control) {
|
|
||||||
Label lbl = new Label(labelText);
|
|
||||||
lbl.setMinWidth(60);
|
|
||||||
lbl.setStyle("-fx-text-fill: #aaa;");
|
|
||||||
HBox.setHgrow(control, Priority.ALWAYS);
|
|
||||||
HBox box = new HBox(8, lbl, control);
|
|
||||||
box.setAlignment(Pos.CENTER_LEFT);
|
|
||||||
return box;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String safe(String s) { return s != null ? s : ""; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Label sectionTitle(String text) {
|
||||||
|
Label l = new Label(text);
|
||||||
|
l.setStyle("-fx-font-weight: bold; -fx-font-size: 12; -fx-text-fill: #88aacc;");
|
||||||
|
return l;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HBox row(String labelText, Node control) {
|
||||||
|
Label lbl = new Label(labelText);
|
||||||
|
lbl.setMinWidth(60);
|
||||||
|
lbl.setStyle("-fx-text-fill: #aaa;");
|
||||||
|
HBox.setHgrow(control, Priority.ALWAYS);
|
||||||
|
HBox box = new HBox(8, lbl, control);
|
||||||
|
box.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
return box;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String safe(String s) { return s != null ? s : ""; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,13 @@ package de.blight.editor.ui;
|
|||||||
|
|
||||||
import de.blight.common.model.*;
|
import de.blight.common.model.*;
|
||||||
import de.blight.common.model.quests.Quest;
|
import de.blight.common.model.quests.Quest;
|
||||||
|
import de.blight.common.model.quests.QuestIO;
|
||||||
import de.blight.common.model.QuestRef;
|
import de.blight.common.model.QuestRef;
|
||||||
|
import de.blight.editor.ProjectRoot;
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.geometry.Orientation;
|
import javafx.geometry.Orientation;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.Cursor;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
import javafx.scene.layout.*;
|
import javafx.scene.layout.*;
|
||||||
@@ -16,6 +19,7 @@ import javafx.scene.shape.Rectangle;
|
|||||||
import javafx.scene.text.Text;
|
import javafx.scene.text.Text;
|
||||||
import javafx.stage.Modality;
|
import javafx.stage.Modality;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -89,8 +93,14 @@ public class DialogEditorView extends BorderPane {
|
|||||||
collectOptions(opt);
|
collectOptions(opt);
|
||||||
if (opt.getId() != null) rootIds.add(opt.getId());
|
if (opt.getId() != null) rootIds.add(opt.getId());
|
||||||
}
|
}
|
||||||
normalizeReferences();
|
|
||||||
}
|
}
|
||||||
|
// Restore orphaned options that were preserved separately
|
||||||
|
if (npc.getEditorOnlyOptions() != null) {
|
||||||
|
for (DialogOption opt : npc.getEditorOnlyOptions()) {
|
||||||
|
collectOptions(opt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
normalizeReferences();
|
||||||
refreshOptionList();
|
refreshOptionList();
|
||||||
clearDetailForm();
|
clearDetailForm();
|
||||||
}
|
}
|
||||||
@@ -112,6 +122,26 @@ public class DialogEditorView extends BorderPane {
|
|||||||
if (opt != null) roots.add(opt);
|
if (opt != null) roots.add(opt);
|
||||||
}
|
}
|
||||||
npc.setCurrentOptions(roots.isEmpty() ? null : roots);
|
npc.setCurrentOptions(roots.isEmpty() ? null : roots);
|
||||||
|
|
||||||
|
// Collect orphaned options (not reachable from any root) and preserve them
|
||||||
|
Set<String> reachable = new HashSet<>();
|
||||||
|
for (DialogOption root : roots) collectReachable(root, reachable);
|
||||||
|
List<DialogOption> orphans = new ArrayList<>();
|
||||||
|
for (Map.Entry<String, DialogOption> entry : allOptions.entrySet()) {
|
||||||
|
if (!reachable.contains(entry.getKey())) orphans.add(entry.getValue());
|
||||||
|
}
|
||||||
|
npc.setEditorOnlyOptions(orphans.isEmpty() ? null : orphans);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void collectReachable(DialogOption opt, Set<String> visited) {
|
||||||
|
if (opt == null || opt.getId() == null || visited.contains(opt.getId())) return;
|
||||||
|
visited.add(opt.getId());
|
||||||
|
if (opt.getNextOptions() != null) {
|
||||||
|
for (DialogOption next : opt.getNextOptions()) collectReachable(next, visited);
|
||||||
|
}
|
||||||
|
if (opt.getDisablesOptions() != null) {
|
||||||
|
for (DialogOption dis : opt.getDisablesOptions()) collectReachable(dis, visited);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Top-bar ────────────────────────────────────────────────────────────────
|
// ── Top-bar ────────────────────────────────────────────────────────────────
|
||||||
@@ -147,7 +177,12 @@ public class DialogEditorView extends BorderPane {
|
|||||||
private SplitPane buildListPane() {
|
private SplitPane buildListPane() {
|
||||||
// ── Left: option list ─────────────────────────────────────────────────
|
// ── Left: option list ─────────────────────────────────────────────────
|
||||||
optionListView = new ListView<>();
|
optionListView = new ListView<>();
|
||||||
|
optionListView.setStyle("-fx-control-inner-background: #2a2a3a; -fx-text-fill: #ddd;"
|
||||||
|
+ " -fx-selection-bar: #3a5a8a; -fx-selection-bar-text: white;");
|
||||||
optionListView.setCellFactory(lv -> new ListCell<>() {
|
optionListView.setCellFactory(lv -> new ListCell<>() {
|
||||||
|
{
|
||||||
|
selectedProperty().addListener((obs, old, sel) -> applyStyle());
|
||||||
|
}
|
||||||
@Override protected void updateItem(String id, boolean empty) {
|
@Override protected void updateItem(String id, boolean empty) {
|
||||||
super.updateItem(id, empty);
|
super.updateItem(id, empty);
|
||||||
if (empty || id == null) { setText(null); setStyle(""); return; }
|
if (empty || id == null) { setText(null); setStyle(""); return; }
|
||||||
@@ -155,7 +190,14 @@ public class DialogEditorView extends BorderPane {
|
|||||||
String lbl = (opt != null && opt.getLabel() != null && !opt.getLabel().isBlank())
|
String lbl = (opt != null && opt.getLabel() != null && !opt.getLabel().isBlank())
|
||||||
? opt.getLabel() : id.substring(0, Math.min(8, id.length())) + "…";
|
? opt.getLabel() : id.substring(0, Math.min(8, id.length())) + "…";
|
||||||
setText((rootIds.contains(id) ? "★ " : " ") + lbl);
|
setText((rootIds.contains(id) ? "★ " : " ") + lbl);
|
||||||
setStyle("-fx-text-fill: " + (rootIds.contains(id) ? "#ffdd88" : "#cccccc") + ";");
|
applyStyle();
|
||||||
|
}
|
||||||
|
private void applyStyle() {
|
||||||
|
String id = getItem();
|
||||||
|
if (id == null || isEmpty()) { setStyle(""); return; }
|
||||||
|
String text = rootIds.contains(id) ? "#ffdd88" : "#cccccc";
|
||||||
|
String bg = isSelected() ? "#3a5a8a" : "#2a2a3a";
|
||||||
|
setStyle("-fx-background-color: " + bg + "; -fx-text-fill: " + text + ";");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
optionListView.getSelectionModel().selectedItemProperty().addListener(
|
optionListView.getSelectionModel().selectedItemProperty().addListener(
|
||||||
@@ -236,9 +278,16 @@ public class DialogEditorView extends BorderPane {
|
|||||||
statusCombo.setMaxWidth(Double.MAX_VALUE);
|
statusCombo.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
|
||||||
questOpenField = new TextField();
|
questOpenField = new TextField();
|
||||||
questOpenField.setPromptText("Quest-ID");
|
questOpenField.setPromptText("Quest wählen…");
|
||||||
|
questOpenField.setEditable(false);
|
||||||
|
questOpenField.setCursor(Cursor.HAND);
|
||||||
|
questOpenField.setOnMouseClicked(e -> pickQuestId(questOpenField));
|
||||||
|
|
||||||
questCompleteField = new TextField();
|
questCompleteField = new TextField();
|
||||||
questCompleteField.setPromptText("Quest-ID");
|
questCompleteField.setPromptText("Quest wählen…");
|
||||||
|
questCompleteField.setEditable(false);
|
||||||
|
questCompleteField.setCursor(Cursor.HAND);
|
||||||
|
questCompleteField.setOnMouseClicked(e -> pickQuestId(questCompleteField));
|
||||||
|
|
||||||
form.getChildren().addAll(
|
form.getChildren().addAll(
|
||||||
sectionTitle("Voraussetzungen"),
|
sectionTitle("Voraussetzungen"),
|
||||||
@@ -284,16 +333,23 @@ public class DialogEditorView extends BorderPane {
|
|||||||
|
|
||||||
// Quests
|
// Quests
|
||||||
recvQuestField = new TextField();
|
recvQuestField = new TextField();
|
||||||
recvQuestField.setPromptText("Quest-ID");
|
recvQuestField.setPromptText("Quest wählen…");
|
||||||
|
recvQuestField.setEditable(false);
|
||||||
|
recvQuestField.setCursor(Cursor.HAND);
|
||||||
|
recvQuestField.setOnMouseClicked(e -> pickQuestId(recvQuestField));
|
||||||
|
|
||||||
fulfillsQuestField = new TextField();
|
fulfillsQuestField = new TextField();
|
||||||
fulfillsQuestField.setPromptText("Quest-ID");
|
fulfillsQuestField.setPromptText("Quest wählen…");
|
||||||
|
fulfillsQuestField.setEditable(false);
|
||||||
|
fulfillsQuestField.setCursor(Cursor.HAND);
|
||||||
|
fulfillsQuestField.setOnMouseClicked(e -> pickQuestId(fulfillsQuestField));
|
||||||
|
|
||||||
abortsQuestsView = new ListView<>();
|
abortsQuestsView = new ListView<>();
|
||||||
abortsQuestsView.setPrefHeight(80);
|
abortsQuestsView.setPrefHeight(80);
|
||||||
abortsQuestsView.setStyle("-fx-background-color: #1e1e2e; -fx-control-inner-background: #1e1e2e;");
|
abortsQuestsView.setStyle("-fx-background-color: #1e1e2e; -fx-control-inner-background: #1e1e2e;");
|
||||||
Button addAbortsBtn = smallBtn("+");
|
Button addAbortsBtn = smallBtn("+");
|
||||||
Button delAbortsBtn = smallBtn("−");
|
Button delAbortsBtn = smallBtn("−");
|
||||||
addAbortsBtn.setOnAction(e -> promptQuestId(abortsQuestsView));
|
addAbortsBtn.setOnAction(e -> pickQuestIdForList(abortsQuestsView));
|
||||||
delAbortsBtn.setOnAction(e -> removeSelected(abortsQuestsView));
|
delAbortsBtn.setOnAction(e -> removeSelected(abortsQuestsView));
|
||||||
|
|
||||||
form.getChildren().addAll(
|
form.getChildren().addAll(
|
||||||
@@ -518,7 +574,12 @@ public class DialogEditorView extends BorderPane {
|
|||||||
DialogOption opt = new DialogOption();
|
DialogOption opt = new DialogOption();
|
||||||
opt.setLabel("Neue Option");
|
opt.setLabel("Neue Option");
|
||||||
allOptions.put(opt.getId(), opt);
|
allOptions.put(opt.getId(), opt);
|
||||||
if (asRoot) rootIds.add(opt.getId());
|
if (asRoot) {
|
||||||
|
rootIds.add(opt.getId());
|
||||||
|
} else if (selectedId != null) {
|
||||||
|
// Auto-link to currently selected option so no orphans are created
|
||||||
|
nextOptionsView.getItems().add(opt.getId());
|
||||||
|
}
|
||||||
refreshOptionList();
|
refreshOptionList();
|
||||||
optionListView.getSelectionModel().select(opt.getId());
|
optionListView.getSelectionModel().select(opt.getId());
|
||||||
}
|
}
|
||||||
@@ -580,14 +641,63 @@ public class DialogEditorView extends BorderPane {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void promptQuestId(ListView<String> target) {
|
private void pickQuestId(TextField target) {
|
||||||
TextInputDialog dlg = new TextInputDialog();
|
String chosen = showQuestPickerDialog();
|
||||||
dlg.setTitle("Quest-ID");
|
if (chosen != null) target.setText(chosen);
|
||||||
dlg.setHeaderText("Quest-ID eingeben:");
|
}
|
||||||
|
|
||||||
|
private void pickQuestIdForList(ListView<String> target) {
|
||||||
|
String chosen = showQuestPickerDialog();
|
||||||
|
if (chosen != null && !target.getItems().contains(chosen)) target.getItems().add(chosen);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String showQuestPickerDialog() {
|
||||||
|
Path questDir = ProjectRoot.resolve("blight-assets", "src", "main", "resources").resolve("quests");
|
||||||
|
List<Quest> questList = QuestIO.loadAll(questDir);
|
||||||
|
|
||||||
|
Dialog<String> dlg = new Dialog<>();
|
||||||
|
dlg.setTitle("Quest auswählen");
|
||||||
dlg.initModality(Modality.APPLICATION_MODAL);
|
dlg.initModality(Modality.APPLICATION_MODAL);
|
||||||
dlg.showAndWait().ifPresent(id -> {
|
|
||||||
if (!id.isBlank() && !target.getItems().contains(id)) target.getItems().add(id);
|
ListView<Quest> chooser = new ListView<>();
|
||||||
|
chooser.setCellFactory(lv -> new ListCell<>() {
|
||||||
|
@Override protected void updateItem(Quest q, boolean empty) {
|
||||||
|
super.updateItem(q, empty);
|
||||||
|
if (empty || q == null) { setText(null); return; }
|
||||||
|
String id = q.getQuestId() != null ? q.getQuestId() : "—";
|
||||||
|
String name = q.getText() != null ? q.getText().id() : "";
|
||||||
|
setText(name.isBlank() ? id : id + " — " + name);
|
||||||
|
setStyle("-fx-text-fill: #cccccc;");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
chooser.getItems().setAll(questList);
|
||||||
|
chooser.setStyle("-fx-background-color: #1a1a2a; -fx-control-inner-background: #1a1a2a;"
|
||||||
|
+ " -fx-selection-bar: #3a5a8a;");
|
||||||
|
chooser.setPrefSize(380, 300);
|
||||||
|
|
||||||
|
if (questList.isEmpty()) {
|
||||||
|
Label hint = new Label("Keine Quests gefunden.\nQuests im Quest-Editor anlegen.");
|
||||||
|
hint.setStyle("-fx-text-fill: #888; -fx-font-style: italic;");
|
||||||
|
dlg.getDialogPane().setContent(hint);
|
||||||
|
} else {
|
||||||
|
dlg.getDialogPane().setContent(chooser);
|
||||||
|
}
|
||||||
|
dlg.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
|
||||||
|
dlg.getDialogPane().setStyle("-fx-background-color: #252535;");
|
||||||
|
|
||||||
|
Button okBtn = (Button) dlg.getDialogPane().lookupButton(ButtonType.OK);
|
||||||
|
okBtn.setDisable(true);
|
||||||
|
chooser.getSelectionModel().selectedItemProperty()
|
||||||
|
.addListener((obs, o, n) -> okBtn.setDisable(n == null));
|
||||||
|
chooser.setOnMouseClicked(e -> {
|
||||||
|
if (e.getClickCount() == 2 && !chooser.getSelectionModel().isEmpty()) okBtn.fire();
|
||||||
|
});
|
||||||
|
dlg.setResultConverter(bt -> {
|
||||||
|
if (bt != ButtonType.OK) return null;
|
||||||
|
Quest sel = chooser.getSelectionModel().getSelectedItem();
|
||||||
|
return sel != null ? sel.getQuestId() : null;
|
||||||
|
});
|
||||||
|
return dlg.showAndWait().orElse(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void removeSelected(ListView<String> list) {
|
private static void removeSelected(ListView<String> list) {
|
||||||
|
|||||||
@@ -15,288 +15,262 @@ import java.nio.file.Path;
|
|||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fraktions-Verwaltung: zwei identische FractionPanel-Instanzen nebeneinander.
|
* Fraktions-Verwaltung: Liste links, Formular rechts.
|
||||||
* Sortiert nach Name-ID, dann nach UUID.
|
* Sortiert nach Name-ID, dann nach UUID.
|
||||||
*/
|
*/
|
||||||
public class FractionEditorView extends BorderPane {
|
public class FractionEditorView extends BorderPane {
|
||||||
|
|
||||||
private final ObservableList<Fraction> sharedFractions = FXCollections.observableArrayList();
|
private final ObservableList<Fraction> fractions = FXCollections.observableArrayList();
|
||||||
private final Path fractionDir;
|
private final Path fractionDir;
|
||||||
|
|
||||||
|
// ── List ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private final SortedList<Fraction> sortedFractions;
|
||||||
|
private ListView<Fraction> listView;
|
||||||
|
private Button deleteBtn;
|
||||||
|
private Fraction current = null;
|
||||||
|
|
||||||
|
// ── Form fields ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private Label idLabel;
|
||||||
|
private TextField nameField;
|
||||||
|
private TextField maleMemberField;
|
||||||
|
private TextField femaleMemberField;
|
||||||
|
private TextField rank1Field;
|
||||||
|
private TextField rank2Field;
|
||||||
|
private TextField rank3Field;
|
||||||
|
private VBox formContainer;
|
||||||
|
|
||||||
public FractionEditorView(Path fractionDir) {
|
public FractionEditorView(Path fractionDir) {
|
||||||
this.fractionDir = fractionDir;
|
this.fractionDir = fractionDir;
|
||||||
|
this.sortedFractions = new SortedList<>(fractions, FractionIO.SORT_ORDER);
|
||||||
setStyle("-fx-background-color: #1e1e2e;");
|
setStyle("-fx-background-color: #1e1e2e;");
|
||||||
reload();
|
reload();
|
||||||
|
|
||||||
FractionPanel left = new FractionPanel("Liste 1", sharedFractions, fractionDir, this::reload);
|
SplitPane split = new SplitPane(buildListPanel(), buildFormPanel());
|
||||||
FractionPanel right = new FractionPanel("Liste 2", sharedFractions, fractionDir, this::reload);
|
split.setDividerPositions(0.28);
|
||||||
|
setCenter(split);
|
||||||
|
}
|
||||||
|
|
||||||
HBox panels = new HBox(1, left, right);
|
// ── List panel ────────────────────────────────────────────────────────────
|
||||||
HBox.setHgrow(left, Priority.ALWAYS);
|
|
||||||
HBox.setHgrow(right, Priority.ALWAYS);
|
private VBox buildListPanel() {
|
||||||
setCenter(panels);
|
listView = new ListView<>(sortedFractions);
|
||||||
|
listView.setStyle("-fx-background-color: #1a1a2a; -fx-control-inner-background: #1a1a2a;");
|
||||||
|
listView.setCellFactory(lv -> new ListCell<>() {
|
||||||
|
@Override protected void updateItem(Fraction f, boolean empty) {
|
||||||
|
super.updateItem(f, empty);
|
||||||
|
if (empty || f == null) { setText(null); setStyle(""); return; }
|
||||||
|
String name = f.getName() != null ? f.getName().id() : "—";
|
||||||
|
setText(name);
|
||||||
|
setTooltip(new Tooltip("ID: " + (f.getFractionId() != null ? f.getFractionId() : "?")));
|
||||||
|
setStyle("-fx-text-fill: #dddddd;"
|
||||||
|
+ " -fx-border-color: transparent transparent transparent #6699cc;"
|
||||||
|
+ " -fx-border-width: 0 0 0 3; -fx-padding: 3 6 3 8;");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
listView.getSelectionModel().selectedItemProperty()
|
||||||
|
.addListener((obs, old, nw) -> onFractionSelected(old, nw));
|
||||||
|
VBox.setVgrow(listView, Priority.ALWAYS);
|
||||||
|
|
||||||
|
Button newBtn = new Button("Neue Fraktion");
|
||||||
|
newBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
newBtn.setStyle("-fx-background-color: #3a6a3a; -fx-text-fill: white;");
|
||||||
|
newBtn.setOnAction(e -> createFraction());
|
||||||
|
|
||||||
|
deleteBtn = new Button("Löschen");
|
||||||
|
deleteBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
deleteBtn.setStyle("-fx-background-color: #6a2a2a; -fx-text-fill: white;");
|
||||||
|
deleteBtn.setDisable(true);
|
||||||
|
deleteBtn.setOnAction(e -> deleteSelected());
|
||||||
|
|
||||||
|
Button refreshBtn = new Button("↺ Neu laden");
|
||||||
|
refreshBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
refreshBtn.setOnAction(e -> reload());
|
||||||
|
|
||||||
|
HBox listButtons = new HBox(6, newBtn, deleteBtn);
|
||||||
|
HBox.setHgrow(newBtn, Priority.ALWAYS);
|
||||||
|
HBox.setHgrow(deleteBtn, Priority.ALWAYS);
|
||||||
|
listButtons.setPadding(new Insets(6, 8, 6, 8));
|
||||||
|
|
||||||
|
VBox panel = new VBox(6, listView, listButtons, refreshBtn);
|
||||||
|
VBox.setVgrow(listView, Priority.ALWAYS);
|
||||||
|
panel.setPadding(new Insets(8));
|
||||||
|
panel.setStyle("-fx-background-color: #1a1a2a;");
|
||||||
|
return panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Form panel ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private ScrollPane buildFormPanel() {
|
||||||
|
formContainer = buildForm();
|
||||||
|
formContainer.setDisable(true);
|
||||||
|
ScrollPane scroll = new ScrollPane(formContainer);
|
||||||
|
scroll.setFitToWidth(true);
|
||||||
|
scroll.setStyle("-fx-background-color: #252535; -fx-background: #252535;");
|
||||||
|
return scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
private VBox buildForm() {
|
||||||
|
VBox form = new VBox(6);
|
||||||
|
form.setPadding(new Insets(12));
|
||||||
|
form.setStyle("-fx-background-color: #252535;");
|
||||||
|
|
||||||
|
idLabel = new Label("—");
|
||||||
|
idLabel.setStyle("-fx-font-size: 10; -fx-text-fill: #666; -fx-font-family: monospace;");
|
||||||
|
|
||||||
|
nameField = field("z. B. faction.guards");
|
||||||
|
maleMemberField = field("z. B. faction.guards.member.male");
|
||||||
|
femaleMemberField = field("z. B. faction.guards.member.female");
|
||||||
|
rank1Field = field("z. B. faction.guards.rank1");
|
||||||
|
rank2Field = field("z. B. faction.guards.rank2");
|
||||||
|
rank3Field = field("z. B. faction.guards.rank3");
|
||||||
|
|
||||||
|
Button saveBtn = new Button("Fraktion speichern");
|
||||||
|
saveBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
saveBtn.setStyle("-fx-font-weight: bold; -fx-background-color: #3a5a8a; -fx-text-fill: white;");
|
||||||
|
saveBtn.setOnAction(e -> saveCurrentFraction());
|
||||||
|
|
||||||
|
form.getChildren().addAll(
|
||||||
|
sectionTitle("Kennung"),
|
||||||
|
new Separator(),
|
||||||
|
row("UUID:", idLabel),
|
||||||
|
sectionTitle("Text-Referenzen"),
|
||||||
|
new Separator(),
|
||||||
|
row("Name:", nameField),
|
||||||
|
row("Mitglied (m):", maleMemberField),
|
||||||
|
row("Mitglied (w):", femaleMemberField),
|
||||||
|
sectionTitle("Ränge"),
|
||||||
|
new Separator(),
|
||||||
|
row("Rang 1:", rank1Field),
|
||||||
|
row("Rang 2:", rank2Field),
|
||||||
|
row("Rang 3:", rank3Field),
|
||||||
|
new Separator(),
|
||||||
|
saveBtn
|
||||||
|
);
|
||||||
|
return form;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Form load / save ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void onFractionSelected(Fraction old, Fraction nw) {
|
||||||
|
if (old != null) saveFormToFraction(old);
|
||||||
|
current = nw;
|
||||||
|
deleteBtn.setDisable(nw == null);
|
||||||
|
if (nw != null) {
|
||||||
|
formContainer.setDisable(false);
|
||||||
|
loadFormFromFraction(nw);
|
||||||
|
} else {
|
||||||
|
formContainer.setDisable(true);
|
||||||
|
clearForm();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadFormFromFraction(Fraction f) {
|
||||||
|
idLabel.setText(f.getFractionId() != null ? f.getFractionId().toString() : "—");
|
||||||
|
nameField.setText(textId(f.getName()));
|
||||||
|
maleMemberField.setText(textId(f.getMaleMemberName()));
|
||||||
|
femaleMemberField.setText(textId(f.getFemaleMemberName()));
|
||||||
|
rank1Field.setText(textId(f.getRank1Name()));
|
||||||
|
rank2Field.setText(textId(f.getRank2Name()));
|
||||||
|
rank3Field.setText(textId(f.getRank3Name()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveFormToFraction(Fraction f) {
|
||||||
|
f.setName(ref(nameField.getText()));
|
||||||
|
f.setMaleMemberName(ref(maleMemberField.getText()));
|
||||||
|
f.setFemaleMemberName(ref(femaleMemberField.getText()));
|
||||||
|
f.setRank1Name(ref(rank1Field.getText()));
|
||||||
|
f.setRank2Name(ref(rank2Field.getText()));
|
||||||
|
f.setRank3Name(ref(rank3Field.getText()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearForm() {
|
||||||
|
idLabel.setText("—");
|
||||||
|
nameField.clear();
|
||||||
|
maleMemberField.clear();
|
||||||
|
femaleMemberField.clear();
|
||||||
|
rank1Field.clear();
|
||||||
|
rank2Field.clear();
|
||||||
|
rank3Field.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── List operations ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void createFraction() {
|
||||||
|
Fraction f = new Fraction();
|
||||||
|
f.setFractionId(UUID.randomUUID());
|
||||||
|
fractions.add(f);
|
||||||
|
listView.getSelectionModel().select(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteSelected() {
|
||||||
|
Fraction sel = listView.getSelectionModel().getSelectedItem();
|
||||||
|
if (sel == null) return;
|
||||||
|
UUID id = sel.getFractionId();
|
||||||
|
fractions.remove(sel);
|
||||||
|
try { FractionIO.delete(id, fractionDir); } catch (IOException ignored) {}
|
||||||
|
current = null;
|
||||||
|
clearForm();
|
||||||
|
formContainer.setDisable(true);
|
||||||
|
deleteBtn.setDisable(true);
|
||||||
|
reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveCurrentFraction() {
|
||||||
|
if (current == null) return;
|
||||||
|
saveFormToFraction(current);
|
||||||
|
if (current.getFractionId() == null) {
|
||||||
|
new Alert(Alert.AlertType.ERROR,
|
||||||
|
"Fraktion hat keine UUID – bitte neu erstellen.", ButtonType.OK).showAndWait();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
FractionIO.save(current, fractionDir);
|
||||||
|
} catch (IOException e) {
|
||||||
|
new Alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reload();
|
||||||
|
final UUID fid = current.getFractionId();
|
||||||
|
fractions.stream()
|
||||||
|
.filter(f -> fid.equals(f.getFractionId()))
|
||||||
|
.findFirst()
|
||||||
|
.ifPresent(listView.getSelectionModel()::select);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void reload() {
|
public void reload() {
|
||||||
sharedFractions.setAll(FractionIO.loadAll(fractionDir));
|
fractions.setAll(FractionIO.loadAll(fractionDir));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Single panel ──────────────────────────────────────────────────────────
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
static class FractionPanel extends VBox {
|
private static TextReference ref(String text) {
|
||||||
|
String t = text == null ? "" : text.trim();
|
||||||
|
return t.isBlank() ? null : new TextReference(t);
|
||||||
|
}
|
||||||
|
|
||||||
private final ObservableList<Fraction> fractions;
|
private static String textId(TextReference r) { return r != null ? r.id() : ""; }
|
||||||
private final Path fractionDir;
|
|
||||||
private final Runnable onSaved;
|
|
||||||
|
|
||||||
private final SortedList<Fraction> sortedFractions;
|
private static TextField field(String prompt) {
|
||||||
private final ListView<Fraction> listView;
|
TextField tf = new TextField();
|
||||||
|
tf.setPromptText(prompt);
|
||||||
|
return tf;
|
||||||
|
}
|
||||||
|
|
||||||
private Fraction current = null;
|
private static Label sectionTitle(String text) {
|
||||||
|
Label l = new Label(text);
|
||||||
|
l.setStyle("-fx-font-weight: bold; -fx-font-size: 12; -fx-text-fill: #88aacc;");
|
||||||
|
return l;
|
||||||
|
}
|
||||||
|
|
||||||
// Form fields
|
private static HBox row(String labelText, Node control) {
|
||||||
private Label idLabel;
|
Label lbl = new Label(labelText);
|
||||||
private TextField nameField;
|
lbl.setMinWidth(100);
|
||||||
private TextField maleMemberField;
|
lbl.setStyle("-fx-text-fill: #aaa;");
|
||||||
private TextField femaleMemberField;
|
HBox.setHgrow(control, Priority.ALWAYS);
|
||||||
private TextField rank1Field;
|
HBox box = new HBox(8, lbl, control);
|
||||||
private TextField rank2Field;
|
box.setAlignment(Pos.CENTER_LEFT);
|
||||||
private TextField rank3Field;
|
return box;
|
||||||
|
|
||||||
private VBox formContainer;
|
|
||||||
private Button deleteBtn;
|
|
||||||
|
|
||||||
FractionPanel(String title, ObservableList<Fraction> fractions, Path fractionDir, Runnable onSaved) {
|
|
||||||
this.fractions = fractions;
|
|
||||||
this.fractionDir = fractionDir;
|
|
||||||
this.onSaved = onSaved;
|
|
||||||
|
|
||||||
setStyle("-fx-background-color: #252535; -fx-border-color: #3a3a4a; -fx-border-width: 0 1 0 0;");
|
|
||||||
|
|
||||||
// ── Header ────────────────────────────────────────────────────────
|
|
||||||
Label titleLbl = new Label(title);
|
|
||||||
titleLbl.setStyle("-fx-font-weight: bold; -fx-font-size: 13; -fx-text-fill: #aaccee;");
|
|
||||||
Button refreshBtn = new Button("↺");
|
|
||||||
refreshBtn.setStyle("-fx-background-color: transparent; -fx-text-fill: #888; -fx-cursor: hand;");
|
|
||||||
refreshBtn.setOnAction(e -> onSaved.run());
|
|
||||||
HBox header = new HBox(8, titleLbl, refreshBtn);
|
|
||||||
header.setPadding(new Insets(8, 10, 8, 10));
|
|
||||||
header.setAlignment(Pos.CENTER_LEFT);
|
|
||||||
header.setStyle("-fx-background-color: #1a1a2a; -fx-border-color: #444;"
|
|
||||||
+ " -fx-border-width: 0 0 1 0;");
|
|
||||||
|
|
||||||
// ── Fraction list ─────────────────────────────────────────────────
|
|
||||||
sortedFractions = new SortedList<>(fractions, FractionIO.SORT_ORDER);
|
|
||||||
listView = new ListView<>(sortedFractions);
|
|
||||||
listView.setPrefHeight(180);
|
|
||||||
listView.setStyle("-fx-background-color: #1a1a2a; -fx-control-inner-background: #1a1a2a;");
|
|
||||||
listView.setCellFactory(lv -> new ListCell<>() {
|
|
||||||
@Override protected void updateItem(Fraction f, boolean empty) {
|
|
||||||
super.updateItem(f, empty);
|
|
||||||
if (empty || f == null) { setText(null); setStyle(""); return; }
|
|
||||||
String name = f.getName() != null ? f.getName().id() : "—";
|
|
||||||
String uuid = f.getFractionId() != null ? f.getFractionId().toString().substring(0, 8) + "…" : "?";
|
|
||||||
setText(name);
|
|
||||||
setTooltip(new Tooltip("ID: " + (f.getFractionId() != null ? f.getFractionId() : "?")));
|
|
||||||
setStyle("-fx-text-fill: #dddddd;"
|
|
||||||
+ " -fx-border-color: transparent transparent transparent #6699cc;"
|
|
||||||
+ " -fx-border-width: 0 0 0 3; -fx-padding: 3 6 3 8;");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
listView.getSelectionModel().selectedItemProperty()
|
|
||||||
.addListener((obs, old, nw) -> onFractionSelected(old, nw));
|
|
||||||
|
|
||||||
Button newBtn = new Button("Neue Fraktion");
|
|
||||||
newBtn.setMaxWidth(Double.MAX_VALUE);
|
|
||||||
newBtn.setStyle("-fx-background-color: #3a6a3a; -fx-text-fill: white;");
|
|
||||||
newBtn.setOnAction(e -> createFraction());
|
|
||||||
|
|
||||||
deleteBtn = new Button("Löschen");
|
|
||||||
deleteBtn.setMaxWidth(Double.MAX_VALUE);
|
|
||||||
deleteBtn.setStyle("-fx-background-color: #6a2a2a; -fx-text-fill: white;");
|
|
||||||
deleteBtn.setDisable(true);
|
|
||||||
deleteBtn.setOnAction(e -> deleteSelected());
|
|
||||||
|
|
||||||
HBox listButtons = new HBox(6, newBtn, deleteBtn);
|
|
||||||
listButtons.setPadding(new Insets(6, 8, 6, 8));
|
|
||||||
HBox.setHgrow(newBtn, Priority.ALWAYS);
|
|
||||||
HBox.setHgrow(deleteBtn, Priority.ALWAYS);
|
|
||||||
|
|
||||||
VBox listSection = new VBox(listView, listButtons);
|
|
||||||
listSection.setStyle("-fx-background-color: #1a1a2a;");
|
|
||||||
|
|
||||||
// ── Form ──────────────────────────────────────────────────────────
|
|
||||||
formContainer = buildForm();
|
|
||||||
formContainer.setDisable(true);
|
|
||||||
|
|
||||||
ScrollPane formScroll = new ScrollPane(formContainer);
|
|
||||||
formScroll.setFitToWidth(true);
|
|
||||||
formScroll.setStyle("-fx-background-color: #252535; -fx-background: #252535;");
|
|
||||||
VBox.setVgrow(formScroll, Priority.ALWAYS);
|
|
||||||
|
|
||||||
getChildren().addAll(header, listSection, new Separator(), formScroll);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Form construction ─────────────────────────────────────────────────
|
|
||||||
|
|
||||||
private VBox buildForm() {
|
|
||||||
VBox form = new VBox(6);
|
|
||||||
form.setPadding(new Insets(10));
|
|
||||||
form.setStyle("-fx-background-color: #252535;");
|
|
||||||
|
|
||||||
idLabel = new Label("—");
|
|
||||||
idLabel.setStyle("-fx-font-size: 10; -fx-text-fill: #666; -fx-font-family: monospace;");
|
|
||||||
|
|
||||||
nameField = field("z. B. faction.guards");
|
|
||||||
maleMemberField = field("z. B. faction.guards.member.male");
|
|
||||||
femaleMemberField = field("z. B. faction.guards.member.female");
|
|
||||||
rank1Field = field("z. B. faction.guards.rank1");
|
|
||||||
rank2Field = field("z. B. faction.guards.rank2");
|
|
||||||
rank3Field = field("z. B. faction.guards.rank3");
|
|
||||||
|
|
||||||
Button saveBtn = new Button("Fraktion speichern");
|
|
||||||
saveBtn.setMaxWidth(Double.MAX_VALUE);
|
|
||||||
saveBtn.setStyle("-fx-font-weight: bold; -fx-background-color: #3a5a8a; -fx-text-fill: white;");
|
|
||||||
saveBtn.setOnAction(e -> saveCurrentFraction());
|
|
||||||
|
|
||||||
form.getChildren().addAll(
|
|
||||||
sectionTitle("Kennung"),
|
|
||||||
new Separator(),
|
|
||||||
row("UUID:", idLabel),
|
|
||||||
sectionTitle("Text-Referenzen"),
|
|
||||||
new Separator(),
|
|
||||||
row("Name:", nameField),
|
|
||||||
row("Mitglied (m):", maleMemberField),
|
|
||||||
row("Mitglied (w):", femaleMemberField),
|
|
||||||
sectionTitle("Ränge"),
|
|
||||||
new Separator(),
|
|
||||||
row("Rang 1:", rank1Field),
|
|
||||||
row("Rang 2:", rank2Field),
|
|
||||||
row("Rang 3:", rank3Field),
|
|
||||||
new Separator(),
|
|
||||||
saveBtn
|
|
||||||
);
|
|
||||||
return form;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Form load / save ──────────────────────────────────────────────────
|
|
||||||
|
|
||||||
private void onFractionSelected(Fraction old, Fraction nw) {
|
|
||||||
if (old != null) saveFormToFraction(old);
|
|
||||||
current = nw;
|
|
||||||
deleteBtn.setDisable(nw == null);
|
|
||||||
if (nw != null) {
|
|
||||||
formContainer.setDisable(false);
|
|
||||||
loadFormFromFraction(nw);
|
|
||||||
} else {
|
|
||||||
formContainer.setDisable(true);
|
|
||||||
clearForm();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void loadFormFromFraction(Fraction f) {
|
|
||||||
idLabel.setText(f.getFractionId() != null ? f.getFractionId().toString() : "—");
|
|
||||||
nameField.setText(textId(f.getName()));
|
|
||||||
maleMemberField.setText(textId(f.getMaleMemberName()));
|
|
||||||
femaleMemberField.setText(textId(f.getFemaleMemberName()));
|
|
||||||
rank1Field.setText(textId(f.getRank1Name()));
|
|
||||||
rank2Field.setText(textId(f.getRank2Name()));
|
|
||||||
rank3Field.setText(textId(f.getRank3Name()));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void saveFormToFraction(Fraction f) {
|
|
||||||
f.setName(ref(nameField.getText()));
|
|
||||||
f.setMaleMemberName(ref(maleMemberField.getText()));
|
|
||||||
f.setFemaleMemberName(ref(femaleMemberField.getText()));
|
|
||||||
f.setRank1Name(ref(rank1Field.getText()));
|
|
||||||
f.setRank2Name(ref(rank2Field.getText()));
|
|
||||||
f.setRank3Name(ref(rank3Field.getText()));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void clearForm() {
|
|
||||||
idLabel.setText("—");
|
|
||||||
nameField.clear();
|
|
||||||
maleMemberField.clear();
|
|
||||||
femaleMemberField.clear();
|
|
||||||
rank1Field.clear();
|
|
||||||
rank2Field.clear();
|
|
||||||
rank3Field.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── List operations ───────────────────────────────────────────────────
|
|
||||||
|
|
||||||
private void createFraction() {
|
|
||||||
Fraction f = new Fraction();
|
|
||||||
f.setFractionId(UUID.randomUUID());
|
|
||||||
fractions.add(f);
|
|
||||||
listView.getSelectionModel().select(f);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void deleteSelected() {
|
|
||||||
Fraction sel = listView.getSelectionModel().getSelectedItem();
|
|
||||||
if (sel == null) return;
|
|
||||||
UUID id = sel.getFractionId();
|
|
||||||
fractions.remove(sel);
|
|
||||||
try { FractionIO.delete(id, fractionDir); } catch (IOException ignored) {}
|
|
||||||
current = null;
|
|
||||||
clearForm();
|
|
||||||
formContainer.setDisable(true);
|
|
||||||
deleteBtn.setDisable(true);
|
|
||||||
onSaved.run();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void saveCurrentFraction() {
|
|
||||||
if (current == null) return;
|
|
||||||
saveFormToFraction(current);
|
|
||||||
|
|
||||||
if (current.getFractionId() == null) {
|
|
||||||
new Alert(Alert.AlertType.ERROR,
|
|
||||||
"Fraktion hat keine UUID – bitte neu erstellen.", ButtonType.OK).showAndWait();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
FractionIO.save(current, fractionDir);
|
|
||||||
} catch (IOException e) {
|
|
||||||
new Alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
onSaved.run();
|
|
||||||
final UUID fid = current.getFractionId();
|
|
||||||
fractions.stream()
|
|
||||||
.filter(f -> fid.equals(f.getFractionId()))
|
|
||||||
.findFirst()
|
|
||||||
.ifPresent(listView.getSelectionModel()::select);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Helpers ───────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
private static TextReference ref(String text) {
|
|
||||||
String t = text == null ? "" : text.trim();
|
|
||||||
return t.isBlank() ? null : new TextReference(t);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String textId(TextReference r) { return r != null ? r.id() : ""; }
|
|
||||||
|
|
||||||
private static TextField field(String prompt) {
|
|
||||||
TextField tf = new TextField();
|
|
||||||
tf.setPromptText(prompt);
|
|
||||||
return tf;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Label sectionTitle(String text) {
|
|
||||||
Label l = new Label(text);
|
|
||||||
l.setStyle("-fx-font-weight: bold; -fx-font-size: 12; -fx-text-fill: #88aacc;");
|
|
||||||
return l;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static HBox row(String labelText, Node control) {
|
|
||||||
Label lbl = new Label(labelText);
|
|
||||||
lbl.setMinWidth(100);
|
|
||||||
lbl.setStyle("-fx-text-fill: #aaa;");
|
|
||||||
HBox.setHgrow(control, Priority.ALWAYS);
|
|
||||||
HBox box = new HBox(8, lbl, control);
|
|
||||||
box.setAlignment(Pos.CENTER_LEFT);
|
|
||||||
return box;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,237 +15,239 @@ import java.io.IOException;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Locations-Verwaltung: zwei identische LocationPanel-Instanzen nebeneinander.
|
* Locations-Verwaltung: Liste links, Formular rechts.
|
||||||
* Alle Locations werden gemeinsam in einer Datei gespeichert (LocationIO).
|
* Alle Locations werden gemeinsam in einer Datei gespeichert (LocationIO).
|
||||||
*/
|
*/
|
||||||
public class LocationEditorView extends BorderPane {
|
public class LocationEditorView extends BorderPane {
|
||||||
|
|
||||||
private final ObservableList<Location> sharedLocations = FXCollections.observableArrayList();
|
private final ObservableList<Location> locations = FXCollections.observableArrayList();
|
||||||
|
|
||||||
|
// ── List ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private ListView<Location> listView;
|
||||||
|
private Button deleteBtn;
|
||||||
|
private Location current = null;
|
||||||
|
|
||||||
|
// ── Form fields ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private TextField nameIdField;
|
||||||
|
private TextField centerXField;
|
||||||
|
private TextField centerZField;
|
||||||
|
private TextField radiusField;
|
||||||
|
private TriggerListEditor triggerEditor;
|
||||||
|
private VBox formContainer;
|
||||||
|
|
||||||
public LocationEditorView() {
|
public LocationEditorView() {
|
||||||
setStyle("-fx-background-color: #1e1e2e;");
|
setStyle("-fx-background-color: #1e1e2e;");
|
||||||
reload();
|
reload();
|
||||||
|
|
||||||
LocationPanel left = new LocationPanel("Liste 1", sharedLocations, this::saveAll);
|
SplitPane split = new SplitPane(buildListPanel(), buildFormPanel());
|
||||||
LocationPanel right = new LocationPanel("Liste 2", sharedLocations, this::saveAll);
|
split.setDividerPositions(0.28);
|
||||||
|
setCenter(split);
|
||||||
HBox panels = new HBox(1, left, right);
|
|
||||||
HBox.setHgrow(left, Priority.ALWAYS);
|
|
||||||
HBox.setHgrow(right, Priority.ALWAYS);
|
|
||||||
setCenter(panels);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void reload() {
|
// ── List panel ────────────────────────────────────────────────────────────
|
||||||
try { sharedLocations.setAll(LocationIO.load()); }
|
|
||||||
catch (IOException e) { sharedLocations.clear(); }
|
private VBox buildListPanel() {
|
||||||
|
listView = new ListView<>(locations);
|
||||||
|
listView.setStyle("-fx-background-color: #1a1a2a; -fx-control-inner-background: #1a1a2a;");
|
||||||
|
listView.setCellFactory(lv -> new ListCell<>() {
|
||||||
|
@Override protected void updateItem(Location loc, boolean empty) {
|
||||||
|
super.updateItem(loc, empty);
|
||||||
|
if (empty || loc == null) { setText(null); setStyle(""); return; }
|
||||||
|
setText(loc.getId().isBlank() ? "—" : loc.getId());
|
||||||
|
setStyle("-fx-text-fill: #dddddd;"
|
||||||
|
+ " -fx-border-color: transparent transparent transparent #66aacc;"
|
||||||
|
+ " -fx-border-width: 0 0 0 3; -fx-padding: 3 6 3 8;");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
listView.getSelectionModel().selectedItemProperty()
|
||||||
|
.addListener((obs, old, nw) -> onSelected(old, nw));
|
||||||
|
VBox.setVgrow(listView, Priority.ALWAYS);
|
||||||
|
|
||||||
|
Button newBtn = new Button("Neue Location");
|
||||||
|
newBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
newBtn.setStyle("-fx-background-color: #3a6a3a; -fx-text-fill: white;");
|
||||||
|
newBtn.setOnAction(e -> createLocation());
|
||||||
|
|
||||||
|
deleteBtn = new Button("Löschen");
|
||||||
|
deleteBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
deleteBtn.setStyle("-fx-background-color: #6a2a2a; -fx-text-fill: white;");
|
||||||
|
deleteBtn.setDisable(true);
|
||||||
|
deleteBtn.setOnAction(e -> deleteSelected());
|
||||||
|
|
||||||
|
Button refreshBtn = new Button("↺ Neu laden");
|
||||||
|
refreshBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
refreshBtn.setOnAction(e -> reload());
|
||||||
|
|
||||||
|
HBox listButtons = new HBox(6, newBtn, deleteBtn);
|
||||||
|
HBox.setHgrow(newBtn, Priority.ALWAYS);
|
||||||
|
HBox.setHgrow(deleteBtn, Priority.ALWAYS);
|
||||||
|
listButtons.setPadding(new Insets(6, 8, 6, 8));
|
||||||
|
|
||||||
|
VBox panel = new VBox(6, listView, listButtons, refreshBtn);
|
||||||
|
VBox.setVgrow(listView, Priority.ALWAYS);
|
||||||
|
panel.setPadding(new Insets(8));
|
||||||
|
panel.setStyle("-fx-background-color: #1a1a2a;");
|
||||||
|
return panel;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void saveAll() {
|
// ── Form panel ────────────────────────────────────────────────────────────
|
||||||
try { LocationIO.save(sharedLocations); }
|
|
||||||
catch (IOException e) {
|
private ScrollPane buildFormPanel() {
|
||||||
new Alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait();
|
formContainer = buildForm();
|
||||||
}
|
formContainer.setDisable(true);
|
||||||
|
ScrollPane scroll = new ScrollPane(formContainer);
|
||||||
|
scroll.setFitToWidth(true);
|
||||||
|
scroll.setStyle("-fx-background-color: #252535; -fx-background: #252535;");
|
||||||
|
return scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
private VBox buildForm() {
|
||||||
|
VBox form = new VBox(6);
|
||||||
|
form.setPadding(new Insets(12));
|
||||||
|
form.setStyle("-fx-background-color: #252535;");
|
||||||
|
|
||||||
|
nameIdField = field("z. B. location.village");
|
||||||
|
centerXField = field("X-Koordinate");
|
||||||
|
centerZField = field("Z-Koordinate");
|
||||||
|
radiusField = field("Radius in Meter");
|
||||||
|
|
||||||
|
triggerEditor = new TriggerListEditor(List.of(), () -> {});
|
||||||
|
|
||||||
|
Button saveBtn = new Button("Location speichern");
|
||||||
|
saveBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
saveBtn.setStyle("-fx-font-weight: bold; -fx-background-color: #3a5a8a; -fx-text-fill: white;");
|
||||||
|
saveBtn.setOnAction(e -> saveCurrent());
|
||||||
|
|
||||||
|
form.getChildren().addAll(
|
||||||
|
sectionTitle("Kennung & Position"),
|
||||||
|
new Separator(),
|
||||||
|
row("Name-ID:", nameIdField),
|
||||||
|
row("Mitte X:", centerXField),
|
||||||
|
row("Mitte Z:", centerZField),
|
||||||
|
row("Radius:", radiusField),
|
||||||
|
sectionTitle("Trigger"),
|
||||||
|
new Separator(),
|
||||||
|
triggerEditor,
|
||||||
|
new Separator(),
|
||||||
|
saveBtn
|
||||||
|
);
|
||||||
|
return form;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Form load / save ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void onSelected(Location old, Location nw) {
|
||||||
|
if (old != null) saveFormToLocation(old);
|
||||||
|
current = nw;
|
||||||
|
deleteBtn.setDisable(nw == null);
|
||||||
|
if (nw != null) { formContainer.setDisable(false); loadForm(nw); }
|
||||||
|
else { formContainer.setDisable(true); clearForm(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadForm(Location loc) {
|
||||||
|
nameIdField.setText(loc.getId());
|
||||||
|
centerXField.setText(String.valueOf(loc.getCenterX()));
|
||||||
|
centerZField.setText(String.valueOf(loc.getCenterZ()));
|
||||||
|
radiusField.setText(String.valueOf(loc.getRadius()));
|
||||||
|
|
||||||
|
int idx = formContainer.getChildren().indexOf(triggerEditor);
|
||||||
|
triggerEditor = new TriggerListEditor(
|
||||||
|
loc.getTriggers() != null ? loc.getTriggers() : List.of(), () -> {});
|
||||||
|
if (idx >= 0) formContainer.getChildren().set(idx, triggerEditor);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveFormToLocation(Location loc) {
|
||||||
|
String nameId = nameIdField.getText().trim();
|
||||||
|
loc.setName(nameId.isBlank() ? null : new TextReference(nameId));
|
||||||
|
loc.setCenterX(parseFloat(centerXField.getText()));
|
||||||
|
loc.setCenterZ(parseFloat(centerZField.getText()));
|
||||||
|
loc.setRadius(parseFloat(radiusField.getText()));
|
||||||
|
loc.setTriggers(triggerEditor.getTriggers());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearForm() {
|
||||||
|
nameIdField.clear();
|
||||||
|
centerXField.clear();
|
||||||
|
centerZField.clear();
|
||||||
|
radiusField.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── List operations ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void createLocation() {
|
||||||
|
Location loc = new Location();
|
||||||
|
loc.setName(new TextReference("location.neu_" + System.currentTimeMillis()));
|
||||||
|
locations.add(loc);
|
||||||
|
listView.getSelectionModel().select(loc);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteSelected() {
|
||||||
|
if (current == null) return;
|
||||||
|
locations.remove(current);
|
||||||
|
current = null;
|
||||||
|
clearForm();
|
||||||
|
formContainer.setDisable(true);
|
||||||
|
deleteBtn.setDisable(true);
|
||||||
|
persist();
|
||||||
reload();
|
reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Single panel ──────────────────────────────────────────────────────────
|
private void saveCurrent() {
|
||||||
|
if (current == null) return;
|
||||||
static class LocationPanel extends VBox {
|
saveFormToLocation(current);
|
||||||
|
if (current.getId().isBlank()) {
|
||||||
private final ObservableList<Location> locations;
|
new Alert(Alert.AlertType.ERROR, "Name-ID darf nicht leer sein.", ButtonType.OK).showAndWait();
|
||||||
private final Runnable onSaved;
|
return;
|
||||||
|
|
||||||
private final ListView<Location> listView;
|
|
||||||
private Location current = null;
|
|
||||||
|
|
||||||
// Form fields
|
|
||||||
private TextField nameIdField;
|
|
||||||
private TextField centerXField;
|
|
||||||
private TextField centerZField;
|
|
||||||
private TextField radiusField;
|
|
||||||
private TriggerListEditor triggerEditor;
|
|
||||||
|
|
||||||
private VBox formContainer;
|
|
||||||
private Button deleteBtn;
|
|
||||||
|
|
||||||
LocationPanel(String title, ObservableList<Location> locations, Runnable onSaved) {
|
|
||||||
this.locations = locations;
|
|
||||||
this.onSaved = onSaved;
|
|
||||||
|
|
||||||
setStyle("-fx-background-color: #252535; -fx-border-color: #3a3a4a; -fx-border-width: 0 1 0 0;");
|
|
||||||
|
|
||||||
Label titleLbl = new Label(title);
|
|
||||||
titleLbl.setStyle("-fx-font-weight: bold; -fx-font-size: 13; -fx-text-fill: #aaccee;");
|
|
||||||
Button refreshBtn = new Button("↺");
|
|
||||||
refreshBtn.setStyle("-fx-background-color: transparent; -fx-text-fill: #888; -fx-cursor: hand;");
|
|
||||||
refreshBtn.setOnAction(e -> onSaved.run());
|
|
||||||
HBox header = new HBox(8, titleLbl, refreshBtn);
|
|
||||||
header.setPadding(new Insets(8, 10, 8, 10));
|
|
||||||
header.setAlignment(Pos.CENTER_LEFT);
|
|
||||||
header.setStyle("-fx-background-color: #1a1a2a; -fx-border-color: #444; -fx-border-width: 0 0 1 0;");
|
|
||||||
|
|
||||||
listView = new ListView<>(locations);
|
|
||||||
listView.setPrefHeight(180);
|
|
||||||
listView.setStyle("-fx-background-color: #1a1a2a; -fx-control-inner-background: #1a1a2a;");
|
|
||||||
listView.setCellFactory(lv -> new ListCell<>() {
|
|
||||||
@Override protected void updateItem(Location loc, boolean empty) {
|
|
||||||
super.updateItem(loc, empty);
|
|
||||||
if (empty || loc == null) { setText(null); setStyle(""); return; }
|
|
||||||
setText(loc.getId().isBlank() ? "—" : loc.getId());
|
|
||||||
setStyle("-fx-text-fill: #dddddd;"
|
|
||||||
+ " -fx-border-color: transparent transparent transparent #66aacc;"
|
|
||||||
+ " -fx-border-width: 0 0 0 3; -fx-padding: 3 6 3 8;");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
listView.getSelectionModel().selectedItemProperty()
|
|
||||||
.addListener((obs, old, nw) -> onSelected(old, nw));
|
|
||||||
|
|
||||||
Button newBtn = new Button("Neue Location");
|
|
||||||
newBtn.setMaxWidth(Double.MAX_VALUE);
|
|
||||||
newBtn.setStyle("-fx-background-color: #3a6a3a; -fx-text-fill: white;");
|
|
||||||
newBtn.setOnAction(e -> createLocation());
|
|
||||||
|
|
||||||
deleteBtn = new Button("Löschen");
|
|
||||||
deleteBtn.setMaxWidth(Double.MAX_VALUE);
|
|
||||||
deleteBtn.setStyle("-fx-background-color: #6a2a2a; -fx-text-fill: white;");
|
|
||||||
deleteBtn.setDisable(true);
|
|
||||||
deleteBtn.setOnAction(e -> deleteSelected());
|
|
||||||
|
|
||||||
HBox listButtons = new HBox(6, newBtn, deleteBtn);
|
|
||||||
listButtons.setPadding(new Insets(6, 8, 6, 8));
|
|
||||||
HBox.setHgrow(newBtn, Priority.ALWAYS);
|
|
||||||
HBox.setHgrow(deleteBtn, Priority.ALWAYS);
|
|
||||||
|
|
||||||
VBox listSection = new VBox(listView, listButtons);
|
|
||||||
listSection.setStyle("-fx-background-color: #1a1a2a;");
|
|
||||||
|
|
||||||
formContainer = buildForm();
|
|
||||||
formContainer.setDisable(true);
|
|
||||||
|
|
||||||
ScrollPane formScroll = new ScrollPane(formContainer);
|
|
||||||
formScroll.setFitToWidth(true);
|
|
||||||
formScroll.setStyle("-fx-background-color: #252535; -fx-background: #252535;");
|
|
||||||
VBox.setVgrow(formScroll, Priority.ALWAYS);
|
|
||||||
|
|
||||||
getChildren().addAll(header, listSection, new Separator(), formScroll);
|
|
||||||
}
|
}
|
||||||
|
String savedId = current.getId();
|
||||||
|
persist();
|
||||||
|
reload();
|
||||||
|
locations.stream()
|
||||||
|
.filter(l -> savedId.equals(l.getId()))
|
||||||
|
.findFirst()
|
||||||
|
.ifPresent(listView.getSelectionModel()::select);
|
||||||
|
}
|
||||||
|
|
||||||
private VBox buildForm() {
|
private void persist() {
|
||||||
VBox form = new VBox(6);
|
try { LocationIO.save(locations); }
|
||||||
form.setPadding(new Insets(10));
|
catch (IOException e) {
|
||||||
form.setStyle("-fx-background-color: #252535;");
|
new Alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait();
|
||||||
|
|
||||||
nameIdField = field("z. B. location.village");
|
|
||||||
centerXField = field("X-Koordinate");
|
|
||||||
centerZField = field("Z-Koordinate");
|
|
||||||
radiusField = field("Radius in Meter");
|
|
||||||
|
|
||||||
// TriggerEditor — placeholder; rebuilt when item selected
|
|
||||||
triggerEditor = new TriggerListEditor(List.of(), () -> {});
|
|
||||||
|
|
||||||
Button saveBtn = new Button("Location speichern");
|
|
||||||
saveBtn.setMaxWidth(Double.MAX_VALUE);
|
|
||||||
saveBtn.setStyle("-fx-font-weight: bold; -fx-background-color: #3a5a8a; -fx-text-fill: white;");
|
|
||||||
saveBtn.setOnAction(e -> saveCurrent());
|
|
||||||
|
|
||||||
form.getChildren().addAll(
|
|
||||||
sectionTitle("Kennung & Position"),
|
|
||||||
new Separator(),
|
|
||||||
row("Name-ID:", nameIdField),
|
|
||||||
row("Mitte X:", centerXField),
|
|
||||||
row("Mitte Z:", centerZField),
|
|
||||||
row("Radius:", radiusField),
|
|
||||||
sectionTitle("Trigger"),
|
|
||||||
new Separator(),
|
|
||||||
triggerEditor,
|
|
||||||
new Separator(),
|
|
||||||
saveBtn
|
|
||||||
);
|
|
||||||
return form;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onSelected(Location old, Location nw) {
|
|
||||||
if (old != null) saveFormToLocation(old);
|
|
||||||
current = nw;
|
|
||||||
deleteBtn.setDisable(nw == null);
|
|
||||||
if (nw != null) { formContainer.setDisable(false); loadForm(nw); }
|
|
||||||
else { formContainer.setDisable(true); clearForm(); }
|
|
||||||
}
|
|
||||||
|
|
||||||
private void loadForm(Location loc) {
|
|
||||||
nameIdField.setText(loc.getId());
|
|
||||||
centerXField.setText(String.valueOf(loc.getCenterX()));
|
|
||||||
centerZField.setText(String.valueOf(loc.getCenterZ()));
|
|
||||||
radiusField.setText(String.valueOf(loc.getRadius()));
|
|
||||||
|
|
||||||
// Rebuild trigger editor
|
|
||||||
int idx = formContainer.getChildren().indexOf(triggerEditor);
|
|
||||||
triggerEditor = new TriggerListEditor(
|
|
||||||
loc.getTriggers() != null ? loc.getTriggers() : List.of(), () -> {});
|
|
||||||
if (idx >= 0) formContainer.getChildren().set(idx, triggerEditor);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void saveFormToLocation(Location loc) {
|
|
||||||
String nameId = nameIdField.getText().trim();
|
|
||||||
loc.setName(nameId.isBlank() ? null : new TextReference(nameId));
|
|
||||||
loc.setCenterX(parseFloat(centerXField.getText()));
|
|
||||||
loc.setCenterZ(parseFloat(centerZField.getText()));
|
|
||||||
loc.setRadius(parseFloat(radiusField.getText()));
|
|
||||||
loc.setTriggers(triggerEditor.getTriggers());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void clearForm() {
|
|
||||||
nameIdField.clear(); centerXField.clear(); centerZField.clear(); radiusField.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void createLocation() {
|
|
||||||
Location loc = new Location();
|
|
||||||
loc.setName(new TextReference("location.neu_" + System.currentTimeMillis()));
|
|
||||||
locations.add(loc);
|
|
||||||
listView.getSelectionModel().select(loc);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void deleteSelected() {
|
|
||||||
if (current == null) return;
|
|
||||||
locations.remove(current);
|
|
||||||
current = null; clearForm(); formContainer.setDisable(true); deleteBtn.setDisable(true);
|
|
||||||
onSaved.run();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void saveCurrent() {
|
|
||||||
if (current == null) return;
|
|
||||||
saveFormToLocation(current);
|
|
||||||
if (current.getId().isBlank()) {
|
|
||||||
new Alert(Alert.AlertType.ERROR, "Name-ID darf nicht leer sein.", ButtonType.OK).showAndWait();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
onSaved.run();
|
|
||||||
listView.refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static float parseFloat(String s) {
|
|
||||||
try { return Float.parseFloat(s.trim().replace(',', '.')); }
|
|
||||||
catch (NumberFormatException ignored) { return 0f; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Label sectionTitle(String text) {
|
|
||||||
Label l = new Label(text);
|
|
||||||
l.setStyle("-fx-font-weight: bold; -fx-font-size: 12; -fx-text-fill: #88aacc;");
|
|
||||||
return l;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static HBox row(String labelText, Node control) {
|
|
||||||
Label lbl = new Label(labelText);
|
|
||||||
lbl.setMinWidth(80);
|
|
||||||
lbl.setStyle("-fx-text-fill: #aaa;");
|
|
||||||
HBox.setHgrow(control, Priority.ALWAYS);
|
|
||||||
HBox box = new HBox(8, lbl, control);
|
|
||||||
box.setAlignment(Pos.CENTER_LEFT);
|
|
||||||
return box;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static TextField field(String prompt) {
|
|
||||||
TextField tf = new TextField(); tf.setPromptText(prompt); return tf;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void reload() {
|
||||||
|
try { locations.setAll(LocationIO.load()); }
|
||||||
|
catch (IOException e) { locations.clear(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static float parseFloat(String s) {
|
||||||
|
try { return Float.parseFloat(s.trim().replace(',', '.')); }
|
||||||
|
catch (NumberFormatException ignored) { return 0f; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Label sectionTitle(String text) {
|
||||||
|
Label l = new Label(text);
|
||||||
|
l.setStyle("-fx-font-weight: bold; -fx-font-size: 12; -fx-text-fill: #88aacc;");
|
||||||
|
return l;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HBox row(String labelText, Node control) {
|
||||||
|
Label lbl = new Label(labelText);
|
||||||
|
lbl.setMinWidth(80);
|
||||||
|
lbl.setStyle("-fx-text-fill: #aaa;");
|
||||||
|
HBox.setHgrow(control, Priority.ALWAYS);
|
||||||
|
HBox box = new HBox(8, lbl, control);
|
||||||
|
box.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
return box;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TextField field(String prompt) {
|
||||||
|
TextField tf = new TextField();
|
||||||
|
tf.setPromptText(prompt);
|
||||||
|
return tf;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import de.blight.common.model.quests.*;
|
|||||||
import javafx.collections.FXCollections;
|
import javafx.collections.FXCollections;
|
||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.geometry.Orientation;
|
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
@@ -21,467 +20,454 @@ import java.nio.file.Path;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Quest-Verwaltung: zwei identische QuestPanel-Instanzen nebeneinander.
|
* Quest-Verwaltung: Liste links, Formular rechts.
|
||||||
* Beide Panels teilen die gleiche ObservableList und speichern in dasselbe Verzeichnis.
|
|
||||||
*/
|
*/
|
||||||
public class QuestEditorView extends BorderPane {
|
public class QuestEditorView extends BorderPane {
|
||||||
|
|
||||||
private final ObservableList<Quest> sharedQuests = FXCollections.observableArrayList();
|
private final ObservableList<Quest> quests = FXCollections.observableArrayList();
|
||||||
private final Path questDir;
|
private final Path questDir;
|
||||||
|
|
||||||
|
// ── List ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private ListView<Quest> listView;
|
||||||
|
private Button deleteBtn;
|
||||||
|
private Quest current = null;
|
||||||
|
|
||||||
|
// ── Common form fields ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private TextField idField;
|
||||||
|
private Spinner<Integer> xpSpinner;
|
||||||
|
private TextField textField;
|
||||||
|
private TextField descField;
|
||||||
|
private TextField successField;
|
||||||
|
private ComboBox<String> typeCombo;
|
||||||
|
private VBox dynamicArea;
|
||||||
|
private VBox formContainer;
|
||||||
|
private Button saveBtn;
|
||||||
|
|
||||||
|
// ── Type-specific fields ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private TextField f1, f2, f3;
|
||||||
|
private Spinner<Integer> countSpinner;
|
||||||
|
|
||||||
public QuestEditorView(Path questDir) {
|
public QuestEditorView(Path questDir) {
|
||||||
this.questDir = questDir;
|
this.questDir = questDir;
|
||||||
setStyle("-fx-background-color: #1e1e2e;");
|
setStyle("-fx-background-color: #1e1e2e;");
|
||||||
|
|
||||||
reload();
|
reload();
|
||||||
|
|
||||||
QuestPanel left = new QuestPanel("Liste 1", sharedQuests, questDir, this::reload);
|
SplitPane split = new SplitPane(buildListPanel(), buildFormPanel());
|
||||||
QuestPanel right = new QuestPanel("Liste 2", sharedQuests, questDir, this::reload);
|
split.setDividerPositions(0.28);
|
||||||
|
setCenter(split);
|
||||||
|
}
|
||||||
|
|
||||||
HBox panels = new HBox(1, left, right);
|
// ── List panel ────────────────────────────────────────────────────────────
|
||||||
HBox.setHgrow(left, Priority.ALWAYS);
|
|
||||||
HBox.setHgrow(right, Priority.ALWAYS);
|
private VBox buildListPanel() {
|
||||||
setCenter(panels);
|
listView = new ListView<>(quests);
|
||||||
|
listView.setStyle("-fx-background-color: #1a1a2a; -fx-control-inner-background: #1a1a2a;");
|
||||||
|
listView.setCellFactory(lv -> new ListCell<>() {
|
||||||
|
@Override protected void updateItem(Quest q, boolean empty) {
|
||||||
|
super.updateItem(q, empty);
|
||||||
|
if (empty || q == null) { setText(null); setStyle(""); return; }
|
||||||
|
String id = q.getQuestId() != null ? q.getQuestId() : "—";
|
||||||
|
String type = QuestIO.typeOf(q);
|
||||||
|
setText("[" + type + "] " + id);
|
||||||
|
setStyle("-fx-text-fill: #cccccc;");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
listView.getSelectionModel().selectedItemProperty()
|
||||||
|
.addListener((obs, old, nw) -> onQuestSelected(old, nw));
|
||||||
|
VBox.setVgrow(listView, Priority.ALWAYS);
|
||||||
|
|
||||||
|
Button newBtn = new Button("Neue Quest");
|
||||||
|
newBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
newBtn.setStyle("-fx-background-color: #3a6a3a; -fx-text-fill: white;");
|
||||||
|
newBtn.setOnAction(e -> createQuest());
|
||||||
|
|
||||||
|
deleteBtn = new Button("Löschen");
|
||||||
|
deleteBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
deleteBtn.setStyle("-fx-background-color: #6a2a2a; -fx-text-fill: white;");
|
||||||
|
deleteBtn.setDisable(true);
|
||||||
|
deleteBtn.setOnAction(e -> deleteSelected());
|
||||||
|
|
||||||
|
Button refreshBtn = new Button("↺ Neu laden");
|
||||||
|
refreshBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
refreshBtn.setOnAction(e -> reload());
|
||||||
|
|
||||||
|
HBox listButtons = new HBox(6, newBtn, deleteBtn);
|
||||||
|
HBox.setHgrow(newBtn, Priority.ALWAYS);
|
||||||
|
HBox.setHgrow(deleteBtn, Priority.ALWAYS);
|
||||||
|
listButtons.setPadding(new Insets(6, 8, 6, 8));
|
||||||
|
|
||||||
|
VBox panel = new VBox(6, listView, listButtons, refreshBtn);
|
||||||
|
VBox.setVgrow(listView, Priority.ALWAYS);
|
||||||
|
panel.setPadding(new Insets(8));
|
||||||
|
panel.setStyle("-fx-background-color: #1a1a2a;");
|
||||||
|
return panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Form panel ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private ScrollPane buildFormPanel() {
|
||||||
|
formContainer = buildForm();
|
||||||
|
formContainer.setDisable(true);
|
||||||
|
|
||||||
|
ScrollPane scroll = new ScrollPane(formContainer);
|
||||||
|
scroll.setFitToWidth(true);
|
||||||
|
scroll.setStyle("-fx-background-color: #252535; -fx-background: #252535;");
|
||||||
|
return scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
private VBox buildForm() {
|
||||||
|
VBox form = new VBox(6);
|
||||||
|
form.setPadding(new Insets(12));
|
||||||
|
form.setStyle("-fx-background-color: #252535;");
|
||||||
|
|
||||||
|
// ID field with no-space filter + auto-fill listener
|
||||||
|
idField = new TextField();
|
||||||
|
idField.setPromptText("eindeutige ID (keine Leerzeichen)");
|
||||||
|
idField.setTextFormatter(new TextFormatter<>(change -> {
|
||||||
|
change.setText(change.getText().replace(" ", ""));
|
||||||
|
return change;
|
||||||
|
}));
|
||||||
|
idField.focusedProperty().addListener((obs, wasFocused, isFocused) -> {
|
||||||
|
if (!isFocused) autoFillTextRefs();
|
||||||
|
});
|
||||||
|
|
||||||
|
xpSpinner = new Spinner<>(0, 99999, 0);
|
||||||
|
xpSpinner.setEditable(true);
|
||||||
|
xpSpinner.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
|
||||||
|
textField = new TextField();
|
||||||
|
textField.setPromptText("TextReference-Schlüssel");
|
||||||
|
descField = new TextField();
|
||||||
|
descField.setPromptText("TextReference-Schlüssel");
|
||||||
|
successField = new TextField();
|
||||||
|
successField.setPromptText("TextReference-Schlüssel");
|
||||||
|
|
||||||
|
form.getChildren().addAll(
|
||||||
|
sectionTitle("Quest"),
|
||||||
|
new Separator(),
|
||||||
|
row("Quest-ID:", idField),
|
||||||
|
row("XP:", xpSpinner),
|
||||||
|
new Separator(),
|
||||||
|
sectionTitle("Texte"),
|
||||||
|
row("Text:", textField),
|
||||||
|
row("Beschreibung:", descField),
|
||||||
|
row("Erfolgstext:", successField),
|
||||||
|
new Separator()
|
||||||
|
);
|
||||||
|
|
||||||
|
typeCombo = new ComboBox<>();
|
||||||
|
typeCombo.getItems().addAll("BringQuest", "FollowQuest", "InteractQuest", "ItemQuest", "TalkQuest");
|
||||||
|
typeCombo.setPromptText("Typ auswählen…");
|
||||||
|
typeCombo.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
typeCombo.setOnAction(e -> rebuildDynamicArea(typeCombo.getValue()));
|
||||||
|
|
||||||
|
dynamicArea = new VBox(6);
|
||||||
|
|
||||||
|
form.getChildren().addAll(
|
||||||
|
sectionTitle("Typ"),
|
||||||
|
typeCombo,
|
||||||
|
dynamicArea,
|
||||||
|
new Separator()
|
||||||
|
);
|
||||||
|
|
||||||
|
saveBtn = new Button("Quest speichern");
|
||||||
|
saveBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
saveBtn.setStyle("-fx-font-weight: bold; -fx-background-color: #3a5a8a; -fx-text-fill: white;");
|
||||||
|
saveBtn.setOnAction(e -> saveCurrentQuest());
|
||||||
|
form.getChildren().add(saveBtn);
|
||||||
|
|
||||||
|
return form;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void autoFillTextRefs() {
|
||||||
|
String id = idField.getText().trim();
|
||||||
|
if (id.isBlank()) return;
|
||||||
|
if (textField.getText().isBlank()) textField.setText(id + ".name");
|
||||||
|
if (descField.getText().isBlank()) descField.setText(id + ".description");
|
||||||
|
if (successField.getText().isBlank()) successField.setText(id + ".successmassage");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void rebuildDynamicArea(String type) {
|
||||||
|
dynamicArea.getChildren().clear();
|
||||||
|
f1 = null; f2 = null; f3 = null; countSpinner = null;
|
||||||
|
if (type == null) return;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "BringQuest" -> {
|
||||||
|
f1 = tf("NPC-ID (bringen)");
|
||||||
|
f2 = tf("Location-ID (Ziel)");
|
||||||
|
dynamicArea.getChildren().addAll(
|
||||||
|
sectionTitle("BringQuest"),
|
||||||
|
row("NPC:", f1),
|
||||||
|
row("Ziel-Location:", f2)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case "FollowQuest" -> {
|
||||||
|
f1 = tf("NPC-ID (folgen)");
|
||||||
|
f2 = tf("Location-ID (Ziel)");
|
||||||
|
dynamicArea.getChildren().addAll(
|
||||||
|
sectionTitle("FollowQuest"),
|
||||||
|
row("NPC:", f1),
|
||||||
|
row("Ziel-Location:", f2)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case "InteractQuest" -> {
|
||||||
|
f1 = tf("Interactable-ID");
|
||||||
|
dynamicArea.getChildren().addAll(
|
||||||
|
sectionTitle("InteractQuest"),
|
||||||
|
row("Interactable:", f1)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case "ItemQuest" -> {
|
||||||
|
f1 = tf("Item-ID");
|
||||||
|
countSpinner = new Spinner<>(1, 9999, 1);
|
||||||
|
countSpinner.setEditable(true);
|
||||||
|
countSpinner.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
dynamicArea.getChildren().addAll(
|
||||||
|
sectionTitle("ItemQuest"),
|
||||||
|
row("Item:", f1),
|
||||||
|
row("Anzahl:", countSpinner)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case "TalkQuest" -> {
|
||||||
|
f1 = tf("NPC-ID");
|
||||||
|
dynamicArea.getChildren().addAll(
|
||||||
|
sectionTitle("TalkQuest"),
|
||||||
|
row("NPC:", f1)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Form load / save ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void onQuestSelected(Quest old, Quest nw) {
|
||||||
|
if (old != null) saveFormToQuest(old);
|
||||||
|
current = nw;
|
||||||
|
deleteBtn.setDisable(nw == null);
|
||||||
|
if (nw != null) {
|
||||||
|
formContainer.setDisable(false);
|
||||||
|
loadFormFromQuest(nw);
|
||||||
|
} else {
|
||||||
|
formContainer.setDisable(true);
|
||||||
|
clearForm();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadFormFromQuest(Quest q) {
|
||||||
|
idField.setText(safe(q.getQuestId()));
|
||||||
|
xpSpinner.getValueFactory().setValue(q.getXp());
|
||||||
|
textField.setText(q.getText() != null ? q.getText().id() : "");
|
||||||
|
descField.setText(q.getDescription() != null ? q.getDescription().id() : "");
|
||||||
|
successField.setText(q.getSuccessText() != null ? q.getSuccessText().id() : "");
|
||||||
|
|
||||||
|
String type = QuestIO.typeOf(q);
|
||||||
|
typeCombo.setValue(switch (type) {
|
||||||
|
case "BRING" -> "BringQuest";
|
||||||
|
case "FOLLOW" -> "FollowQuest";
|
||||||
|
case "INTERACT" -> "InteractQuest";
|
||||||
|
case "ITEM" -> "ItemQuest";
|
||||||
|
case "TALK" -> "TalkQuest";
|
||||||
|
default -> null;
|
||||||
|
});
|
||||||
|
rebuildDynamicArea(typeCombo.getValue());
|
||||||
|
|
||||||
|
switch (q) {
|
||||||
|
case BringQuest bq -> {
|
||||||
|
if (f1 != null) f1.setText(bq.getBring() != null ? safe(bq.getBring().getCharacterId()) : "");
|
||||||
|
if (f2 != null) f2.setText(bq.getBringTo() != null ? safe(bq.getBringTo().getId()) : "");
|
||||||
|
}
|
||||||
|
case FollowQuest fq -> {
|
||||||
|
if (f1 != null) f1.setText(fq.getFollow() != null ? safe(fq.getFollow().getCharacterId()) : "");
|
||||||
|
if (f2 != null) f2.setText(fq.getFollowTo() != null ? safe(fq.getFollowTo().getId()) : "");
|
||||||
|
}
|
||||||
|
case InteractQuest iq -> {
|
||||||
|
if (f1 != null && iq.getInteractWith() instanceof InteractableRef ir)
|
||||||
|
f1.setText(safe(ir.getId()));
|
||||||
|
}
|
||||||
|
case ItemQuest iq -> {
|
||||||
|
if (f1 != null) f1.setText(iq.getItem() != null ? safe(iq.getItem().getItemId()) : "");
|
||||||
|
if (countSpinner != null) countSpinner.getValueFactory().setValue(iq.getCount());
|
||||||
|
}
|
||||||
|
case TalkQuest tq -> {
|
||||||
|
if (f1 != null) f1.setText(tq.getTalkTo() != null ? safe(tq.getTalkTo().getCharacterId()) : "");
|
||||||
|
}
|
||||||
|
default -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveFormToQuest(Quest q) {
|
||||||
|
q.setQuestId(idField.getText().trim());
|
||||||
|
q.setXp(xpSpinner.getValue());
|
||||||
|
q.setText(ref(textField));
|
||||||
|
q.setDescription(ref(descField));
|
||||||
|
q.setSuccessText(ref(successField));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Quest buildQuestFromForm() {
|
||||||
|
String type = typeCombo.getValue();
|
||||||
|
if (type == null) return null;
|
||||||
|
|
||||||
|
Quest q = switch (type) {
|
||||||
|
case "BringQuest" -> {
|
||||||
|
BringQuest bq = new BringQuest();
|
||||||
|
if (f1 != null && !f1.getText().isBlank()) {
|
||||||
|
NPC n = new NPC(); n.setCharacterId(f1.getText().trim()); bq.setBring(n);
|
||||||
|
}
|
||||||
|
if (f2 != null && !f2.getText().isBlank()) {
|
||||||
|
Location l = new Location();
|
||||||
|
l.setName(new TextReference(f2.getText().trim()));
|
||||||
|
bq.setBringTo(l);
|
||||||
|
}
|
||||||
|
yield bq;
|
||||||
|
}
|
||||||
|
case "FollowQuest" -> {
|
||||||
|
FollowQuest fq = new FollowQuest();
|
||||||
|
if (f1 != null && !f1.getText().isBlank()) {
|
||||||
|
NPC n = new NPC(); n.setCharacterId(f1.getText().trim()); fq.setFollow(n);
|
||||||
|
}
|
||||||
|
if (f2 != null && !f2.getText().isBlank()) {
|
||||||
|
Location l = new Location();
|
||||||
|
l.setName(new TextReference(f2.getText().trim()));
|
||||||
|
fq.setFollowTo(l);
|
||||||
|
}
|
||||||
|
yield fq;
|
||||||
|
}
|
||||||
|
case "InteractQuest" -> {
|
||||||
|
InteractQuest iq = new InteractQuest();
|
||||||
|
if (f1 != null && !f1.getText().isBlank()) {
|
||||||
|
InteractableRef ir = new InteractableRef(); ir.setId(f1.getText().trim());
|
||||||
|
iq.setInteractWith(ir);
|
||||||
|
}
|
||||||
|
yield iq;
|
||||||
|
}
|
||||||
|
case "ItemQuest" -> {
|
||||||
|
ItemQuest iq = new ItemQuest();
|
||||||
|
if (f1 != null && !f1.getText().isBlank()) {
|
||||||
|
Item item = new Item(); item.setItemId(f1.getText().trim()); iq.setItem(item);
|
||||||
|
}
|
||||||
|
iq.setCount(countSpinner != null ? countSpinner.getValue() : 1);
|
||||||
|
yield iq;
|
||||||
|
}
|
||||||
|
case "TalkQuest" -> {
|
||||||
|
TalkQuest tq = new TalkQuest();
|
||||||
|
if (f1 != null && !f1.getText().isBlank()) {
|
||||||
|
NPC n = new NPC(); n.setCharacterId(f1.getText().trim()); tq.setTalkTo(n);
|
||||||
|
}
|
||||||
|
yield tq;
|
||||||
|
}
|
||||||
|
default -> new TalkQuest();
|
||||||
|
};
|
||||||
|
|
||||||
|
q.setQuestId(idField.getText().trim());
|
||||||
|
q.setXp(xpSpinner.getValue());
|
||||||
|
q.setText(ref(textField));
|
||||||
|
q.setDescription(ref(descField));
|
||||||
|
q.setSuccessText(ref(successField));
|
||||||
|
return q;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearForm() {
|
||||||
|
idField.clear();
|
||||||
|
xpSpinner.getValueFactory().setValue(0);
|
||||||
|
textField.clear();
|
||||||
|
descField.clear();
|
||||||
|
successField.clear();
|
||||||
|
typeCombo.setValue(null);
|
||||||
|
dynamicArea.getChildren().clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── List operations ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void createQuest() {
|
||||||
|
TalkQuest q = new TalkQuest();
|
||||||
|
q.setQuestId("neue_quest_" + System.currentTimeMillis());
|
||||||
|
quests.add(q);
|
||||||
|
listView.getSelectionModel().select(q);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteSelected() {
|
||||||
|
Quest sel = listView.getSelectionModel().getSelectedItem();
|
||||||
|
if (sel == null) return;
|
||||||
|
String qId = sel.getQuestId();
|
||||||
|
quests.remove(sel);
|
||||||
|
if (qId != null && !qId.isBlank()) {
|
||||||
|
try { QuestIO.delete(qId, questDir); }
|
||||||
|
catch (IOException e) { /* ignore */ }
|
||||||
|
}
|
||||||
|
current = null;
|
||||||
|
clearForm();
|
||||||
|
formContainer.setDisable(true);
|
||||||
|
deleteBtn.setDisable(true);
|
||||||
|
reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveCurrentQuest() {
|
||||||
|
Quest built = buildQuestFromForm();
|
||||||
|
if (built == null) {
|
||||||
|
showError("Bitte einen Typ wählen.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (built.getQuestId() == null || built.getQuestId().isBlank()) {
|
||||||
|
showError("Quest-ID darf nicht leer sein.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int idx = quests.indexOf(current);
|
||||||
|
if (idx >= 0) quests.set(idx, built);
|
||||||
|
else quests.add(built);
|
||||||
|
current = built;
|
||||||
|
listView.getSelectionModel().select(built);
|
||||||
|
try {
|
||||||
|
QuestIO.save(built, questDir);
|
||||||
|
} catch (IOException e) {
|
||||||
|
showError("Fehler beim Speichern: " + e.getMessage());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void reload() {
|
public void reload() {
|
||||||
List<Quest> loaded = QuestIO.loadAll(questDir);
|
List<Quest> loaded = QuestIO.loadAll(questDir);
|
||||||
sharedQuests.setAll(loaded);
|
quests.setAll(loaded);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Single panel ──────────────────────────────────────────────────────────
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
static class QuestPanel extends VBox {
|
private static Label sectionTitle(String text) {
|
||||||
|
Label l = new Label(text);
|
||||||
|
l.setStyle("-fx-font-weight: bold; -fx-font-size: 12; -fx-text-fill: #88aacc;");
|
||||||
|
return l;
|
||||||
|
}
|
||||||
|
|
||||||
private final ObservableList<Quest> quests;
|
private static HBox row(String labelText, Node control) {
|
||||||
private final Path questDir;
|
Label lbl = new Label(labelText);
|
||||||
private final Runnable onSaved;
|
lbl.setMinWidth(130);
|
||||||
|
lbl.setStyle("-fx-text-fill: #aaa;");
|
||||||
|
HBox.setHgrow(control, Priority.ALWAYS);
|
||||||
|
HBox box = new HBox(8, lbl, control);
|
||||||
|
box.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
return box;
|
||||||
|
}
|
||||||
|
|
||||||
private final ListView<Quest> listView;
|
private static TextField tf(String prompt) {
|
||||||
private Quest current = null;
|
TextField f = new TextField();
|
||||||
|
f.setPromptText(prompt);
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
// Common fields
|
private static String safe(String s) { return s != null ? s : ""; }
|
||||||
private TextField idField;
|
|
||||||
private Spinner<Integer> xpSpinner;
|
|
||||||
private TextField textField;
|
|
||||||
private TextField descField;
|
|
||||||
private TextField successField;
|
|
||||||
|
|
||||||
// Type selection
|
private static TextReference ref(TextField f) {
|
||||||
private ComboBox<String> typeCombo;
|
String s = f.getText().trim();
|
||||||
|
return s.isBlank() ? null : new TextReference(s);
|
||||||
|
}
|
||||||
|
|
||||||
// Dynamic area
|
private void showError(String msg) {
|
||||||
private VBox dynamicArea;
|
Alert a = new Alert(Alert.AlertType.ERROR, msg, ButtonType.OK);
|
||||||
|
a.showAndWait();
|
||||||
// Type-specific fields (lazily filled)
|
|
||||||
private TextField f1, f2, f3;
|
|
||||||
private Spinner<Integer> countSpinner;
|
|
||||||
|
|
||||||
// Form container (disabled when nothing loaded)
|
|
||||||
private VBox formContainer;
|
|
||||||
private Button saveBtn;
|
|
||||||
private Button deleteBtn;
|
|
||||||
|
|
||||||
QuestPanel(String title, ObservableList<Quest> quests, Path questDir, Runnable onSaved) {
|
|
||||||
this.quests = quests;
|
|
||||||
this.questDir = questDir;
|
|
||||||
this.onSaved = onSaved;
|
|
||||||
|
|
||||||
setStyle("-fx-background-color: #252535; -fx-border-color: #3a3a4a; -fx-border-width: 0 1 0 0;");
|
|
||||||
setSpacing(0);
|
|
||||||
|
|
||||||
// ── Header ────────────────────────────────────────────────────────
|
|
||||||
Label titleLbl = new Label(title);
|
|
||||||
titleLbl.setStyle("-fx-font-weight: bold; -fx-font-size: 13; -fx-text-fill: #aaccee;");
|
|
||||||
Button refreshBtn = new Button("↺");
|
|
||||||
refreshBtn.setStyle("-fx-background-color: transparent; -fx-text-fill: #888; -fx-cursor: hand;");
|
|
||||||
refreshBtn.setOnAction(e -> onSaved.run());
|
|
||||||
HBox header = new HBox(8, titleLbl, refreshBtn);
|
|
||||||
header.setPadding(new Insets(8, 10, 8, 10));
|
|
||||||
header.setAlignment(Pos.CENTER_LEFT);
|
|
||||||
header.setStyle("-fx-background-color: #1a1a2a; -fx-border-color: #444;"
|
|
||||||
+ " -fx-border-width: 0 0 1 0;");
|
|
||||||
|
|
||||||
// ── Quest list ────────────────────────────────────────────────────
|
|
||||||
listView = new ListView<>(quests);
|
|
||||||
listView.setPrefHeight(160);
|
|
||||||
listView.setStyle("-fx-background-color: #1a1a2a; -fx-control-inner-background: #1a1a2a;");
|
|
||||||
listView.setCellFactory(lv -> new ListCell<>() {
|
|
||||||
@Override protected void updateItem(Quest q, boolean empty) {
|
|
||||||
super.updateItem(q, empty);
|
|
||||||
if (empty || q == null) { setText(null); setStyle(""); return; }
|
|
||||||
String id = q.getQuestId() != null ? q.getQuestId() : "—";
|
|
||||||
String type = QuestIO.typeOf(q);
|
|
||||||
setText("[" + type + "] " + id);
|
|
||||||
setStyle("-fx-text-fill: #cccccc;");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
listView.getSelectionModel().selectedItemProperty()
|
|
||||||
.addListener((obs, old, nw) -> onQuestSelected(old, nw));
|
|
||||||
|
|
||||||
Button newBtn = new Button("Neue Quest");
|
|
||||||
newBtn.setMaxWidth(Double.MAX_VALUE);
|
|
||||||
newBtn.setStyle("-fx-background-color: #3a6a3a; -fx-text-fill: white;");
|
|
||||||
newBtn.setOnAction(e -> createQuest());
|
|
||||||
|
|
||||||
deleteBtn = new Button("Löschen");
|
|
||||||
deleteBtn.setMaxWidth(Double.MAX_VALUE);
|
|
||||||
deleteBtn.setStyle("-fx-background-color: #6a2a2a; -fx-text-fill: white;");
|
|
||||||
deleteBtn.setDisable(true);
|
|
||||||
deleteBtn.setOnAction(e -> deleteSelected());
|
|
||||||
|
|
||||||
HBox listButtons = new HBox(6, newBtn, deleteBtn);
|
|
||||||
listButtons.setPadding(new Insets(6, 8, 6, 8));
|
|
||||||
HBox.setHgrow(newBtn, Priority.ALWAYS);
|
|
||||||
HBox.setHgrow(deleteBtn, Priority.ALWAYS);
|
|
||||||
|
|
||||||
VBox listSection = new VBox(listView, listButtons);
|
|
||||||
listSection.setStyle("-fx-background-color: #1a1a2a;");
|
|
||||||
|
|
||||||
// ── Form ──────────────────────────────────────────────────────────
|
|
||||||
formContainer = buildForm();
|
|
||||||
formContainer.setDisable(true);
|
|
||||||
|
|
||||||
ScrollPane formScroll = new ScrollPane(formContainer);
|
|
||||||
formScroll.setFitToWidth(true);
|
|
||||||
formScroll.setStyle("-fx-background-color: #252535; -fx-background: #252535;");
|
|
||||||
VBox.setVgrow(formScroll, Priority.ALWAYS);
|
|
||||||
|
|
||||||
getChildren().addAll(header, listSection, new Separator(), formScroll);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Form construction ─────────────────────────────────────────────────
|
|
||||||
|
|
||||||
private VBox buildForm() {
|
|
||||||
VBox form = new VBox(6);
|
|
||||||
form.setPadding(new Insets(10));
|
|
||||||
form.setStyle("-fx-background-color: #252535;");
|
|
||||||
|
|
||||||
// Common fields
|
|
||||||
idField = new TextField();
|
|
||||||
idField.setPromptText("eindeutige ID");
|
|
||||||
xpSpinner = new Spinner<>(0, 99999, 0);
|
|
||||||
xpSpinner.setEditable(true);
|
|
||||||
xpSpinner.setMaxWidth(Double.MAX_VALUE);
|
|
||||||
textField = new TextField();
|
|
||||||
textField.setPromptText("TextReference-Schlüssel");
|
|
||||||
descField = new TextField();
|
|
||||||
descField.setPromptText("TextReference-Schlüssel");
|
|
||||||
successField = new TextField();
|
|
||||||
successField.setPromptText("TextReference-Schlüssel");
|
|
||||||
|
|
||||||
form.getChildren().addAll(
|
|
||||||
sectionTitle("Quest"),
|
|
||||||
new Separator(),
|
|
||||||
row("Quest-ID:", idField),
|
|
||||||
row("XP:", xpSpinner),
|
|
||||||
new Separator(),
|
|
||||||
sectionTitle("Texte"),
|
|
||||||
row("Text:", textField),
|
|
||||||
row("Beschreibung:", descField),
|
|
||||||
row("Erfolgstext:", successField),
|
|
||||||
new Separator()
|
|
||||||
);
|
|
||||||
|
|
||||||
// Type selection
|
|
||||||
typeCombo = new ComboBox<>();
|
|
||||||
typeCombo.getItems().addAll("BringQuest", "FollowQuest", "InteractQuest", "ItemQuest", "TalkQuest");
|
|
||||||
typeCombo.setPromptText("Typ auswählen…");
|
|
||||||
typeCombo.setMaxWidth(Double.MAX_VALUE);
|
|
||||||
typeCombo.setOnAction(e -> rebuildDynamicArea(typeCombo.getValue()));
|
|
||||||
|
|
||||||
dynamicArea = new VBox(6);
|
|
||||||
|
|
||||||
form.getChildren().addAll(
|
|
||||||
sectionTitle("Typ"),
|
|
||||||
typeCombo,
|
|
||||||
dynamicArea,
|
|
||||||
new Separator()
|
|
||||||
);
|
|
||||||
|
|
||||||
// Save button
|
|
||||||
saveBtn = new Button("Quest speichern");
|
|
||||||
saveBtn.setMaxWidth(Double.MAX_VALUE);
|
|
||||||
saveBtn.setStyle("-fx-font-weight: bold; -fx-background-color: #3a5a8a; -fx-text-fill: white;");
|
|
||||||
saveBtn.setOnAction(e -> saveCurrentQuest());
|
|
||||||
form.getChildren().add(saveBtn);
|
|
||||||
|
|
||||||
return form;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void rebuildDynamicArea(String type) {
|
|
||||||
dynamicArea.getChildren().clear();
|
|
||||||
f1 = null; f2 = null; f3 = null; countSpinner = null;
|
|
||||||
if (type == null) return;
|
|
||||||
|
|
||||||
switch (type) {
|
|
||||||
case "BringQuest" -> {
|
|
||||||
f1 = tf("NPC-ID (bringen)");
|
|
||||||
f2 = tf("Location-ID (Ziel)");
|
|
||||||
dynamicArea.getChildren().addAll(
|
|
||||||
sectionTitle("BringQuest"),
|
|
||||||
row("NPC:", f1),
|
|
||||||
row("Ziel-Location:", f2)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
case "FollowQuest" -> {
|
|
||||||
f1 = tf("NPC-ID (folgen)");
|
|
||||||
f2 = tf("Location-ID (Ziel)");
|
|
||||||
dynamicArea.getChildren().addAll(
|
|
||||||
sectionTitle("FollowQuest"),
|
|
||||||
row("NPC:", f1),
|
|
||||||
row("Ziel-Location:", f2)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
case "InteractQuest" -> {
|
|
||||||
f1 = tf("Interactable-ID");
|
|
||||||
dynamicArea.getChildren().addAll(
|
|
||||||
sectionTitle("InteractQuest"),
|
|
||||||
row("Interactable:", f1)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
case "ItemQuest" -> {
|
|
||||||
f1 = tf("Item-ID");
|
|
||||||
countSpinner = new Spinner<>(1, 9999, 1);
|
|
||||||
countSpinner.setEditable(true);
|
|
||||||
countSpinner.setMaxWidth(Double.MAX_VALUE);
|
|
||||||
dynamicArea.getChildren().addAll(
|
|
||||||
sectionTitle("ItemQuest"),
|
|
||||||
row("Item:", f1),
|
|
||||||
row("Anzahl:", countSpinner)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
case "TalkQuest" -> {
|
|
||||||
f1 = tf("NPC-ID");
|
|
||||||
dynamicArea.getChildren().addAll(
|
|
||||||
sectionTitle("TalkQuest"),
|
|
||||||
row("NPC:", f1)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Form load / save ──────────────────────────────────────────────────
|
|
||||||
|
|
||||||
private void onQuestSelected(Quest old, Quest nw) {
|
|
||||||
if (old != null) saveFormToQuest(old);
|
|
||||||
current = nw;
|
|
||||||
deleteBtn.setDisable(nw == null);
|
|
||||||
if (nw != null) {
|
|
||||||
formContainer.setDisable(false);
|
|
||||||
loadFormFromQuest(nw);
|
|
||||||
} else {
|
|
||||||
formContainer.setDisable(true);
|
|
||||||
clearForm();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void loadFormFromQuest(Quest q) {
|
|
||||||
idField.setText(safe(q.getQuestId()));
|
|
||||||
xpSpinner.getValueFactory().setValue(q.getXp());
|
|
||||||
textField.setText(q.getText() != null ? q.getText().id() : "");
|
|
||||||
descField.setText(q.getDescription() != null ? q.getDescription().id() : "");
|
|
||||||
successField.setText(q.getSuccessText() != null ? q.getSuccessText().id() : "");
|
|
||||||
|
|
||||||
String type = QuestIO.typeOf(q);
|
|
||||||
typeCombo.setValue(switch (type) {
|
|
||||||
case "BRING" -> "BringQuest";
|
|
||||||
case "FOLLOW" -> "FollowQuest";
|
|
||||||
case "INTERACT" -> "InteractQuest";
|
|
||||||
case "ITEM" -> "ItemQuest";
|
|
||||||
case "TALK" -> "TalkQuest";
|
|
||||||
default -> null;
|
|
||||||
});
|
|
||||||
rebuildDynamicArea(typeCombo.getValue());
|
|
||||||
|
|
||||||
// Fill type-specific fields
|
|
||||||
switch (q) {
|
|
||||||
case BringQuest bq -> {
|
|
||||||
if (f1 != null) f1.setText(bq.getBring() != null ? safe(bq.getBring().getCharacterId()) : "");
|
|
||||||
if (f2 != null) f2.setText(bq.getBringTo() != null ? safe(bq.getBringTo().getId()) : "");
|
|
||||||
}
|
|
||||||
case FollowQuest fq -> {
|
|
||||||
if (f1 != null) f1.setText(fq.getFollow() != null ? safe(fq.getFollow().getCharacterId()) : "");
|
|
||||||
if (f2 != null) f2.setText(fq.getFollowTo() != null ? safe(fq.getFollowTo().getId()) : "");
|
|
||||||
}
|
|
||||||
case InteractQuest iq -> {
|
|
||||||
if (f1 != null && iq.getInteractWith() instanceof InteractableRef ir)
|
|
||||||
f1.setText(safe(ir.getId()));
|
|
||||||
}
|
|
||||||
case ItemQuest iq -> {
|
|
||||||
if (f1 != null) f1.setText(iq.getItem() != null ? safe(iq.getItem().getItemId()) : "");
|
|
||||||
if (countSpinner != null) countSpinner.getValueFactory().setValue(iq.getCount());
|
|
||||||
}
|
|
||||||
case TalkQuest tq -> {
|
|
||||||
if (f1 != null) f1.setText(tq.getTalkTo() != null ? safe(tq.getTalkTo().getCharacterId()) : "");
|
|
||||||
}
|
|
||||||
default -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void saveFormToQuest(Quest q) {
|
|
||||||
q.setQuestId(idField.getText().trim());
|
|
||||||
q.setXp(xpSpinner.getValue());
|
|
||||||
q.setText(ref(textField));
|
|
||||||
q.setDescription(ref(descField));
|
|
||||||
q.setSuccessText(ref(successField));
|
|
||||||
|
|
||||||
// Type-specific fields written when actually saving (buildQuestFromForm)
|
|
||||||
}
|
|
||||||
|
|
||||||
private Quest buildQuestFromForm() {
|
|
||||||
String type = typeCombo.getValue();
|
|
||||||
if (type == null) return null;
|
|
||||||
|
|
||||||
Quest q = switch (type) {
|
|
||||||
case "BringQuest" -> {
|
|
||||||
BringQuest bq = new BringQuest();
|
|
||||||
if (f1 != null && !f1.getText().isBlank()) {
|
|
||||||
NPC n = new NPC(); n.setCharacterId(f1.getText().trim()); bq.setBring(n);
|
|
||||||
}
|
|
||||||
if (f2 != null && !f2.getText().isBlank()) {
|
|
||||||
Location l = new Location(); l.setName(new de.blight.common.model.TextReference(f2.getText().trim())); bq.setBringTo(l);
|
|
||||||
}
|
|
||||||
yield bq;
|
|
||||||
}
|
|
||||||
case "FollowQuest" -> {
|
|
||||||
FollowQuest fq = new FollowQuest();
|
|
||||||
if (f1 != null && !f1.getText().isBlank()) {
|
|
||||||
NPC n = new NPC(); n.setCharacterId(f1.getText().trim()); fq.setFollow(n);
|
|
||||||
}
|
|
||||||
if (f2 != null && !f2.getText().isBlank()) {
|
|
||||||
Location l = new Location(); l.setName(new de.blight.common.model.TextReference(f2.getText().trim())); fq.setFollowTo(l);
|
|
||||||
}
|
|
||||||
yield fq;
|
|
||||||
}
|
|
||||||
case "InteractQuest" -> {
|
|
||||||
InteractQuest iq = new InteractQuest();
|
|
||||||
if (f1 != null && !f1.getText().isBlank()) {
|
|
||||||
InteractableRef ir = new InteractableRef(); ir.setId(f1.getText().trim());
|
|
||||||
iq.setInteractWith(ir);
|
|
||||||
}
|
|
||||||
yield iq;
|
|
||||||
}
|
|
||||||
case "ItemQuest" -> {
|
|
||||||
ItemQuest iq = new ItemQuest();
|
|
||||||
if (f1 != null && !f1.getText().isBlank()) {
|
|
||||||
Item item = new Item(); item.setItemId(f1.getText().trim()); iq.setItem(item);
|
|
||||||
}
|
|
||||||
iq.setCount(countSpinner != null ? countSpinner.getValue() : 1);
|
|
||||||
yield iq;
|
|
||||||
}
|
|
||||||
case "TalkQuest" -> {
|
|
||||||
TalkQuest tq = new TalkQuest();
|
|
||||||
if (f1 != null && !f1.getText().isBlank()) {
|
|
||||||
NPC n = new NPC(); n.setCharacterId(f1.getText().trim()); tq.setTalkTo(n);
|
|
||||||
}
|
|
||||||
yield tq;
|
|
||||||
}
|
|
||||||
default -> new TalkQuest();
|
|
||||||
};
|
|
||||||
|
|
||||||
q.setQuestId(idField.getText().trim());
|
|
||||||
q.setXp(xpSpinner.getValue());
|
|
||||||
q.setText(ref(textField));
|
|
||||||
q.setDescription(ref(descField));
|
|
||||||
q.setSuccessText(ref(successField));
|
|
||||||
return q;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void clearForm() {
|
|
||||||
idField.clear();
|
|
||||||
xpSpinner.getValueFactory().setValue(0);
|
|
||||||
textField.clear();
|
|
||||||
descField.clear();
|
|
||||||
successField.clear();
|
|
||||||
typeCombo.setValue(null);
|
|
||||||
dynamicArea.getChildren().clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── List operations ───────────────────────────────────────────────────
|
|
||||||
|
|
||||||
private void createQuest() {
|
|
||||||
TalkQuest q = new TalkQuest();
|
|
||||||
q.setQuestId("neue_quest_" + System.currentTimeMillis());
|
|
||||||
quests.add(q);
|
|
||||||
listView.getSelectionModel().select(q);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void deleteSelected() {
|
|
||||||
Quest sel = listView.getSelectionModel().getSelectedItem();
|
|
||||||
if (sel == null) return;
|
|
||||||
String qId = sel.getQuestId();
|
|
||||||
quests.remove(sel);
|
|
||||||
if (qId != null && !qId.isBlank()) {
|
|
||||||
try { QuestIO.delete(qId, questDir); }
|
|
||||||
catch (IOException e) { /* ignore */ }
|
|
||||||
}
|
|
||||||
current = null;
|
|
||||||
clearForm();
|
|
||||||
formContainer.setDisable(true);
|
|
||||||
deleteBtn.setDisable(true);
|
|
||||||
onSaved.run();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void saveCurrentQuest() {
|
|
||||||
Quest built = buildQuestFromForm();
|
|
||||||
if (built == null) {
|
|
||||||
showError("Bitte einen Typ wählen.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (built.getQuestId() == null || built.getQuestId().isBlank()) {
|
|
||||||
showError("Quest-ID darf nicht leer sein.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Replace or add in shared list
|
|
||||||
int idx = quests.indexOf(current);
|
|
||||||
if (idx >= 0) quests.set(idx, built);
|
|
||||||
else quests.add(built);
|
|
||||||
current = built;
|
|
||||||
listView.getSelectionModel().select(built);
|
|
||||||
try {
|
|
||||||
QuestIO.save(built, questDir);
|
|
||||||
} catch (IOException e) {
|
|
||||||
showError("Fehler beim Speichern: " + e.getMessage());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
onSaved.run();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Helpers ───────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
private static Label sectionTitle(String text) {
|
|
||||||
Label l = new Label(text);
|
|
||||||
l.setStyle("-fx-font-weight: bold; -fx-font-size: 12; -fx-text-fill: #88aacc;");
|
|
||||||
return l;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static HBox row(String labelText, Node control) {
|
|
||||||
Label lbl = new Label(labelText);
|
|
||||||
lbl.setMinWidth(130);
|
|
||||||
lbl.setStyle("-fx-text-fill: #aaa;");
|
|
||||||
HBox.setHgrow(control, Priority.ALWAYS);
|
|
||||||
HBox box = new HBox(8, lbl, control);
|
|
||||||
box.setAlignment(Pos.CENTER_LEFT);
|
|
||||||
return box;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static TextField tf(String prompt) {
|
|
||||||
TextField f = new TextField();
|
|
||||||
f.setPromptText(prompt);
|
|
||||||
return f;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String safe(String s) { return s != null ? s : ""; }
|
|
||||||
|
|
||||||
private static TextReference ref(TextField f) {
|
|
||||||
String s = f.getText().trim();
|
|
||||||
return s.isBlank() ? null : new TextReference(s);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showError(String msg) {
|
|
||||||
Alert a = new Alert(Alert.AlertType.ERROR, msg, ButtonType.OK);
|
|
||||||
a.showAndWait();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import javafx.collections.FXCollections;
|
|||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
import javafx.collections.transformation.SortedList;
|
import javafx.collections.transformation.SortedList;
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.geometry.Orientation;
|
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
@@ -17,492 +16,450 @@ import java.nio.file.Path;
|
|||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rezept-Verwaltung: zwei identische RecipePanel-Instanzen nebeneinander.
|
* Rezept-Verwaltung: Liste links, Formular rechts.
|
||||||
* Sortiert nach CraftingTableType, dann nach erstelltem Item-ID.
|
* Sortiert nach CraftingTableType, dann nach erstelltem Item-ID.
|
||||||
*/
|
*/
|
||||||
public class RecipeEditorView extends BorderPane {
|
public class RecipeEditorView extends BorderPane {
|
||||||
|
|
||||||
private final ObservableList<Recipe> sharedRecipes = FXCollections.observableArrayList();
|
private static final String NO_TABLE = "— kein Tisch —";
|
||||||
|
|
||||||
|
private final ObservableList<Recipe> recipes = FXCollections.observableArrayList();
|
||||||
private final Path recipeDir;
|
private final Path recipeDir;
|
||||||
|
|
||||||
|
// ── List ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private final SortedList<Recipe> sortedRecipes;
|
||||||
|
private ListView<Recipe> listView;
|
||||||
|
private Button deleteBtn;
|
||||||
|
private Recipe current = null;
|
||||||
|
private String oldFileId = null;
|
||||||
|
|
||||||
|
// ── Form fields ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private TextField createsField;
|
||||||
|
private ListView<String> componentsList;
|
||||||
|
private ComboBox<String> tableCombo;
|
||||||
|
private HBox alchemyRow;
|
||||||
|
private HBox enchantingRow;
|
||||||
|
private HBox smitheryRow;
|
||||||
|
private HBox engineeringRow;
|
||||||
|
private Spinner<Integer> alchemySpinner;
|
||||||
|
private Spinner<Integer> enchantingSpinner;
|
||||||
|
private Spinner<Integer> smitherySpinner;
|
||||||
|
private Spinner<Integer> engineeringSpinner;
|
||||||
|
private VBox formContainer;
|
||||||
|
|
||||||
public RecipeEditorView(Path recipeDir) {
|
public RecipeEditorView(Path recipeDir) {
|
||||||
this.recipeDir = recipeDir;
|
this.recipeDir = recipeDir;
|
||||||
|
this.sortedRecipes = new SortedList<>(recipes, RecipeIO.SORT_ORDER);
|
||||||
setStyle("-fx-background-color: #1e1e2e;");
|
setStyle("-fx-background-color: #1e1e2e;");
|
||||||
reload();
|
reload();
|
||||||
|
|
||||||
RecipePanel left = new RecipePanel("Liste 1", sharedRecipes, recipeDir, this::reload);
|
SplitPane split = new SplitPane(buildListPanel(), buildFormPanel());
|
||||||
RecipePanel right = new RecipePanel("Liste 2", sharedRecipes, recipeDir, this::reload);
|
split.setDividerPositions(0.28);
|
||||||
|
setCenter(split);
|
||||||
|
}
|
||||||
|
|
||||||
HBox panels = new HBox(1, left, right);
|
// ── List panel ────────────────────────────────────────────────────────────
|
||||||
HBox.setHgrow(left, Priority.ALWAYS);
|
|
||||||
HBox.setHgrow(right, Priority.ALWAYS);
|
private VBox buildListPanel() {
|
||||||
setCenter(panels);
|
listView = new ListView<>(sortedRecipes);
|
||||||
|
listView.setStyle("-fx-background-color: #1a1a2a; -fx-control-inner-background: #1a1a2a;");
|
||||||
|
listView.setCellFactory(lv -> new ListCell<>() {
|
||||||
|
@Override protected void updateItem(Recipe r, boolean empty) {
|
||||||
|
super.updateItem(r, empty);
|
||||||
|
if (empty || r == null) { setText(null); setStyle(""); return; }
|
||||||
|
String creates = r.getCreates() != null ? safe(r.getCreates().getItemId()) : "—";
|
||||||
|
String table = r.getTable() != null && r.getTable().getType() != null
|
||||||
|
? r.getTable().getType().name() : "Handwerk";
|
||||||
|
setText(creates);
|
||||||
|
setTooltip(new Tooltip("[" + table + "] erstellt: " + creates));
|
||||||
|
String color = tableColor(r.getTable());
|
||||||
|
setStyle("-fx-text-fill: #dddddd;"
|
||||||
|
+ " -fx-border-color: transparent transparent transparent " + color + ";"
|
||||||
|
+ " -fx-border-width: 0 0 0 3; -fx-padding: 3 6 3 8;");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
listView.getSelectionModel().selectedItemProperty()
|
||||||
|
.addListener((obs, old, nw) -> onRecipeSelected(old, nw));
|
||||||
|
VBox.setVgrow(listView, Priority.ALWAYS);
|
||||||
|
|
||||||
|
Button newBtn = new Button("Neues Rezept");
|
||||||
|
newBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
newBtn.setStyle("-fx-background-color: #3a6a3a; -fx-text-fill: white;");
|
||||||
|
newBtn.setOnAction(e -> createRecipe());
|
||||||
|
|
||||||
|
deleteBtn = new Button("Löschen");
|
||||||
|
deleteBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
deleteBtn.setStyle("-fx-background-color: #6a2a2a; -fx-text-fill: white;");
|
||||||
|
deleteBtn.setDisable(true);
|
||||||
|
deleteBtn.setOnAction(e -> deleteSelected());
|
||||||
|
|
||||||
|
Button refreshBtn = new Button("↺ Neu laden");
|
||||||
|
refreshBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
refreshBtn.setOnAction(e -> reload());
|
||||||
|
|
||||||
|
HBox listButtons = new HBox(6, newBtn, deleteBtn);
|
||||||
|
HBox.setHgrow(newBtn, Priority.ALWAYS);
|
||||||
|
HBox.setHgrow(deleteBtn, Priority.ALWAYS);
|
||||||
|
listButtons.setPadding(new Insets(6, 8, 6, 8));
|
||||||
|
|
||||||
|
VBox panel = new VBox(6, listView, listButtons, refreshBtn);
|
||||||
|
VBox.setVgrow(listView, Priority.ALWAYS);
|
||||||
|
panel.setPadding(new Insets(8));
|
||||||
|
panel.setStyle("-fx-background-color: #1a1a2a;");
|
||||||
|
return panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Form panel ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private ScrollPane buildFormPanel() {
|
||||||
|
formContainer = buildForm();
|
||||||
|
formContainer.setDisable(true);
|
||||||
|
ScrollPane scroll = new ScrollPane(formContainer);
|
||||||
|
scroll.setFitToWidth(true);
|
||||||
|
scroll.setStyle("-fx-background-color: #252535; -fx-background: #252535;");
|
||||||
|
return scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
private VBox buildForm() {
|
||||||
|
VBox form = new VBox(6);
|
||||||
|
form.setPadding(new Insets(10));
|
||||||
|
form.setStyle("-fx-background-color: #252535;");
|
||||||
|
|
||||||
|
createsField = new TextField();
|
||||||
|
createsField.setPromptText("Item-ID des erstellten Items");
|
||||||
|
|
||||||
|
componentsList = new ListView<>();
|
||||||
|
componentsList.setPrefHeight(110);
|
||||||
|
componentsList.setStyle("-fx-background-color: #1e1e2e; -fx-control-inner-background: #1e1e2e;");
|
||||||
|
componentsList.setCellFactory(lv -> new ListCell<>() {
|
||||||
|
@Override protected void updateItem(String s, boolean empty) {
|
||||||
|
super.updateItem(s, empty);
|
||||||
|
setText(empty || s == null ? null : s);
|
||||||
|
setStyle(empty ? "" : "-fx-text-fill: #cccccc;");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Button addCompBtn = smallBtn("+");
|
||||||
|
Button delCompBtn = smallBtn("−");
|
||||||
|
addCompBtn.setOnAction(e -> addComponent());
|
||||||
|
delCompBtn.setOnAction(e -> {
|
||||||
|
String sel = componentsList.getSelectionModel().getSelectedItem();
|
||||||
|
if (sel != null) componentsList.getItems().remove(sel);
|
||||||
|
});
|
||||||
|
HBox compButtons = new HBox(4, addCompBtn, delCompBtn);
|
||||||
|
|
||||||
|
tableCombo = new ComboBox<>();
|
||||||
|
tableCombo.getItems().add(NO_TABLE);
|
||||||
|
for (CraftingTable.CraftingTableType t : CraftingTable.CraftingTableType.values()) {
|
||||||
|
tableCombo.getItems().add(t.name());
|
||||||
|
}
|
||||||
|
tableCombo.setValue(NO_TABLE);
|
||||||
|
tableCombo.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
tableCombo.setOnAction(e -> updateRequirementRows(tableCombo.getValue()));
|
||||||
|
|
||||||
|
alchemySpinner = lvlSpinner();
|
||||||
|
enchantingSpinner = lvlSpinner();
|
||||||
|
smitherySpinner = lvlSpinner();
|
||||||
|
engineeringSpinner = lvlSpinner();
|
||||||
|
|
||||||
|
alchemyRow = requirementRow("Lvl Alchemie:", alchemySpinner);
|
||||||
|
enchantingRow = requirementRow("Lvl Verzauberung:", enchantingSpinner);
|
||||||
|
smitheryRow = requirementRow("Lvl Schmieden:", smitherySpinner);
|
||||||
|
engineeringRow = requirementRow("Lvl Engineering:", engineeringSpinner);
|
||||||
|
|
||||||
|
Button saveBtn = new Button("Rezept speichern");
|
||||||
|
saveBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
saveBtn.setStyle("-fx-font-weight: bold; -fx-background-color: #3a5a8a; -fx-text-fill: white;");
|
||||||
|
saveBtn.setOnAction(e -> saveCurrentRecipe());
|
||||||
|
|
||||||
|
form.getChildren().addAll(
|
||||||
|
sectionTitle("Ergebnis"),
|
||||||
|
new Separator(),
|
||||||
|
row("Erstellt:", createsField),
|
||||||
|
sectionTitle("Zutaten"),
|
||||||
|
componentsList,
|
||||||
|
compButtons,
|
||||||
|
new Separator(),
|
||||||
|
sectionTitle("Crafting Table"),
|
||||||
|
tableCombo,
|
||||||
|
alchemyRow,
|
||||||
|
enchantingRow,
|
||||||
|
smitheryRow,
|
||||||
|
engineeringRow,
|
||||||
|
new Separator(),
|
||||||
|
saveBtn
|
||||||
|
);
|
||||||
|
|
||||||
|
updateRequirementRows(NO_TABLE);
|
||||||
|
return form;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateRequirementRows(String tableValue) {
|
||||||
|
boolean noTable = tableValue == null || tableValue.equals(NO_TABLE);
|
||||||
|
setRowVisible(alchemyRow, false);
|
||||||
|
setRowVisible(enchantingRow, false);
|
||||||
|
setRowVisible(smitheryRow, false);
|
||||||
|
setRowVisible(engineeringRow, false);
|
||||||
|
if (noTable) return;
|
||||||
|
switch (tableValue) {
|
||||||
|
case "AlchemyTable" -> setRowVisible(alchemyRow, true);
|
||||||
|
case "EnchantmentTable" -> setRowVisible(enchantingRow, true);
|
||||||
|
case "Smithy",
|
||||||
|
"Goldsmiths" -> setRowVisible(smitheryRow, true);
|
||||||
|
case "Workshop" -> setRowVisible(engineeringRow, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setRowVisible(HBox row, boolean visible) {
|
||||||
|
row.setVisible(visible);
|
||||||
|
row.setManaged(visible);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Form load / save ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void onRecipeSelected(Recipe old, Recipe nw) {
|
||||||
|
if (old != null) saveFormToRecipe(old);
|
||||||
|
current = nw;
|
||||||
|
oldFileId = nw != null ? RecipeIO.fileId(nw) : null;
|
||||||
|
deleteBtn.setDisable(nw == null);
|
||||||
|
if (nw != null) {
|
||||||
|
formContainer.setDisable(false);
|
||||||
|
loadFormFromRecipe(nw);
|
||||||
|
} else {
|
||||||
|
formContainer.setDisable(true);
|
||||||
|
clearForm();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadFormFromRecipe(Recipe r) {
|
||||||
|
createsField.setText(r.getCreates() != null ? safe(r.getCreates().getItemId()) : "");
|
||||||
|
|
||||||
|
componentsList.getItems().clear();
|
||||||
|
if (r.getComponents() != null) {
|
||||||
|
r.getComponents().forEach((item, count) ->
|
||||||
|
componentsList.getItems().add(item.getItemId() + " × " + count));
|
||||||
|
}
|
||||||
|
|
||||||
|
String tableVal = NO_TABLE;
|
||||||
|
if (r.getTable() != null && r.getTable().getType() != null) {
|
||||||
|
tableVal = r.getTable().getType().name();
|
||||||
|
}
|
||||||
|
tableCombo.setValue(tableVal);
|
||||||
|
updateRequirementRows(tableVal);
|
||||||
|
|
||||||
|
alchemySpinner.getValueFactory().setValue(
|
||||||
|
r.getRequiresLvlAlchemy() != null ? r.getRequiresLvlAlchemy() : 1);
|
||||||
|
enchantingSpinner.getValueFactory().setValue(
|
||||||
|
r.getRequiresLvlEnchanting() != null ? r.getRequiresLvlEnchanting() : 1);
|
||||||
|
smitherySpinner.getValueFactory().setValue(
|
||||||
|
r.getRequiresLvlSmithery() != null ? r.getRequiresLvlSmithery() : 1);
|
||||||
|
engineeringSpinner.getValueFactory().setValue(
|
||||||
|
r.getRequiresLvlEngineering() != null ? r.getRequiresLvlEngineering() : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveFormToRecipe(Recipe r) {
|
||||||
|
String cId = createsField.getText().trim();
|
||||||
|
if (!cId.isBlank()) {
|
||||||
|
Item creates = r.getCreates() != null ? r.getCreates() : new Item();
|
||||||
|
creates.setItemId(cId);
|
||||||
|
r.setCreates(creates);
|
||||||
|
} else {
|
||||||
|
r.setCreates(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<Item, Integer> comps = new LinkedHashMap<>();
|
||||||
|
for (String entry : componentsList.getItems()) {
|
||||||
|
int sep = entry.lastIndexOf(" × ");
|
||||||
|
if (sep < 0) continue;
|
||||||
|
String itemId = entry.substring(0, sep).trim();
|
||||||
|
int count = 1;
|
||||||
|
try { count = Integer.parseInt(entry.substring(sep + 3).trim()); }
|
||||||
|
catch (NumberFormatException ignored) {}
|
||||||
|
Item item = new Item(); item.setItemId(itemId);
|
||||||
|
comps.put(item, count);
|
||||||
|
}
|
||||||
|
r.setComponents(comps.isEmpty() ? null : comps);
|
||||||
|
|
||||||
|
String tv = tableCombo.getValue();
|
||||||
|
if (tv == null || tv.equals(NO_TABLE)) {
|
||||||
|
r.setTable(null);
|
||||||
|
r.setRequiresLvlAlchemy(null);
|
||||||
|
r.setRequiresLvlEngineering(null);
|
||||||
|
r.setRequiresLvlSmithery(null);
|
||||||
|
r.setRequiresLvlEnchanting(null);
|
||||||
|
} else {
|
||||||
|
CraftingTable table = r.getTable() != null ? r.getTable() : new CraftingTable();
|
||||||
|
table.setType(CraftingTable.CraftingTableType.valueOf(tv));
|
||||||
|
r.setTable(table);
|
||||||
|
r.setRequiresLvlAlchemy(null);
|
||||||
|
r.setRequiresLvlEngineering(null);
|
||||||
|
r.setRequiresLvlSmithery(null);
|
||||||
|
r.setRequiresLvlEnchanting(null);
|
||||||
|
switch (tv) {
|
||||||
|
case "AlchemyTable" -> r.setRequiresLvlAlchemy(alchemySpinner.getValue());
|
||||||
|
case "EnchantmentTable" -> r.setRequiresLvlEnchanting(enchantingSpinner.getValue());
|
||||||
|
case "Smithy",
|
||||||
|
"Goldsmiths" -> r.setRequiresLvlSmithery(smitherySpinner.getValue());
|
||||||
|
case "Workshop" -> r.setRequiresLvlEngineering(engineeringSpinner.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearForm() {
|
||||||
|
createsField.clear();
|
||||||
|
componentsList.getItems().clear();
|
||||||
|
tableCombo.setValue(NO_TABLE);
|
||||||
|
updateRequirementRows(NO_TABLE);
|
||||||
|
alchemySpinner.getValueFactory().setValue(1);
|
||||||
|
enchantingSpinner.getValueFactory().setValue(1);
|
||||||
|
smitherySpinner.getValueFactory().setValue(1);
|
||||||
|
engineeringSpinner.getValueFactory().setValue(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── List operations ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void createRecipe() {
|
||||||
|
Recipe r = new Recipe();
|
||||||
|
Item creates = new Item();
|
||||||
|
creates.setItemId("neues_rezept_" + System.currentTimeMillis());
|
||||||
|
r.setCreates(creates);
|
||||||
|
recipes.add(r);
|
||||||
|
listView.getSelectionModel().select(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteSelected() {
|
||||||
|
Recipe sel = listView.getSelectionModel().getSelectedItem();
|
||||||
|
if (sel == null) return;
|
||||||
|
String fid = RecipeIO.fileId(sel);
|
||||||
|
recipes.remove(sel);
|
||||||
|
try { RecipeIO.delete(fid, recipeDir); } catch (IOException ignored) {}
|
||||||
|
current = null;
|
||||||
|
oldFileId = null;
|
||||||
|
clearForm();
|
||||||
|
formContainer.setDisable(true);
|
||||||
|
deleteBtn.setDisable(true);
|
||||||
|
reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveCurrentRecipe() {
|
||||||
|
if (current == null) return;
|
||||||
|
saveFormToRecipe(current);
|
||||||
|
|
||||||
|
String newFileId = RecipeIO.fileId(current);
|
||||||
|
if (newFileId.startsWith("unbenanntes")) {
|
||||||
|
new Alert(Alert.AlertType.ERROR,
|
||||||
|
"Item-ID des erstellten Items darf nicht leer sein.", ButtonType.OK).showAndWait();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (oldFileId != null && !oldFileId.equals(newFileId)) {
|
||||||
|
RecipeIO.delete(oldFileId, recipeDir);
|
||||||
|
}
|
||||||
|
RecipeIO.save(current, recipeDir);
|
||||||
|
oldFileId = newFileId;
|
||||||
|
} catch (IOException e) {
|
||||||
|
new Alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reload();
|
||||||
|
final String fid = newFileId;
|
||||||
|
recipes.stream()
|
||||||
|
.filter(r -> fid.equals(RecipeIO.fileId(r)))
|
||||||
|
.findFirst()
|
||||||
|
.ifPresent(listView.getSelectionModel()::select);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addComponent() {
|
||||||
|
Dialog<String> dlg = new Dialog<>();
|
||||||
|
dlg.setTitle("Zutat hinzufügen");
|
||||||
|
dlg.initModality(Modality.APPLICATION_MODAL);
|
||||||
|
|
||||||
|
TextField itemIdField = new TextField();
|
||||||
|
itemIdField.setPromptText("Item-ID");
|
||||||
|
Spinner<Integer> countSpin = new Spinner<>(1, 9999, 1);
|
||||||
|
countSpin.setEditable(true);
|
||||||
|
|
||||||
|
GridPane grid = new GridPane();
|
||||||
|
grid.setHgap(10); grid.setVgap(8);
|
||||||
|
grid.setPadding(new Insets(12));
|
||||||
|
grid.add(new Label("Item-ID:"), 0, 0); grid.add(itemIdField, 1, 0);
|
||||||
|
grid.add(new Label("Anzahl:"), 0, 1); grid.add(countSpin, 1, 1);
|
||||||
|
GridPane.setHgrow(itemIdField, Priority.ALWAYS);
|
||||||
|
|
||||||
|
dlg.getDialogPane().setContent(grid);
|
||||||
|
dlg.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
|
||||||
|
Button okBtn = (Button) dlg.getDialogPane().lookupButton(ButtonType.OK);
|
||||||
|
okBtn.setDisable(true);
|
||||||
|
itemIdField.textProperty().addListener((obs, o, n) -> okBtn.setDisable(n.isBlank()));
|
||||||
|
|
||||||
|
dlg.setResultConverter(bt -> bt == ButtonType.OK
|
||||||
|
? itemIdField.getText().trim() + " × " + countSpin.getValue() : null);
|
||||||
|
dlg.showAndWait().ifPresent(entry -> {
|
||||||
|
if (!componentsList.getItems().contains(entry)) {
|
||||||
|
componentsList.getItems().add(entry);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void reload() {
|
public void reload() {
|
||||||
sharedRecipes.setAll(RecipeIO.loadAll(recipeDir));
|
recipes.setAll(RecipeIO.loadAll(recipeDir));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Single panel ──────────────────────────────────────────────────────────
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
static class RecipePanel extends VBox {
|
private static String tableColor(CraftingTable t) {
|
||||||
|
if (t == null || t.getType() == null) return "#666666";
|
||||||
private static final String NO_TABLE = "— kein Tisch —";
|
return switch (t.getType()) {
|
||||||
|
case AlchemyTable -> "#44bb88";
|
||||||
private final ObservableList<Recipe> recipes;
|
case EnchantmentTable -> "#aa55ee";
|
||||||
private final Path recipeDir;
|
case Smithy -> "#cc8833";
|
||||||
private final Runnable onSaved;
|
case Goldsmiths -> "#ddbb22";
|
||||||
|
case Workshop -> "#4488cc";
|
||||||
private final SortedList<Recipe> sortedRecipes;
|
case Fireplace -> "#ee6633";
|
||||||
private final ListView<Recipe> listView;
|
case Kitchen -> "#88aa44";
|
||||||
|
};
|
||||||
private Recipe current = null;
|
|
||||||
private String oldFileId = null; // for rename-on-save detection
|
|
||||||
|
|
||||||
// Form fields
|
|
||||||
private TextField createsField;
|
|
||||||
private ListView<String> componentsList; // "itemId × count"
|
|
||||||
private ComboBox<String> tableCombo;
|
|
||||||
|
|
||||||
// Level-Anforderungs-Zeilen (jeweils Label + Spinner)
|
|
||||||
private HBox alchemyRow;
|
|
||||||
private HBox enchantingRow;
|
|
||||||
private HBox smitheryRow;
|
|
||||||
private HBox engineeringRow;
|
|
||||||
private Spinner<Integer> alchemySpinner;
|
|
||||||
private Spinner<Integer> enchantingSpinner;
|
|
||||||
private Spinner<Integer> smitherySpinner;
|
|
||||||
private Spinner<Integer> engineeringSpinner;
|
|
||||||
|
|
||||||
private VBox formContainer;
|
|
||||||
private Button deleteBtn;
|
|
||||||
|
|
||||||
RecipePanel(String title, ObservableList<Recipe> recipes, Path recipeDir, Runnable onSaved) {
|
|
||||||
this.recipes = recipes;
|
|
||||||
this.recipeDir = recipeDir;
|
|
||||||
this.onSaved = onSaved;
|
|
||||||
|
|
||||||
setStyle("-fx-background-color: #252535; -fx-border-color: #3a3a4a; -fx-border-width: 0 1 0 0;");
|
|
||||||
|
|
||||||
// ── Header ────────────────────────────────────────────────────────
|
|
||||||
Label titleLbl = new Label(title);
|
|
||||||
titleLbl.setStyle("-fx-font-weight: bold; -fx-font-size: 13; -fx-text-fill: #aaccee;");
|
|
||||||
Button refreshBtn = new Button("↺");
|
|
||||||
refreshBtn.setStyle("-fx-background-color: transparent; -fx-text-fill: #888; -fx-cursor: hand;");
|
|
||||||
refreshBtn.setOnAction(e -> onSaved.run());
|
|
||||||
HBox header = new HBox(8, titleLbl, refreshBtn);
|
|
||||||
header.setPadding(new Insets(8, 10, 8, 10));
|
|
||||||
header.setAlignment(Pos.CENTER_LEFT);
|
|
||||||
header.setStyle("-fx-background-color: #1a1a2a; -fx-border-color: #444;"
|
|
||||||
+ " -fx-border-width: 0 0 1 0;");
|
|
||||||
|
|
||||||
// ── Recipe list ───────────────────────────────────────────────────
|
|
||||||
sortedRecipes = new SortedList<>(recipes, RecipeIO.SORT_ORDER);
|
|
||||||
listView = new ListView<>(sortedRecipes);
|
|
||||||
listView.setPrefHeight(180);
|
|
||||||
listView.setStyle("-fx-background-color: #1a1a2a; -fx-control-inner-background: #1a1a2a;");
|
|
||||||
listView.setCellFactory(lv -> new ListCell<>() {
|
|
||||||
@Override protected void updateItem(Recipe r, boolean empty) {
|
|
||||||
super.updateItem(r, empty);
|
|
||||||
if (empty || r == null) { setText(null); setStyle(""); return; }
|
|
||||||
String creates = r.getCreates() != null ? safe(r.getCreates().getItemId()) : "—";
|
|
||||||
String table = r.getTable() != null && r.getTable().getType() != null
|
|
||||||
? r.getTable().getType().name() : "Handwerk";
|
|
||||||
setText(creates);
|
|
||||||
setTooltip(new Tooltip("[" + table + "] erstellt: " + creates));
|
|
||||||
String color = tableColor(r.getTable());
|
|
||||||
setStyle("-fx-text-fill: #dddddd;"
|
|
||||||
+ " -fx-border-color: transparent transparent transparent " + color + ";"
|
|
||||||
+ " -fx-border-width: 0 0 0 3; -fx-padding: 3 6 3 8;");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
listView.getSelectionModel().selectedItemProperty()
|
|
||||||
.addListener((obs, old, nw) -> onRecipeSelected(old, nw));
|
|
||||||
|
|
||||||
Button newBtn = new Button("Neues Rezept");
|
|
||||||
newBtn.setMaxWidth(Double.MAX_VALUE);
|
|
||||||
newBtn.setStyle("-fx-background-color: #3a6a3a; -fx-text-fill: white;");
|
|
||||||
newBtn.setOnAction(e -> createRecipe());
|
|
||||||
|
|
||||||
deleteBtn = new Button("Löschen");
|
|
||||||
deleteBtn.setMaxWidth(Double.MAX_VALUE);
|
|
||||||
deleteBtn.setStyle("-fx-background-color: #6a2a2a; -fx-text-fill: white;");
|
|
||||||
deleteBtn.setDisable(true);
|
|
||||||
deleteBtn.setOnAction(e -> deleteSelected());
|
|
||||||
|
|
||||||
HBox listButtons = new HBox(6, newBtn, deleteBtn);
|
|
||||||
listButtons.setPadding(new Insets(6, 8, 6, 8));
|
|
||||||
HBox.setHgrow(newBtn, Priority.ALWAYS);
|
|
||||||
HBox.setHgrow(deleteBtn, Priority.ALWAYS);
|
|
||||||
|
|
||||||
VBox listSection = new VBox(listView, listButtons);
|
|
||||||
listSection.setStyle("-fx-background-color: #1a1a2a;");
|
|
||||||
|
|
||||||
// ── Form ──────────────────────────────────────────────────────────
|
|
||||||
formContainer = buildForm();
|
|
||||||
formContainer.setDisable(true);
|
|
||||||
|
|
||||||
ScrollPane formScroll = new ScrollPane(formContainer);
|
|
||||||
formScroll.setFitToWidth(true);
|
|
||||||
formScroll.setStyle("-fx-background-color: #252535; -fx-background: #252535;");
|
|
||||||
VBox.setVgrow(formScroll, Priority.ALWAYS);
|
|
||||||
|
|
||||||
getChildren().addAll(header, listSection, new Separator(), formScroll);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Form construction ─────────────────────────────────────────────────
|
|
||||||
|
|
||||||
private VBox buildForm() {
|
|
||||||
VBox form = new VBox(6);
|
|
||||||
form.setPadding(new Insets(10));
|
|
||||||
form.setStyle("-fx-background-color: #252535;");
|
|
||||||
|
|
||||||
// Erstellt
|
|
||||||
createsField = new TextField();
|
|
||||||
createsField.setPromptText("Item-ID des erstellten Items");
|
|
||||||
|
|
||||||
// Zutaten
|
|
||||||
componentsList = new ListView<>();
|
|
||||||
componentsList.setPrefHeight(110);
|
|
||||||
componentsList.setStyle("-fx-background-color: #1e1e2e; -fx-control-inner-background: #1e1e2e;");
|
|
||||||
componentsList.setCellFactory(lv -> new ListCell<>() {
|
|
||||||
@Override protected void updateItem(String s, boolean empty) {
|
|
||||||
super.updateItem(s, empty);
|
|
||||||
setText(empty || s == null ? null : s);
|
|
||||||
setStyle(empty ? "" : "-fx-text-fill: #cccccc;");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Button addCompBtn = smallBtn("+");
|
|
||||||
Button delCompBtn = smallBtn("−");
|
|
||||||
addCompBtn.setOnAction(e -> addComponent());
|
|
||||||
delCompBtn.setOnAction(e -> {
|
|
||||||
String sel = componentsList.getSelectionModel().getSelectedItem();
|
|
||||||
if (sel != null) componentsList.getItems().remove(sel);
|
|
||||||
});
|
|
||||||
HBox compButtons = new HBox(4, addCompBtn, delCompBtn);
|
|
||||||
|
|
||||||
form.getChildren().addAll(
|
|
||||||
sectionTitle("Ergebnis"),
|
|
||||||
new Separator(),
|
|
||||||
row("Erstellt:", createsField),
|
|
||||||
sectionTitle("Zutaten"),
|
|
||||||
componentsList,
|
|
||||||
compButtons,
|
|
||||||
new Separator()
|
|
||||||
);
|
|
||||||
|
|
||||||
// Crafting Table
|
|
||||||
tableCombo = new ComboBox<>();
|
|
||||||
tableCombo.getItems().add(NO_TABLE);
|
|
||||||
for (CraftingTable.CraftingTableType t : CraftingTable.CraftingTableType.values())
|
|
||||||
tableCombo.getItems().add(t.name());
|
|
||||||
tableCombo.setValue(NO_TABLE);
|
|
||||||
tableCombo.setMaxWidth(Double.MAX_VALUE);
|
|
||||||
tableCombo.setOnAction(e -> updateRequirementRows(tableCombo.getValue()));
|
|
||||||
|
|
||||||
// Level-Anforderungen (werden je nach Tischtyp aktiviert)
|
|
||||||
alchemySpinner = lvlSpinner();
|
|
||||||
enchantingSpinner = lvlSpinner();
|
|
||||||
smitherySpinner = lvlSpinner();
|
|
||||||
engineeringSpinner = lvlSpinner();
|
|
||||||
|
|
||||||
alchemyRow = requirementRow("Lvl Alchemie:", alchemySpinner);
|
|
||||||
enchantingRow = requirementRow("Lvl Verzauberung:", enchantingSpinner);
|
|
||||||
smitheryRow = requirementRow("Lvl Schmieden:", smitherySpinner);
|
|
||||||
engineeringRow = requirementRow("Lvl Engineering:", engineeringSpinner);
|
|
||||||
|
|
||||||
form.getChildren().addAll(
|
|
||||||
sectionTitle("Crafting Table"),
|
|
||||||
tableCombo,
|
|
||||||
alchemyRow,
|
|
||||||
enchantingRow,
|
|
||||||
smitheryRow,
|
|
||||||
engineeringRow
|
|
||||||
);
|
|
||||||
|
|
||||||
// Anfangs alle Anforderungen deaktiviert
|
|
||||||
updateRequirementRows(NO_TABLE);
|
|
||||||
|
|
||||||
form.getChildren().add(new Separator());
|
|
||||||
|
|
||||||
Button saveBtn = new Button("Rezept speichern");
|
|
||||||
saveBtn.setMaxWidth(Double.MAX_VALUE);
|
|
||||||
saveBtn.setStyle("-fx-font-weight: bold; -fx-background-color: #3a5a8a; -fx-text-fill: white;");
|
|
||||||
saveBtn.setOnAction(e -> saveCurrentRecipe());
|
|
||||||
form.getChildren().add(saveBtn);
|
|
||||||
|
|
||||||
return form;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateRequirementRows(String tableValue) {
|
|
||||||
boolean noTable = tableValue == null || tableValue.equals(NO_TABLE);
|
|
||||||
|
|
||||||
// Alle ausblenden wenn kein Tisch
|
|
||||||
setRowVisible(alchemyRow, false);
|
|
||||||
setRowVisible(enchantingRow, false);
|
|
||||||
setRowVisible(smitheryRow, false);
|
|
||||||
setRowVisible(engineeringRow, false);
|
|
||||||
|
|
||||||
if (noTable) return;
|
|
||||||
|
|
||||||
switch (tableValue) {
|
|
||||||
case "AlchemyTable" -> setRowVisible(alchemyRow, true);
|
|
||||||
case "EnchantmentTable" -> setRowVisible(enchantingRow, true);
|
|
||||||
case "Smithy",
|
|
||||||
"Goldsmiths" -> setRowVisible(smitheryRow, true);
|
|
||||||
case "Workshop" -> setRowVisible(engineeringRow, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void setRowVisible(HBox row, boolean visible) {
|
|
||||||
row.setVisible(visible);
|
|
||||||
row.setManaged(visible);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Form load / save ──────────────────────────────────────────────────
|
|
||||||
|
|
||||||
private void onRecipeSelected(Recipe old, Recipe nw) {
|
|
||||||
if (old != null) saveFormToRecipe(old);
|
|
||||||
current = nw;
|
|
||||||
oldFileId = nw != null ? RecipeIO.fileId(nw) : null;
|
|
||||||
deleteBtn.setDisable(nw == null);
|
|
||||||
if (nw != null) {
|
|
||||||
formContainer.setDisable(false);
|
|
||||||
loadFormFromRecipe(nw);
|
|
||||||
} else {
|
|
||||||
formContainer.setDisable(true);
|
|
||||||
clearForm();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void loadFormFromRecipe(Recipe r) {
|
|
||||||
createsField.setText(r.getCreates() != null ? safe(r.getCreates().getItemId()) : "");
|
|
||||||
|
|
||||||
componentsList.getItems().clear();
|
|
||||||
if (r.getComponents() != null) {
|
|
||||||
r.getComponents().forEach((item, count) ->
|
|
||||||
componentsList.getItems().add(item.getItemId() + " × " + count));
|
|
||||||
}
|
|
||||||
|
|
||||||
String tableVal = NO_TABLE;
|
|
||||||
if (r.getTable() != null && r.getTable().getType() != null)
|
|
||||||
tableVal = r.getTable().getType().name();
|
|
||||||
tableCombo.setValue(tableVal);
|
|
||||||
updateRequirementRows(tableVal);
|
|
||||||
|
|
||||||
alchemySpinner.getValueFactory().setValue(
|
|
||||||
r.getRequiresLvlAlchemy() != null ? r.getRequiresLvlAlchemy() : 1);
|
|
||||||
enchantingSpinner.getValueFactory().setValue(
|
|
||||||
r.getRequiresLvlEnchanting() != null ? r.getRequiresLvlEnchanting() : 1);
|
|
||||||
smitherySpinner.getValueFactory().setValue(
|
|
||||||
r.getRequiresLvlSmithery() != null ? r.getRequiresLvlSmithery() : 1);
|
|
||||||
engineeringSpinner.getValueFactory().setValue(
|
|
||||||
r.getRequiresLvlEngineering() != null ? r.getRequiresLvlEngineering() : 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void saveFormToRecipe(Recipe r) {
|
|
||||||
// creates
|
|
||||||
String cId = createsField.getText().trim();
|
|
||||||
if (!cId.isBlank()) {
|
|
||||||
Item creates = r.getCreates() != null ? r.getCreates() : new Item();
|
|
||||||
creates.setItemId(cId);
|
|
||||||
r.setCreates(creates);
|
|
||||||
} else {
|
|
||||||
r.setCreates(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
// components
|
|
||||||
Map<Item, Integer> comps = new LinkedHashMap<>();
|
|
||||||
for (String entry : componentsList.getItems()) {
|
|
||||||
int sep = entry.lastIndexOf(" × ");
|
|
||||||
if (sep < 0) continue;
|
|
||||||
String itemId = entry.substring(0, sep).trim();
|
|
||||||
int count = 1;
|
|
||||||
try { count = Integer.parseInt(entry.substring(sep + 3).trim()); }
|
|
||||||
catch (NumberFormatException ignored) {}
|
|
||||||
Item item = new Item(); item.setItemId(itemId);
|
|
||||||
comps.put(item, count);
|
|
||||||
}
|
|
||||||
r.setComponents(comps.isEmpty() ? null : comps);
|
|
||||||
|
|
||||||
// table
|
|
||||||
String tv = tableCombo.getValue();
|
|
||||||
if (tv == null || tv.equals(NO_TABLE)) {
|
|
||||||
r.setTable(null);
|
|
||||||
r.setRequiresLvlAlchemy(null);
|
|
||||||
r.setRequiresLvlEngineering(null);
|
|
||||||
r.setRequiresLvlSmithery(null);
|
|
||||||
r.setRequiresLvlEnchanting(null);
|
|
||||||
} else {
|
|
||||||
CraftingTable table = r.getTable() != null ? r.getTable() : new CraftingTable();
|
|
||||||
table.setType(CraftingTable.CraftingTableType.valueOf(tv));
|
|
||||||
r.setTable(table);
|
|
||||||
// Nur das relevante Level-Feld setzen, alle anderen null
|
|
||||||
r.setRequiresLvlAlchemy(null);
|
|
||||||
r.setRequiresLvlEngineering(null);
|
|
||||||
r.setRequiresLvlSmithery(null);
|
|
||||||
r.setRequiresLvlEnchanting(null);
|
|
||||||
switch (tv) {
|
|
||||||
case "AlchemyTable" -> r.setRequiresLvlAlchemy(alchemySpinner.getValue());
|
|
||||||
case "EnchantmentTable" -> r.setRequiresLvlEnchanting(enchantingSpinner.getValue());
|
|
||||||
case "Smithy",
|
|
||||||
"Goldsmiths" -> r.setRequiresLvlSmithery(smitherySpinner.getValue());
|
|
||||||
case "Workshop" -> r.setRequiresLvlEngineering(engineeringSpinner.getValue());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void clearForm() {
|
|
||||||
createsField.clear();
|
|
||||||
componentsList.getItems().clear();
|
|
||||||
tableCombo.setValue(NO_TABLE);
|
|
||||||
updateRequirementRows(NO_TABLE);
|
|
||||||
alchemySpinner.getValueFactory().setValue(1);
|
|
||||||
enchantingSpinner.getValueFactory().setValue(1);
|
|
||||||
smitherySpinner.getValueFactory().setValue(1);
|
|
||||||
engineeringSpinner.getValueFactory().setValue(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── List operations ───────────────────────────────────────────────────
|
|
||||||
|
|
||||||
private void createRecipe() {
|
|
||||||
Recipe r = new Recipe();
|
|
||||||
Item creates = new Item();
|
|
||||||
creates.setItemId("neues_rezept_" + System.currentTimeMillis());
|
|
||||||
r.setCreates(creates);
|
|
||||||
recipes.add(r);
|
|
||||||
listView.getSelectionModel().select(r);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void deleteSelected() {
|
|
||||||
Recipe sel = listView.getSelectionModel().getSelectedItem();
|
|
||||||
if (sel == null) return;
|
|
||||||
String fid = RecipeIO.fileId(sel);
|
|
||||||
recipes.remove(sel);
|
|
||||||
try { RecipeIO.delete(fid, recipeDir); } catch (IOException ignored) {}
|
|
||||||
current = null;
|
|
||||||
oldFileId = null;
|
|
||||||
clearForm();
|
|
||||||
formContainer.setDisable(true);
|
|
||||||
deleteBtn.setDisable(true);
|
|
||||||
onSaved.run();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void saveCurrentRecipe() {
|
|
||||||
if (current == null) return;
|
|
||||||
saveFormToRecipe(current);
|
|
||||||
|
|
||||||
String newFileId = RecipeIO.fileId(current);
|
|
||||||
if (newFileId.startsWith("unbenanntes")) {
|
|
||||||
new Alert(Alert.AlertType.ERROR,
|
|
||||||
"Item-ID des erstellten Items darf nicht leer sein.", ButtonType.OK).showAndWait();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
// Datei umbenennen: alten Eintrag löschen wenn ID sich geändert hat
|
|
||||||
if (oldFileId != null && !oldFileId.equals(newFileId))
|
|
||||||
RecipeIO.delete(oldFileId, recipeDir);
|
|
||||||
RecipeIO.save(current, recipeDir);
|
|
||||||
oldFileId = newFileId;
|
|
||||||
} catch (IOException e) {
|
|
||||||
new Alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
onSaved.run();
|
|
||||||
// Re-select nach Reload
|
|
||||||
final String fid = newFileId;
|
|
||||||
recipes.stream()
|
|
||||||
.filter(r -> fid.equals(RecipeIO.fileId(r)))
|
|
||||||
.findFirst()
|
|
||||||
.ifPresent(listView.getSelectionModel()::select);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addComponent() {
|
|
||||||
Dialog<String> dlg = new Dialog<>();
|
|
||||||
dlg.setTitle("Zutat hinzufügen");
|
|
||||||
dlg.initModality(Modality.APPLICATION_MODAL);
|
|
||||||
|
|
||||||
TextField itemIdField = new TextField();
|
|
||||||
itemIdField.setPromptText("Item-ID");
|
|
||||||
Spinner<Integer> countSpin = new Spinner<>(1, 9999, 1);
|
|
||||||
countSpin.setEditable(true);
|
|
||||||
|
|
||||||
GridPane grid = new GridPane();
|
|
||||||
grid.setHgap(10); grid.setVgap(8);
|
|
||||||
grid.setPadding(new Insets(12));
|
|
||||||
grid.add(new Label("Item-ID:"), 0, 0); grid.add(itemIdField, 1, 0);
|
|
||||||
grid.add(new Label("Anzahl:"), 0, 1); grid.add(countSpin, 1, 1);
|
|
||||||
GridPane.setHgrow(itemIdField, Priority.ALWAYS);
|
|
||||||
|
|
||||||
dlg.getDialogPane().setContent(grid);
|
|
||||||
dlg.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
|
|
||||||
Button okBtn = (Button) dlg.getDialogPane().lookupButton(ButtonType.OK);
|
|
||||||
okBtn.setDisable(true);
|
|
||||||
itemIdField.textProperty().addListener((obs, o, n) -> okBtn.setDisable(n.isBlank()));
|
|
||||||
|
|
||||||
dlg.setResultConverter(bt -> bt == ButtonType.OK
|
|
||||||
? itemIdField.getText().trim() + " × " + countSpin.getValue() : null);
|
|
||||||
dlg.showAndWait().ifPresent(entry -> {
|
|
||||||
if (!componentsList.getItems().contains(entry))
|
|
||||||
componentsList.getItems().add(entry);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Helpers ───────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
private static String tableColor(CraftingTable t) {
|
|
||||||
if (t == null || t.getType() == null) return "#666666";
|
|
||||||
return switch (t.getType()) {
|
|
||||||
case AlchemyTable -> "#44bb88";
|
|
||||||
case EnchantmentTable -> "#aa55ee";
|
|
||||||
case Smithy -> "#cc8833";
|
|
||||||
case Goldsmiths -> "#ddbb22";
|
|
||||||
case Workshop -> "#4488cc";
|
|
||||||
case Fireplace -> "#ee6633";
|
|
||||||
case Kitchen -> "#88aa44";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static HBox requirementRow(String labelText, Spinner<Integer> spinner) {
|
|
||||||
Label lbl = new Label(labelText);
|
|
||||||
lbl.setMinWidth(140);
|
|
||||||
lbl.setStyle("-fx-text-fill: #aaa;");
|
|
||||||
spinner.setMaxWidth(Double.MAX_VALUE);
|
|
||||||
HBox.setHgrow(spinner, Priority.ALWAYS);
|
|
||||||
HBox row = new HBox(8, lbl, spinner);
|
|
||||||
row.setAlignment(Pos.CENTER_LEFT);
|
|
||||||
row.setPadding(new Insets(2, 0, 2, 0));
|
|
||||||
return row;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Spinner<Integer> lvlSpinner() {
|
|
||||||
Spinner<Integer> s = new Spinner<>(1, 100, 1);
|
|
||||||
s.setEditable(true);
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Label sectionTitle(String text) {
|
|
||||||
Label l = new Label(text);
|
|
||||||
l.setStyle("-fx-font-weight: bold; -fx-font-size: 12; -fx-text-fill: #88aacc;");
|
|
||||||
return l;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static HBox row(String labelText, Node control) {
|
|
||||||
Label lbl = new Label(labelText);
|
|
||||||
lbl.setMinWidth(80);
|
|
||||||
lbl.setStyle("-fx-text-fill: #aaa;");
|
|
||||||
HBox.setHgrow(control, Priority.ALWAYS);
|
|
||||||
HBox box = new HBox(8, lbl, control);
|
|
||||||
box.setAlignment(Pos.CENTER_LEFT);
|
|
||||||
return box;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Button smallBtn(String text) {
|
|
||||||
Button b = new Button(text);
|
|
||||||
b.setPrefWidth(28);
|
|
||||||
return b;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String safe(String s) { return s != null ? s : ""; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static HBox requirementRow(String labelText, Spinner<Integer> spinner) {
|
||||||
|
Label lbl = new Label(labelText);
|
||||||
|
lbl.setMinWidth(140);
|
||||||
|
lbl.setStyle("-fx-text-fill: #aaa;");
|
||||||
|
spinner.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
HBox.setHgrow(spinner, Priority.ALWAYS);
|
||||||
|
HBox row = new HBox(8, lbl, spinner);
|
||||||
|
row.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
row.setPadding(new Insets(2, 0, 2, 0));
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Spinner<Integer> lvlSpinner() {
|
||||||
|
Spinner<Integer> s = new Spinner<>(1, 100, 1);
|
||||||
|
s.setEditable(true);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Label sectionTitle(String text) {
|
||||||
|
Label l = new Label(text);
|
||||||
|
l.setStyle("-fx-font-weight: bold; -fx-font-size: 12; -fx-text-fill: #88aacc;");
|
||||||
|
return l;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HBox row(String labelText, Node control) {
|
||||||
|
Label lbl = new Label(labelText);
|
||||||
|
lbl.setMinWidth(80);
|
||||||
|
lbl.setStyle("-fx-text-fill: #aaa;");
|
||||||
|
HBox.setHgrow(control, Priority.ALWAYS);
|
||||||
|
HBox box = new HBox(8, lbl, control);
|
||||||
|
box.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
return box;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Button smallBtn(String text) {
|
||||||
|
Button b = new Button(text);
|
||||||
|
b.setPrefWidth(28);
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String safe(String s) { return s != null ? s : ""; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import com.jme3.system.AppSettings;
|
|||||||
import de.blight.common.BlightHome;
|
import de.blight.common.BlightHome;
|
||||||
import de.blight.common.SaveGameIO;
|
import de.blight.common.SaveGameIO;
|
||||||
import de.blight.game.config.*;
|
import de.blight.game.config.*;
|
||||||
|
import de.blight.game.state.AudioSettingsState;
|
||||||
import de.blight.game.state.SaveGameState;
|
import de.blight.game.state.SaveGameState;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@@ -31,11 +32,13 @@ public class BlightGame extends SimpleApplication {
|
|||||||
|
|
||||||
private KeyBindings keyBindings;
|
private KeyBindings keyBindings;
|
||||||
private GraphicsSettings graphicsSettings;
|
private GraphicsSettings graphicsSettings;
|
||||||
|
private AudioSettings audioSettings;
|
||||||
private ScreenshotAppState screenshotState;
|
private ScreenshotAppState screenshotState;
|
||||||
private Path screenshotDir;
|
private Path screenshotDir;
|
||||||
private WorldScene worldScene;
|
private WorldScene worldScene;
|
||||||
private ConfigScreen configScreen;
|
private ConfigScreen configScreen;
|
||||||
private GraphicsScreen graphicsScreen;
|
private GraphicsScreen graphicsScreen;
|
||||||
|
private AudioScreen audioScreen;
|
||||||
private PauseMenu pauseMenu;
|
private PauseMenu pauseMenu;
|
||||||
private MainMenuState mainMenuState;
|
private MainMenuState mainMenuState;
|
||||||
|
|
||||||
@@ -135,6 +138,8 @@ public class BlightGame extends SimpleApplication {
|
|||||||
|
|
||||||
keyBindings = KeyBindingStore.load();
|
keyBindings = KeyBindingStore.load();
|
||||||
graphicsSettings = GraphicsStore.load();
|
graphicsSettings = GraphicsStore.load();
|
||||||
|
audioSettings = AudioSettingsStore.load();
|
||||||
|
stateManager.attach(new AudioSettingsState(audioSettings));
|
||||||
|
|
||||||
status("Lade Spielstand...");
|
status("Lade Spielstand...");
|
||||||
stateManager.attach(new SaveGameState());
|
stateManager.attach(new SaveGameState());
|
||||||
@@ -164,6 +169,13 @@ public class BlightGame extends SimpleApplication {
|
|||||||
stateManager.attach(graphicsScreen);
|
stateManager.attach(graphicsScreen);
|
||||||
graphicsScreen.setEnabled(false);
|
graphicsScreen.setEnabled(false);
|
||||||
|
|
||||||
|
audioScreen = new AudioScreen(audioSettings, () -> {
|
||||||
|
audioScreen.setEnabled(false);
|
||||||
|
screenCloseTarget.run();
|
||||||
|
});
|
||||||
|
stateManager.attach(audioScreen);
|
||||||
|
audioScreen.setEnabled(false);
|
||||||
|
|
||||||
SaveGameState saveState = stateManager.getState(SaveGameState.class);
|
SaveGameState saveState = stateManager.getState(SaveGameState.class);
|
||||||
|
|
||||||
pauseMenu = new PauseMenu(
|
pauseMenu = new PauseMenu(
|
||||||
@@ -173,6 +185,11 @@ public class BlightGame extends SimpleApplication {
|
|||||||
pauseMenu.setEnabled(false);
|
pauseMenu.setEnabled(false);
|
||||||
graphicsScreen.setEnabled(true);
|
graphicsScreen.setEnabled(true);
|
||||||
},
|
},
|
||||||
|
() -> {
|
||||||
|
screenCloseTarget = () -> pauseMenu.setEnabled(true);
|
||||||
|
pauseMenu.setEnabled(false);
|
||||||
|
audioScreen.setEnabled(true);
|
||||||
|
},
|
||||||
() -> {
|
() -> {
|
||||||
screenCloseTarget = () -> pauseMenu.setEnabled(true);
|
screenCloseTarget = () -> pauseMenu.setEnabled(true);
|
||||||
pauseMenu.setEnabled(false);
|
pauseMenu.setEnabled(false);
|
||||||
@@ -237,6 +254,7 @@ public class BlightGame extends SimpleApplication {
|
|||||||
if (mainMenuState != null && mainMenuState.isEnabled()) return;
|
if (mainMenuState != null && mainMenuState.isEnabled()) return;
|
||||||
|
|
||||||
if (graphicsScreen.isEnabled()) return;
|
if (graphicsScreen.isEnabled()) return;
|
||||||
|
if (audioScreen.isEnabled()) return;
|
||||||
|
|
||||||
if (configScreen.isEnabled()) {
|
if (configScreen.isEnabled()) {
|
||||||
if (configScreen.isWaiting()) {
|
if (configScreen.isWaiting()) {
|
||||||
|
|||||||
@@ -28,6 +28,15 @@ public class AnimSet {
|
|||||||
private String previewModelPath = null;
|
private String previewModelPath = null;
|
||||||
/** Manueller Positions-/Rotations-Versatz pro Clip-Name. */
|
/** Manueller Positions-/Rotations-Versatz pro Clip-Name. */
|
||||||
private Map<String, AnimOffset> animOffsets = new LinkedHashMap<>();
|
private Map<String, AnimOffset> animOffsets = new LinkedHashMap<>();
|
||||||
|
/** Sub-Clip-Definitionen: Name → Zeitfenster aus einem kombinierten Quell-Clip. */
|
||||||
|
private Map<String, SubClipDef> subClips = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
/** Beschreibt einen Sub-Clip: Zeitfenster [start, end] innerhalb eines kombinierten Quell-Clips. */
|
||||||
|
public static class SubClipDef {
|
||||||
|
public String source;
|
||||||
|
public float start;
|
||||||
|
public float end;
|
||||||
|
}
|
||||||
|
|
||||||
public List<String> getClips() { return clips; }
|
public List<String> getClips() { return clips; }
|
||||||
public void setClips(List<String> clips) { this.clips = clips; }
|
public void setClips(List<String> clips) { this.clips = clips; }
|
||||||
@@ -41,6 +50,10 @@ public class AnimSet {
|
|||||||
public void setAnimOffsets(Map<String, AnimOffset> animOffsets) {
|
public void setAnimOffsets(Map<String, AnimOffset> animOffsets) {
|
||||||
this.animOffsets = animOffsets;
|
this.animOffsets = animOffsets;
|
||||||
}
|
}
|
||||||
|
public Map<String, SubClipDef> getSubClips() {
|
||||||
|
return subClips != null ? subClips : new LinkedHashMap<>();
|
||||||
|
}
|
||||||
|
public void setSubClips(Map<String, SubClipDef> subClips) { this.subClips = subClips; }
|
||||||
|
|
||||||
/** Speichert dieses Set als {@code <setName>.animset.json} im Verzeichnis {@code setDir}. */
|
/** Speichert dieses Set als {@code <setName>.animset.json} im Verzeichnis {@code setDir}. */
|
||||||
public void save(Path setDir, String setName) throws IOException {
|
public void save(Path setDir, String setName) throws IOException {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import com.jme3.scene.Spatial;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import com.jme3.math.Quaternion;
|
||||||
import com.jme3.math.Vector3f;
|
import com.jme3.math.Vector3f;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -220,6 +221,71 @@ public class AnimationLibrary extends BaseAppState {
|
|||||||
} else {
|
} else {
|
||||||
log.info("[AnimLib] {} Clips geladen: {}", clips.size(), clips.keySet());
|
log.info("[AnimLib] {} Clips geladen: {}", clips.size(), clips.keySet());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Path setDir = findAssetRoot().resolve("animations").resolve("sets");
|
||||||
|
try {
|
||||||
|
AnimSet animSet = AnimSet.load(setDir, "human");
|
||||||
|
createSubClips(animSet);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[AnimLib] Sub-Clips konnten nicht erstellt werden: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createSubClips(AnimSet animSet) {
|
||||||
|
for (Map.Entry<String, AnimSet.SubClipDef> entry : animSet.getSubClips().entrySet()) {
|
||||||
|
String subName = entry.getKey();
|
||||||
|
AnimSet.SubClipDef def = entry.getValue();
|
||||||
|
AnimClip source = clips.get(def.source);
|
||||||
|
Armature arm = armatures.get(def.source);
|
||||||
|
if (source == null) {
|
||||||
|
log.warn("[AnimLib] SubClip '{}': Quelle '{}' nicht gefunden – übersprungen", subName, def.source);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
AnimClip sub = extractSubClip(source, subName, def.start, def.end);
|
||||||
|
clips.put(subName, sub);
|
||||||
|
if (arm != null) {
|
||||||
|
armatures.put(subName, arm);
|
||||||
|
}
|
||||||
|
log.info("[AnimLib] SubClip '{}' extrahiert [{}-{}s] aus '{}'",
|
||||||
|
subName, def.start, def.end, def.source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AnimClip extractSubClip(AnimClip source, String name, float startSec, float endSec) {
|
||||||
|
List<AnimTrack<?>> newTracks = new ArrayList<>();
|
||||||
|
for (AnimTrack<?> track : source.getTracks()) {
|
||||||
|
if (!(track instanceof TransformTrack tt)) {
|
||||||
|
newTracks.add(track);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
float[] times = tt.getTimes();
|
||||||
|
int first = 0;
|
||||||
|
while (first < times.length - 1 && times[first] < startSec - 1e-5f) {
|
||||||
|
first++;
|
||||||
|
}
|
||||||
|
int last = times.length - 1;
|
||||||
|
while (last > 0 && times[last] > endSec + 1e-5f) {
|
||||||
|
last--;
|
||||||
|
}
|
||||||
|
int count = last - first + 1;
|
||||||
|
if (count <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
float[] newTimes = new float[count];
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
newTimes[i] = times[first + i] - startSec;
|
||||||
|
}
|
||||||
|
Vector3f[] trans = tt.getTranslations();
|
||||||
|
Quaternion[] rots = tt.getRotations();
|
||||||
|
Vector3f[] scales = tt.getScales();
|
||||||
|
Vector3f[] newTrans = trans != null ? Arrays.copyOfRange(trans, first, last + 1) : null;
|
||||||
|
Quaternion[] newRots = rots != null ? Arrays.copyOfRange(rots, first, last + 1) : null;
|
||||||
|
Vector3f[] newScales = scales != null ? Arrays.copyOfRange(scales, first, last + 1) : null;
|
||||||
|
newTracks.add(new TransformTrack(tt.getTarget(), newTimes, newTrans, newRots, newScales));
|
||||||
|
}
|
||||||
|
AnimClip sub = new AnimClip(name);
|
||||||
|
sub.setTracks(newTracks.toArray(new AnimTrack[0]));
|
||||||
|
return sub;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadClipFromFile(Path file) {
|
private void loadClipFromFile(Path file) {
|
||||||
|
|||||||
@@ -0,0 +1,239 @@
|
|||||||
|
package de.blight.game.audio;
|
||||||
|
|
||||||
|
import com.jme3.asset.AssetManager;
|
||||||
|
import com.jme3.audio.AudioData;
|
||||||
|
import com.jme3.audio.AudioNode;
|
||||||
|
import com.jme3.scene.Node;
|
||||||
|
import de.blight.game.state.AudioSettingsState;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Random;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dreistufige Fallback-Logik:
|
||||||
|
* 1. audio/footsteps/{surface}/{gait}/*.ogg
|
||||||
|
* 2. audio/footsteps/{surface}/*.ogg (für alle Gangarten, Lautstärke gait-abhängig)
|
||||||
|
* 3. audio/footsteps/*.ogg (Wurzel-Fallback, gleiche Lautstärke-Logik)
|
||||||
|
*
|
||||||
|
* Auf jeder Stufe werden auch Alias-Oberflächen geprüft:
|
||||||
|
* grass ↔ leaves | pavement ↔ rock | dirt ↔ gravel
|
||||||
|
*
|
||||||
|
* Lautstärke nach Gangart: sprinting=100 %, running=80 %, walking=50 % von BASE_VOLUME.
|
||||||
|
*/
|
||||||
|
public class FootstepSystem {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(FootstepSystem.class);
|
||||||
|
|
||||||
|
private static final float BASE_VOLUME = 0.70f;
|
||||||
|
private static final float PITCH_CENTER = 1.00f;
|
||||||
|
private static final float PITCH_RANGE = 0.10f;
|
||||||
|
|
||||||
|
private static final String[] GAITS = {"walking", "running", "sprinting"};
|
||||||
|
|
||||||
|
// surface-name → alias surface-names
|
||||||
|
private static final Map<String, List<String>> ALIASES;
|
||||||
|
static {
|
||||||
|
ALIASES = new HashMap<>();
|
||||||
|
ALIASES.put("grass", List.of("leaves"));
|
||||||
|
ALIASES.put("leaves", List.of("grass"));
|
||||||
|
ALIASES.put("pavement", List.of("rock"));
|
||||||
|
ALIASES.put("rock", List.of("pavement"));
|
||||||
|
ALIASES.put("dirt", List.of("gravel"));
|
||||||
|
ALIASES.put("gravel", List.of("dirt"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private final AudioSettingsState audioSettings;
|
||||||
|
private final Random random = new Random();
|
||||||
|
|
||||||
|
/** Stufe 1: surface → gait → AudioNodes */
|
||||||
|
private final Map<String, Map<String, List<AudioNode>>> gaitPools = new HashMap<>();
|
||||||
|
/** Stufe 2: surface → AudioNodes (flach, kein Gait-Unterordner) */
|
||||||
|
private final Map<String, List<AudioNode>> flatPools = new HashMap<>();
|
||||||
|
/** Stufe 3: Dateien direkt in audio/footsteps/ */
|
||||||
|
private final List<AudioNode> rootPool = new ArrayList<>();
|
||||||
|
|
||||||
|
public FootstepSystem(AssetManager am, Node parentNode, AudioSettingsState audioSettings,
|
||||||
|
Path assetRoot) {
|
||||||
|
this.audioSettings = audioSettings;
|
||||||
|
|
||||||
|
String rootDir = "audio/footsteps/";
|
||||||
|
|
||||||
|
for (SurfaceType st : SurfaceType.values()) {
|
||||||
|
String key = st.name().toLowerCase();
|
||||||
|
|
||||||
|
// Stufe 1: gait-Unterordner
|
||||||
|
Map<String, List<AudioNode>> gaitMap = new HashMap<>();
|
||||||
|
for (String gait : GAITS) {
|
||||||
|
List<AudioNode> nodes = scanDir(am, parentNode, assetRoot,
|
||||||
|
rootDir + key + "/" + gait + "/");
|
||||||
|
if (!nodes.isEmpty()) {
|
||||||
|
gaitMap.put(gait, nodes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!gaitMap.isEmpty()) {
|
||||||
|
gaitPools.put(key, gaitMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stufe 2: Oberflächen-Ordner flach
|
||||||
|
List<AudioNode> flat = scanDir(am, parentNode, assetRoot, rootDir + key + "/");
|
||||||
|
if (!flat.isEmpty()) {
|
||||||
|
flatPools.put(key, flat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stufe 3: Wurzel-Ordner flach
|
||||||
|
rootPool.addAll(scanDir(am, parentNode, assetRoot, rootDir));
|
||||||
|
|
||||||
|
int total = gaitPools.values().stream()
|
||||||
|
.mapToInt(m -> m.values().stream().mapToInt(List::size).sum()).sum()
|
||||||
|
+ flatPools.values().stream().mapToInt(List::size).sum()
|
||||||
|
+ rootPool.size();
|
||||||
|
log.info("[Footstep] Geladen – Gait-Pools: {}, Flat-Pools: {}, Root: {}, Gesamt: {} AudioNodes",
|
||||||
|
gaitPools.size(), flatPools.size(), rootPool.size(), total);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spielt Schrittsounds für alle Oberflächen parallel, deren Anteil ≥ MIN_WEIGHT ist.
|
||||||
|
* Jeder Sound wird mit vol = base_vol × fraction abgespielt.
|
||||||
|
*
|
||||||
|
* @param surfaceWeights Normierte Anteile (0..1) pro SurfaceType.ordinal()
|
||||||
|
* @param gait "walking" / "running" / "sprinting"
|
||||||
|
*/
|
||||||
|
public void play(float[] surfaceWeights, String gait) {
|
||||||
|
float baseVol = BASE_VOLUME * gaitVolumeFactor(gait)
|
||||||
|
* (audioSettings != null ? audioSettings.effectiveEffects() : 1f);
|
||||||
|
float pitch = PITCH_CENTER + (random.nextFloat() - 0.5f) * PITCH_RANGE;
|
||||||
|
|
||||||
|
SurfaceType[] types = SurfaceType.values();
|
||||||
|
for (int i = 0; i < surfaceWeights.length && i < types.length; i++) {
|
||||||
|
float fraction = surfaceWeights[i];
|
||||||
|
if (fraction < MIN_WEIGHT) continue;
|
||||||
|
|
||||||
|
SurfaceType surface = types[i];
|
||||||
|
if (surface == SurfaceType.UNKNOWN) continue;
|
||||||
|
|
||||||
|
String key = surface.name().toLowerCase();
|
||||||
|
List<AudioNode> nodes = resolve(key, gait);
|
||||||
|
if (nodes == null || nodes.isEmpty()) continue;
|
||||||
|
|
||||||
|
AudioNode node = nodes.get(random.nextInt(nodes.size()));
|
||||||
|
node.setVolume(baseVol * fraction);
|
||||||
|
node.setPitch(pitch);
|
||||||
|
node.playInstance();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Minimaler Anteil (0..1) damit eine Oberfläche einen eigenen Sound bekommt. */
|
||||||
|
private static final float MIN_WEIGHT = 0.10f;
|
||||||
|
|
||||||
|
// ── Auflösung ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private List<AudioNode> resolve(String key, String gait) {
|
||||||
|
// Stufe 1a: exakt surface/gait
|
||||||
|
List<AudioNode> r = fromGaitPool(key, gait);
|
||||||
|
if (r != null) return r;
|
||||||
|
|
||||||
|
// Stufe 1b: Alias/gait
|
||||||
|
for (String alias : aliases(key)) {
|
||||||
|
r = fromGaitPool(alias, gait);
|
||||||
|
if (r != null) return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stufe 2a: surface flach
|
||||||
|
r = fromFlatPool(key);
|
||||||
|
if (r != null) return r;
|
||||||
|
|
||||||
|
// Stufe 2b: Alias flach
|
||||||
|
for (String alias : aliases(key)) {
|
||||||
|
r = fromFlatPool(alias);
|
||||||
|
if (r != null) return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stufe 3: Wurzel
|
||||||
|
if (!rootPool.isEmpty()) return rootPool;
|
||||||
|
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<AudioNode> fromGaitPool(String key, String gait) {
|
||||||
|
Map<String, List<AudioNode>> m = gaitPools.get(key);
|
||||||
|
if (m == null) return null;
|
||||||
|
List<AudioNode> l = m.get(gait);
|
||||||
|
return (l != null && !l.isEmpty()) ? l : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<AudioNode> fromFlatPool(String key) {
|
||||||
|
List<AudioNode> l = flatPools.get(key);
|
||||||
|
return (l != null && !l.isEmpty()) ? l : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> aliases(String key) {
|
||||||
|
return ALIASES.getOrDefault(key, Collections.emptyList());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Lautstärke ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static float gaitVolumeFactor(String gait) {
|
||||||
|
return switch (gait) {
|
||||||
|
case "sprinting" -> 1.00f;
|
||||||
|
case "running" -> 0.80f;
|
||||||
|
default -> 0.50f; // walking
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Laden ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liest alle .ogg-Dateien direkt (nicht rekursiv) aus relDir.
|
||||||
|
* Unterordner werden ignoriert (die werden separat gescannt).
|
||||||
|
*/
|
||||||
|
private List<AudioNode> scanDir(AssetManager am, Node parentNode,
|
||||||
|
Path assetRoot, String relDir) {
|
||||||
|
Path absDir = assetRoot.resolve(relDir);
|
||||||
|
if (!Files.isDirectory(absDir)) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Path> oggFiles;
|
||||||
|
try (var stream = Files.list(absDir)) {
|
||||||
|
oggFiles = stream
|
||||||
|
.filter(p -> Files.isRegularFile(p)
|
||||||
|
&& p.getFileName().toString().toLowerCase().endsWith(".ogg"))
|
||||||
|
.sorted()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("[Footstep] Fehler beim Scannen von {}: {}", absDir, e.getMessage());
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<AudioNode> nodes = new ArrayList<>(oggFiles.size());
|
||||||
|
for (Path file : oggFiles) {
|
||||||
|
String assetPath = relDir + file.getFileName().toString();
|
||||||
|
try {
|
||||||
|
AudioNode node = new AudioNode(am, assetPath, AudioData.DataType.Buffer);
|
||||||
|
node.setPositional(false);
|
||||||
|
node.setLooping(false);
|
||||||
|
node.setVolume(BASE_VOLUME);
|
||||||
|
parentNode.attachChild(node);
|
||||||
|
nodes.add(node);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[Footstep] Nicht ladbar '{}': {}", assetPath, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nodes.isEmpty()) {
|
||||||
|
log.debug("[Footstep] {}: {} Sounds", relDir, nodes.size());
|
||||||
|
}
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package de.blight.game.audio;
|
||||||
|
|
||||||
|
public enum SurfaceType {
|
||||||
|
GRASS, DIRT, SAND, ROCK, GRAVEL, LEAVES, PAVEMENT, WOOD, UNKNOWN;
|
||||||
|
|
||||||
|
public static SurfaceType fromTexturePath(String path) {
|
||||||
|
if (path == null || path.isEmpty()) return UNKNOWN;
|
||||||
|
String p = path.toLowerCase();
|
||||||
|
if (p.contains("gras")) return GRASS;
|
||||||
|
if (p.contains("dirt")) return DIRT;
|
||||||
|
if (p.contains("sand")) return SAND;
|
||||||
|
if (p.contains("rock")) return ROCK;
|
||||||
|
if (p.contains("gravel")) return GRAVEL;
|
||||||
|
if (p.contains("leaves")) return LEAVES;
|
||||||
|
if (p.contains("pavement")) return PAVEMENT;
|
||||||
|
if (p.contains("wood")) return WOOD;
|
||||||
|
return UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SurfaceType fromName(String name) {
|
||||||
|
if (name == null || name.isEmpty()) return UNKNOWN;
|
||||||
|
try {
|
||||||
|
return SurfaceType.valueOf(name.toUpperCase());
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return UNKNOWN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,309 +1,275 @@
|
|||||||
package de.blight.game.config;
|
package de.blight.game.config;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import com.jme3.app.Application;
|
import com.jme3.app.Application;
|
||||||
import com.jme3.app.SimpleApplication;
|
import com.jme3.app.SimpleApplication;
|
||||||
import com.jme3.app.state.BaseAppState;
|
import com.jme3.app.state.BaseAppState;
|
||||||
import com.jme3.font.BitmapFont;
|
import com.jme3.font.BitmapFont;
|
||||||
import com.jme3.font.BitmapText;
|
import com.jme3.font.BitmapText;
|
||||||
import com.jme3.input.KeyInput;
|
import com.jme3.input.KeyInput;
|
||||||
import com.jme3.input.MouseInput;
|
import com.jme3.input.MouseInput;
|
||||||
import com.jme3.input.RawInputListener;
|
import com.jme3.input.RawInputListener;
|
||||||
import com.jme3.input.controls.ActionListener;
|
import com.jme3.input.controls.ActionListener;
|
||||||
import com.jme3.input.controls.MouseButtonTrigger;
|
import com.jme3.input.controls.MouseButtonTrigger;
|
||||||
import com.jme3.input.event.JoyAxisEvent;
|
import com.jme3.input.event.JoyAxisEvent;
|
||||||
import com.jme3.input.event.JoyButtonEvent;
|
import com.jme3.input.event.JoyButtonEvent;
|
||||||
import com.jme3.input.event.KeyInputEvent;
|
import com.jme3.input.event.KeyInputEvent;
|
||||||
import com.jme3.input.event.MouseButtonEvent;
|
import com.jme3.input.event.MouseButtonEvent;
|
||||||
import com.jme3.input.event.MouseMotionEvent;
|
import com.jme3.input.event.MouseMotionEvent;
|
||||||
import com.jme3.input.event.TouchEvent;
|
import com.jme3.input.event.TouchEvent;
|
||||||
import com.jme3.material.Material;
|
import com.jme3.material.Material;
|
||||||
import com.jme3.material.RenderState;
|
import com.jme3.material.RenderState;
|
||||||
import com.jme3.math.ColorRGBA;
|
import com.jme3.math.ColorRGBA;
|
||||||
import com.jme3.math.Vector2f;
|
import com.jme3.math.Vector2f;
|
||||||
import com.jme3.renderer.queue.RenderQueue;
|
import com.jme3.renderer.queue.RenderQueue;
|
||||||
import com.jme3.scene.Geometry;
|
import com.jme3.scene.Geometry;
|
||||||
import com.jme3.scene.Node;
|
import com.jme3.scene.Node;
|
||||||
import com.jme3.scene.shape.Quad;
|
import com.jme3.scene.shape.Quad;
|
||||||
|
import de.blight.lang.TextResolver;
|
||||||
/**
|
|
||||||
* Overlay-AppState der die Tastenbelegungs-Maske anzeigt.
|
public class ConfigScreen extends BaseAppState implements RawInputListener {
|
||||||
*
|
|
||||||
* ESC → Schließen (ohne Speichern)
|
private static final ColorRGBA COL_ROW = new ColorRGBA(0.18f, 0.18f, 0.28f, 1.00f);
|
||||||
* Klick auf Row → wartet auf neue Taste
|
private static final ColorRGBA COL_ROW_WAIT = new ColorRGBA(0.50f, 0.30f, 0.10f, 1.00f);
|
||||||
* ESC während Warten → bricht nur die Zuweisung ab
|
private static final ColorRGBA COL_TEXT = ColorRGBA.White;
|
||||||
* Speichern → schreibt JSON, ruft onSave-Callback
|
private static final ColorRGBA COL_TEXT_KEY = new ColorRGBA(0.85f, 0.85f, 0.50f, 1.00f);
|
||||||
*/
|
|
||||||
public class ConfigScreen extends BaseAppState implements RawInputListener {
|
private SimpleApplication app;
|
||||||
|
private Node guiNode;
|
||||||
// Farben
|
private BitmapFont font;
|
||||||
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 KeyBindings liveBindings;
|
||||||
private static final ColorRGBA COL_ROW = new ColorRGBA(0.18f, 0.18f, 0.28f, 1.00f);
|
private KeyBindings editCopy;
|
||||||
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 Runnable onSave;
|
||||||
private static final ColorRGBA COL_BTN_SAVE = new ColorRGBA(0.15f, 0.40f, 0.15f, 1.00f);
|
private Runnable onClose;
|
||||||
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 Node panel;
|
||||||
private static final ColorRGBA COL_TEXT_KEY = new ColorRGBA(0.85f, 0.85f, 0.50f, 1.00f);
|
private Node bgLayer;
|
||||||
|
private Node canvasNode;
|
||||||
// -----------------------------------------------------------------------
|
private List<Row> rows = new ArrayList<>();
|
||||||
|
private int waitingRow = -1;
|
||||||
private SimpleApplication app;
|
|
||||||
private Node guiNode;
|
private float saveBtnX, saveBtnY, saveBtnW, saveBtnH;
|
||||||
private BitmapFont font;
|
private float cancelBtnX, cancelBtnY;
|
||||||
|
|
||||||
private KeyBindings liveBindings; // geteilt mit der ganzen App
|
private static class Row {
|
||||||
private KeyBindings editCopy; // wird beim Öffnen geklont
|
String field;
|
||||||
|
BitmapText keyText;
|
||||||
private Runnable onSave; // Callback → PlayerInputControl.reloadBindings
|
Geometry bg;
|
||||||
private Runnable onClose; // Callback → PauseMenu wiederherstellen
|
float x, y, w, h;
|
||||||
|
}
|
||||||
private Node panel;
|
|
||||||
private List<Row> rows = new ArrayList<>();
|
public ConfigScreen(KeyBindings liveBindings, Runnable onSave) {
|
||||||
private int waitingRow = -1; // -1 = keine Zuweisung aktiv
|
this.liveBindings = liveBindings;
|
||||||
|
this.onSave = onSave;
|
||||||
// UI-Elemente für Buttons (Bounds in Screen-Koordinaten)
|
}
|
||||||
private float saveBtnX, saveBtnY, saveBtnW, saveBtnH;
|
|
||||||
private float cancelBtnX, cancelBtnY;
|
public boolean isWaiting() { return waitingRow >= 0; }
|
||||||
|
public void setOnClose(Runnable onClose) { this.onClose = onClose; }
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
|
public void cancelWaiting() {
|
||||||
private static class Row {
|
if (waitingRow >= 0) { resetRowColor(waitingRow); waitingRow = -1; }
|
||||||
String field;
|
}
|
||||||
String label;
|
|
||||||
BitmapText keyText;
|
@Override
|
||||||
Geometry bg;
|
protected void initialize(Application app) {
|
||||||
float x, y, w, h; // Button-Bounds
|
this.app = (SimpleApplication) app;
|
||||||
}
|
this.guiNode = this.app.getGuiNode();
|
||||||
|
this.font = app.getAssetManager().loadFont("Interface/Fonts/Default.fnt");
|
||||||
// -----------------------------------------------------------------------
|
}
|
||||||
|
|
||||||
public ConfigScreen(KeyBindings liveBindings, Runnable onSave) {
|
@Override
|
||||||
this.liveBindings = liveBindings;
|
protected void onEnable() {
|
||||||
this.onSave = onSave;
|
editCopy = liveBindings.copy();
|
||||||
}
|
waitingRow = -1;
|
||||||
|
buildUI();
|
||||||
public boolean isWaiting() { return waitingRow >= 0; }
|
app.getInputManager().setCursorVisible(true);
|
||||||
|
app.getInputManager().addRawInputListener(this);
|
||||||
public void setOnClose(Runnable onClose) { this.onClose = onClose; }
|
app.getInputManager().addMapping("_CfgClick", new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
|
||||||
|
app.getInputManager().addListener(clickListener, "_CfgClick");
|
||||||
public void cancelWaiting() {
|
}
|
||||||
if (waitingRow >= 0) { resetRowColor(waitingRow); waitingRow = -1; }
|
|
||||||
}
|
@Override
|
||||||
|
protected void onDisable() {
|
||||||
// -----------------------------------------------------------------------
|
if (bgLayer != null) { guiNode.detachChild(bgLayer); bgLayer = null; }
|
||||||
// Lifecycle
|
if (canvasNode != null) { guiNode.detachChild(canvasNode); canvasNode = null; }
|
||||||
// -----------------------------------------------------------------------
|
panel = null;
|
||||||
|
rows.clear();
|
||||||
@Override
|
waitingRow = -1;
|
||||||
protected void initialize(Application app) {
|
app.getInputManager().removeRawInputListener(this);
|
||||||
this.app = (SimpleApplication) app;
|
app.getInputManager().deleteMapping("_CfgClick");
|
||||||
this.guiNode = this.app.getGuiNode();
|
app.getInputManager().setCursorVisible(false);
|
||||||
this.font = app.getAssetManager().loadFont("Interface/Fonts/Default.fnt");
|
}
|
||||||
}
|
|
||||||
|
@Override protected void cleanup(Application app) {}
|
||||||
@Override
|
|
||||||
protected void onEnable() {
|
private void buildUI() {
|
||||||
editCopy = liveBindings.copy();
|
float sw = MenuCanvas.REF_W;
|
||||||
waitingRow = -1;
|
float sh = MenuCanvas.REF_H;
|
||||||
buildUI();
|
|
||||||
app.getInputManager().setCursorVisible(true);
|
bgLayer = MenuCanvas.createBgLayer(app.getAssetManager(), app.getCamera());
|
||||||
app.getInputManager().addRawInputListener(this);
|
canvasNode = MenuCanvas.createCanvas(app.getCamera());
|
||||||
app.getInputManager().addMapping("_CfgClick", new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
|
guiNode.attachChild(bgLayer);
|
||||||
app.getInputManager().addListener(clickListener, "_CfgClick");
|
guiNode.attachChild(canvasNode);
|
||||||
}
|
|
||||||
|
panel = new Node("cfg-panel");
|
||||||
@Override
|
|
||||||
protected void onDisable() {
|
float pw = 720, ph = 440;
|
||||||
if (panel != null) { guiNode.detachChild(panel); panel = null; }
|
float px = (sw - pw) / 2f, py = (sh - ph) / 2f;
|
||||||
rows.clear();
|
panel.attachChild(NinePatch.panel(app.getAssetManager()).build(px, py, pw, ph, -1));
|
||||||
waitingRow = -1;
|
|
||||||
app.getInputManager().removeRawInputListener(this);
|
BitmapText title = text(t("menu.controls.title"), 20, COL_TEXT);
|
||||||
app.getInputManager().deleteMapping("_CfgClick");
|
centerText(title, px, py + ph - 40, pw);
|
||||||
app.getInputManager().setCursorVisible(false);
|
panel.attachChild(title);
|
||||||
}
|
|
||||||
|
BitmapText hint = text(t("menu.controls.hint"), 14, new ColorRGBA(0.7f, 0.7f, 0.7f, 1f));
|
||||||
@Override protected void cleanup(Application app) {}
|
centerText(hint, px, py + ph - 70, pw);
|
||||||
|
panel.attachChild(hint);
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
// UI aufbauen
|
float rowX = px + 30;
|
||||||
// -----------------------------------------------------------------------
|
float keyX = px + pw - 220;
|
||||||
|
float rowW = 180;
|
||||||
private void buildUI() {
|
float rowH = 36;
|
||||||
float sw = app.getCamera().getWidth();
|
float startY = py + ph - 110;
|
||||||
float sh = app.getCamera().getHeight();
|
float stepY = 48;
|
||||||
|
|
||||||
panel = new Node("cfg-panel");
|
for (int i = 0; i < KeyBindings.ENTRIES.length; i++) {
|
||||||
|
String[] entry = KeyBindings.ENTRIES[i];
|
||||||
// Halbdurchsichtiger Overlay über dem Spiel
|
float ry = startY - i * stepY;
|
||||||
addQuad(panel, 0, 0, sw, sh, COL_BG, -2);
|
|
||||||
|
BitmapText lbl = text(t("key." + entry[0]), 16, COL_TEXT);
|
||||||
float pw = 720, ph = 440;
|
lbl.setLocalTranslation(rowX, ry + rowH - 8, 0);
|
||||||
float px = (sw - pw) / 2f, py = (sh - ph) / 2f;
|
panel.attachChild(lbl);
|
||||||
addQuad(panel, px, py, pw, ph, COL_PANEL, -1);
|
|
||||||
|
Geometry bg = addQuad(panel, keyX, ry, rowW, rowH, COL_ROW, 0);
|
||||||
// Titel
|
|
||||||
BitmapText title = text("TASTENBELEGUNG", 20, COL_TEXT);
|
BitmapText kt = text(KeyNames.of(editCopy.get(entry[0])), 16, COL_TEXT_KEY);
|
||||||
centerText(title, px, py + ph - 40, pw);
|
kt.setLocalTranslation(keyX + 10, ry + rowH - 8, 1);
|
||||||
panel.attachChild(title);
|
panel.attachChild(kt);
|
||||||
|
|
||||||
BitmapText hint = text("Klicke eine Taste um sie neu zu belegen", 14, new ColorRGBA(0.7f, 0.7f, 0.7f, 1f));
|
Row row = new Row();
|
||||||
centerText(hint, px, py + ph - 70, pw);
|
row.field = entry[0];
|
||||||
panel.attachChild(hint);
|
row.keyText = kt;
|
||||||
|
row.bg = bg;
|
||||||
// Reihen
|
row.x = keyX; row.y = ry; row.w = rowW; row.h = rowH;
|
||||||
float rowX = px + 30;
|
rows.add(row);
|
||||||
float keyX = px + pw - 220;
|
}
|
||||||
float rowW = 180;
|
|
||||||
float rowH = 36;
|
float btnW = 160, btnH = 42;
|
||||||
float startY = py + ph - 110;
|
float btnY = py + 25;
|
||||||
float stepY = 48;
|
saveBtnX = px + pw / 2f - btnW - 15;
|
||||||
|
saveBtnY = btnY;
|
||||||
for (int i = 0; i < KeyBindings.ENTRIES.length; i++) {
|
saveBtnW = btnW;
|
||||||
String[] entry = KeyBindings.ENTRIES[i];
|
saveBtnH = btnH;
|
||||||
float ry = startY - i * stepY;
|
cancelBtnX = px + pw / 2f + 15;
|
||||||
|
cancelBtnY = btnY;
|
||||||
BitmapText lbl = text(entry[1], 16, COL_TEXT);
|
|
||||||
lbl.setLocalTranslation(rowX, ry + rowH - 8, 0);
|
panel.attachChild(NinePatch.buttonSave(app.getAssetManager()).build(saveBtnX, saveBtnY, btnW, btnH, 0));
|
||||||
panel.attachChild(lbl);
|
BitmapText saveLabel = text(t("menu.controls.btn.save"), 16, COL_TEXT);
|
||||||
|
centerText(saveLabel, saveBtnX, saveBtnY + btnH - 10, btnW);
|
||||||
Geometry bg = addQuad(panel, keyX, ry, rowW, rowH, COL_ROW, 0);
|
panel.attachChild(saveLabel);
|
||||||
|
|
||||||
BitmapText kt = text(KeyNames.of(editCopy.get(entry[0])), 16, COL_TEXT_KEY);
|
panel.attachChild(NinePatch.buttonQuit(app.getAssetManager()).build(cancelBtnX, cancelBtnY, btnW, btnH, 0));
|
||||||
kt.setLocalTranslation(keyX + 10, ry + rowH - 8, 1);
|
BitmapText cancelLabel = text(t("menu.controls.btn.cancel"), 16, COL_TEXT);
|
||||||
panel.attachChild(kt);
|
centerText(cancelLabel, cancelBtnX, cancelBtnY + btnH - 10, btnW);
|
||||||
|
panel.attachChild(cancelLabel);
|
||||||
Row row = new Row();
|
|
||||||
row.field = entry[0];
|
canvasNode.attachChild(panel);
|
||||||
row.label = entry[1];
|
}
|
||||||
row.keyText = kt;
|
|
||||||
row.bg = bg;
|
private final ActionListener clickListener = (name, isPressed, tpf) -> {
|
||||||
row.x = keyX; row.y = ry; row.w = rowW; row.h = rowH;
|
if (!isPressed) return;
|
||||||
rows.add(row);
|
float[] v = toVirtual(app.getInputManager().getCursorPosition());
|
||||||
}
|
|
||||||
|
for (int i = 0; i < rows.size(); i++) {
|
||||||
// Buttons
|
Row r = rows.get(i);
|
||||||
float btnW = 160, btnH = 42;
|
if (hits(v, r.x, r.y, r.w, r.h)) {
|
||||||
float btnY = py + 25;
|
waitingRow = i;
|
||||||
saveBtnX = px + pw / 2f - btnW - 15;
|
r.bg.getMaterial().setColor("Color", COL_ROW_WAIT);
|
||||||
saveBtnY = btnY;
|
r.keyText.setText("...");
|
||||||
saveBtnW = btnW;
|
return;
|
||||||
saveBtnH = btnH;
|
}
|
||||||
cancelBtnX = px + pw / 2f + 15;
|
}
|
||||||
cancelBtnY = btnY;
|
|
||||||
|
if (hits(v, saveBtnX, saveBtnY, saveBtnW, saveBtnH)) {
|
||||||
addQuad(panel, saveBtnX, saveBtnY, btnW, btnH, COL_BTN_SAVE, 0);
|
liveBindings.copyFrom(editCopy);
|
||||||
BitmapText saveLabel = text("Speichern", 16, COL_TEXT);
|
KeyBindingStore.save(liveBindings);
|
||||||
centerText(saveLabel, saveBtnX, saveBtnY + btnH - 10, btnW);
|
if (onSave != null) onSave.run();
|
||||||
panel.attachChild(saveLabel);
|
setEnabled(false);
|
||||||
|
if (onClose != null) onClose.run();
|
||||||
addQuad(panel, cancelBtnX, cancelBtnY, btnW, btnH, COL_BTN_CANCEL, 0);
|
return;
|
||||||
BitmapText cancelLabel = text("Abbrechen", 16, COL_TEXT);
|
}
|
||||||
centerText(cancelLabel, cancelBtnX, cancelBtnY + btnH - 10, btnW);
|
|
||||||
panel.attachChild(cancelLabel);
|
if (hits(v, cancelBtnX, cancelBtnY, saveBtnW, saveBtnH)) {
|
||||||
|
setEnabled(false);
|
||||||
guiNode.attachChild(panel);
|
if (onClose != null) onClose.run();
|
||||||
}
|
}
|
||||||
|
};
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
// Mausklick
|
@Override
|
||||||
// -----------------------------------------------------------------------
|
public void onKeyEvent(KeyInputEvent evt) {
|
||||||
|
if (!evt.isPressed() || waitingRow < 0) return;
|
||||||
private final ActionListener clickListener = (name, isPressed, tpf) -> {
|
if (evt.getKeyCode() == KeyInput.KEY_ESCAPE) return;
|
||||||
if (!isPressed) return;
|
|
||||||
Vector2f cursor = app.getInputManager().getCursorPosition();
|
Row r = rows.get(waitingRow);
|
||||||
|
editCopy.set(r.field, evt.getKeyCode());
|
||||||
// Reihen prüfen
|
r.keyText.setText(KeyNames.of(evt.getKeyCode()));
|
||||||
for (int i = 0; i < rows.size(); i++) {
|
resetRowColor(waitingRow);
|
||||||
Row r = rows.get(i);
|
waitingRow = -1;
|
||||||
if (hits(cursor, r.x, r.y, r.w, r.h)) {
|
}
|
||||||
waitingRow = i;
|
|
||||||
r.bg.getMaterial().setColor("Color", COL_ROW_WAIT);
|
private void resetRowColor(int idx) {
|
||||||
r.keyText.setText("...");
|
rows.get(idx).bg.getMaterial().setColor("Color", COL_ROW);
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
}
|
private float[] toVirtual(Vector2f screen) {
|
||||||
|
float scale = Math.min(app.getCamera().getWidth() / MenuCanvas.REF_W,
|
||||||
// Speichern
|
app.getCamera().getHeight() / MenuCanvas.REF_H);
|
||||||
if (hits(cursor, saveBtnX, saveBtnY, saveBtnW, saveBtnH)) {
|
float ox = (app.getCamera().getWidth() - MenuCanvas.REF_W * scale) / 2f;
|
||||||
liveBindings.copyFrom(editCopy);
|
float oy = (app.getCamera().getHeight() - MenuCanvas.REF_H * scale) / 2f;
|
||||||
KeyBindingStore.save(liveBindings);
|
return new float[]{ (screen.x - ox) / scale, (screen.y - oy) / scale };
|
||||||
if (onSave != null) onSave.run();
|
}
|
||||||
setEnabled(false);
|
|
||||||
if (onClose != null) onClose.run();
|
private static String t(String id) { return TextResolver.get().resolveId(id); }
|
||||||
return;
|
|
||||||
}
|
@Override public void beginInput() {}
|
||||||
|
@Override public void endInput() {}
|
||||||
// Abbrechen
|
@Override public void onMouseMotionEvent(MouseMotionEvent evt) {}
|
||||||
if (hits(cursor, cancelBtnX, cancelBtnY, saveBtnW, saveBtnH)) {
|
@Override public void onMouseButtonEvent(MouseButtonEvent evt) {}
|
||||||
setEnabled(false);
|
@Override public void onJoyAxisEvent(JoyAxisEvent evt) {}
|
||||||
if (onClose != null) onClose.run();
|
@Override public void onJoyButtonEvent(JoyButtonEvent evt) {}
|
||||||
}
|
@Override public void onTouchEvent(TouchEvent evt) {}
|
||||||
};
|
|
||||||
|
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));
|
||||||
// Tastendruck beim Warten auf Zuweisung (RawInputListener)
|
Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
|
||||||
// -----------------------------------------------------------------------
|
mat.setColor("Color", color.clone());
|
||||||
|
if (color.a < 1f) {
|
||||||
@Override
|
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
|
||||||
public void onKeyEvent(KeyInputEvent evt) {
|
geo.setQueueBucket(RenderQueue.Bucket.Transparent);
|
||||||
if (!evt.isPressed() || waitingRow < 0) return;
|
}
|
||||||
if (evt.getKeyCode() == KeyInput.KEY_ESCAPE) return; // cancelWaiting() wird von BlightApp aufgerufen
|
geo.setMaterial(mat);
|
||||||
|
geo.setLocalTranslation(x, y, z);
|
||||||
Row r = rows.get(waitingRow);
|
parent.attachChild(geo);
|
||||||
editCopy.set(r.field, evt.getKeyCode());
|
return geo;
|
||||||
r.keyText.setText(KeyNames.of(evt.getKeyCode()));
|
}
|
||||||
resetRowColor(waitingRow);
|
|
||||||
waitingRow = -1;
|
private BitmapText text(String content, int size, ColorRGBA color) {
|
||||||
}
|
BitmapText t = new BitmapText(font);
|
||||||
|
t.setSize(size);
|
||||||
private void resetRowColor(int idx) {
|
t.setColor(color);
|
||||||
rows.get(idx).bg.getMaterial().setColor("Color", COL_ROW);
|
t.setText(content);
|
||||||
}
|
return t;
|
||||||
|
}
|
||||||
// RawInputListener-Pflichtmethoden
|
|
||||||
@Override public void beginInput() {}
|
private void centerText(BitmapText t, float x, float y, float width) {
|
||||||
@Override public void endInput() {}
|
t.setLocalTranslation(x + (width - t.getLineWidth()) / 2f, y, 1);
|
||||||
@Override public void onMouseMotionEvent(MouseMotionEvent evt) {}
|
}
|
||||||
@Override public void onMouseButtonEvent(MouseButtonEvent evt) {}
|
|
||||||
@Override public void onJoyAxisEvent(JoyAxisEvent evt) {}
|
private boolean hits(float[] v, float x, float y, float w, float h) {
|
||||||
@Override public void onJoyButtonEvent(JoyButtonEvent evt) {}
|
return v[0] >= x && v[0] <= x + w && v[1] >= y && v[1] <= y + h;
|
||||||
@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);
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,290 +1,291 @@
|
|||||||
package de.blight.game.config;
|
package de.blight.game.config;
|
||||||
|
|
||||||
import com.jme3.app.Application;
|
import com.jme3.app.Application;
|
||||||
import com.jme3.app.SimpleApplication;
|
import com.jme3.app.SimpleApplication;
|
||||||
import com.jme3.app.state.BaseAppState;
|
import com.jme3.app.state.BaseAppState;
|
||||||
import com.jme3.font.BitmapFont;
|
import com.jme3.font.BitmapFont;
|
||||||
import com.jme3.font.BitmapText;
|
import com.jme3.font.BitmapText;
|
||||||
import com.jme3.input.MouseInput;
|
import com.jme3.input.MouseInput;
|
||||||
import com.jme3.input.controls.ActionListener;
|
import com.jme3.input.controls.ActionListener;
|
||||||
import com.jme3.input.controls.MouseButtonTrigger;
|
import com.jme3.input.controls.MouseButtonTrigger;
|
||||||
import com.jme3.material.Material;
|
import com.jme3.material.Material;
|
||||||
import com.jme3.material.RenderState;
|
import com.jme3.material.RenderState;
|
||||||
import com.jme3.math.ColorRGBA;
|
import com.jme3.math.ColorRGBA;
|
||||||
import com.jme3.math.Vector2f;
|
import com.jme3.math.Vector2f;
|
||||||
import com.jme3.renderer.queue.RenderQueue;
|
import com.jme3.renderer.queue.RenderQueue;
|
||||||
import com.jme3.scene.Geometry;
|
import com.jme3.scene.Geometry;
|
||||||
import com.jme3.scene.Node;
|
import com.jme3.scene.Node;
|
||||||
import com.jme3.scene.shape.Quad;
|
import com.jme3.scene.shape.Quad;
|
||||||
import com.jme3.system.AppSettings;
|
import com.jme3.system.AppSettings;
|
||||||
|
import de.blight.lang.TextResolver;
|
||||||
public class GraphicsScreen extends BaseAppState {
|
|
||||||
|
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_TEXT = ColorRGBA.White;
|
||||||
private static final ColorRGBA COL_ROW = new ColorRGBA(0.18f, 0.18f, 0.28f, 1.00f);
|
private static final ColorRGBA COL_TEXT_VAL = new ColorRGBA(0.85f, 0.85f, 0.50f, 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 int[][] RESOLUTIONS = {
|
||||||
private static final ColorRGBA COL_BTN_CANCEL = new ColorRGBA(0.40f, 0.15f, 0.15f, 1.00f);
|
{1280, 720}, {1600, 900}, {1920, 1080}, {2560, 1440}, {3840, 2160}
|
||||||
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[] SAMPLES = {0, 2, 4, 8};
|
||||||
|
|
||||||
private static final int[][] RESOLUTIONS = {
|
private static final int ROW_RES = 0;
|
||||||
{1280, 720}, {1600, 900}, {1920, 1080}, {2560, 1440}, {3840, 2160}
|
private static final int ROW_FULL = 1;
|
||||||
};
|
private static final int ROW_VSYNC = 2;
|
||||||
private static final int[] SAMPLES = {0, 2, 4, 8};
|
private static final int ROW_AA = 3;
|
||||||
|
|
||||||
private static final int ROW_RES = 0;
|
private SimpleApplication app;
|
||||||
private static final int ROW_FULL = 1;
|
private Node guiNode;
|
||||||
private static final int ROW_VSYNC = 2;
|
private BitmapFont font;
|
||||||
private static final int ROW_AA = 3;
|
private Node panel;
|
||||||
|
private Node bgLayer;
|
||||||
private SimpleApplication app;
|
private Node canvasNode;
|
||||||
private Node guiNode;
|
|
||||||
private BitmapFont font;
|
private final GraphicsSettings live;
|
||||||
private Node panel;
|
private GraphicsSettings edit;
|
||||||
|
private final Runnable onClose;
|
||||||
private final GraphicsSettings live;
|
|
||||||
private GraphicsSettings edit;
|
private int resIdx;
|
||||||
private final Runnable onClose;
|
private int samplesIdx;
|
||||||
|
|
||||||
private int resIdx;
|
private final float[] cellX = new float[4];
|
||||||
private int samplesIdx;
|
private final float[] cellY = new float[4];
|
||||||
|
private final float[] cellW = new float[4];
|
||||||
// Per-row layout (indexed by ROW_*)
|
private final float cellH = 36;
|
||||||
private final float[] cellX = new float[4];
|
private final float arrW = 30;
|
||||||
private final float[] cellY = new float[4];
|
private final float[] leftX = new float[4];
|
||||||
private final float[] cellW = new float[4];
|
private final float[] rightX = new float[4];
|
||||||
private final float cellH = 36;
|
private final BitmapText[] valTexts = new BitmapText[4];
|
||||||
private final float arrW = 30;
|
|
||||||
private final float[] leftX = new float[4];
|
private float okX, okY, okW, okH;
|
||||||
private final float[] rightX = new float[4];
|
private float cancelX, cancelY;
|
||||||
private final BitmapText[] valTexts = new BitmapText[4];
|
|
||||||
|
public GraphicsScreen(GraphicsSettings live, Runnable onClose) {
|
||||||
private float okX, okY, okW, okH;
|
this.live = live;
|
||||||
private float cancelX, cancelY;
|
this.onClose = onClose;
|
||||||
|
}
|
||||||
public GraphicsScreen(GraphicsSettings live, Runnable onClose) {
|
|
||||||
this.live = live;
|
@Override
|
||||||
this.onClose = onClose;
|
protected void initialize(Application app) {
|
||||||
}
|
this.app = (SimpleApplication) app;
|
||||||
|
this.guiNode = this.app.getGuiNode();
|
||||||
@Override
|
this.font = app.getAssetManager().loadFont("Interface/Fonts/Default.fnt");
|
||||||
protected void initialize(Application app) {
|
}
|
||||||
this.app = (SimpleApplication) app;
|
|
||||||
this.guiNode = this.app.getGuiNode();
|
@Override
|
||||||
this.font = app.getAssetManager().loadFont("Interface/Fonts/Default.fnt");
|
protected void onEnable() {
|
||||||
}
|
edit = new GraphicsSettings();
|
||||||
|
edit.width = live.width; edit.height = live.height;
|
||||||
@Override
|
edit.fullscreen = live.fullscreen;
|
||||||
protected void onEnable() {
|
edit.vsync = live.vsync;
|
||||||
edit = new GraphicsSettings();
|
edit.samples = live.samples;
|
||||||
edit.width = live.width; edit.height = live.height;
|
|
||||||
edit.fullscreen = live.fullscreen;
|
resIdx = 0;
|
||||||
edit.vsync = live.vsync;
|
for (int i = 0; i < RESOLUTIONS.length; i++) {
|
||||||
edit.samples = live.samples;
|
if (RESOLUTIONS[i][0] == edit.width && RESOLUTIONS[i][1] == edit.height) {
|
||||||
|
resIdx = i; break;
|
||||||
resIdx = 0;
|
}
|
||||||
for (int i = 0; i < RESOLUTIONS.length; i++) {
|
}
|
||||||
if (RESOLUTIONS[i][0] == edit.width && RESOLUTIONS[i][1] == edit.height) {
|
samplesIdx = 0;
|
||||||
resIdx = i; break;
|
for (int i = 0; i < SAMPLES.length; i++) {
|
||||||
}
|
if (SAMPLES[i] == edit.samples) { samplesIdx = i; break; }
|
||||||
}
|
}
|
||||||
samplesIdx = 0;
|
|
||||||
for (int i = 0; i < SAMPLES.length; i++) {
|
buildUI();
|
||||||
if (SAMPLES[i] == edit.samples) { samplesIdx = i; break; }
|
app.getInputManager().setCursorVisible(true);
|
||||||
}
|
app.getInputManager().addMapping("_GfxClick", new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
|
||||||
|
app.getInputManager().addListener(clickListener, "_GfxClick");
|
||||||
buildUI();
|
}
|
||||||
app.getInputManager().setCursorVisible(true);
|
|
||||||
app.getInputManager().addMapping("_GfxClick", new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
|
@Override
|
||||||
app.getInputManager().addListener(clickListener, "_GfxClick");
|
protected void onDisable() {
|
||||||
}
|
if (bgLayer != null) { guiNode.detachChild(bgLayer); bgLayer = null; }
|
||||||
|
if (canvasNode != null) { guiNode.detachChild(canvasNode); canvasNode = null; }
|
||||||
@Override
|
panel = null;
|
||||||
protected void onDisable() {
|
app.getInputManager().deleteMapping("_GfxClick");
|
||||||
if (panel != null) { guiNode.detachChild(panel); panel = null; }
|
app.getInputManager().setCursorVisible(false);
|
||||||
app.getInputManager().deleteMapping("_GfxClick");
|
}
|
||||||
app.getInputManager().setCursorVisible(false);
|
|
||||||
}
|
@Override protected void cleanup(Application app) {}
|
||||||
|
|
||||||
@Override protected void cleanup(Application app) {}
|
private void buildUI() {
|
||||||
|
float sw = MenuCanvas.REF_W;
|
||||||
private void buildUI() {
|
float sh = MenuCanvas.REF_H;
|
||||||
float sw = app.getCamera().getWidth();
|
|
||||||
float sh = app.getCamera().getHeight();
|
bgLayer = MenuCanvas.createBgLayer(app.getAssetManager(), app.getCamera());
|
||||||
panel = new Node("gfx-panel");
|
canvasNode = MenuCanvas.createCanvas(app.getCamera());
|
||||||
addQuad(panel, 0, 0, sw, sh, COL_BG, -2);
|
guiNode.attachChild(bgLayer);
|
||||||
|
guiNode.attachChild(canvasNode);
|
||||||
float pw = 640, ph = 400;
|
|
||||||
float px = (sw - pw) / 2f, py = (sh - ph) / 2f;
|
panel = new Node("gfx-panel");
|
||||||
addQuad(panel, px, py, pw, ph, COL_PANEL, -1);
|
|
||||||
|
float pw = 640, ph = 400;
|
||||||
BitmapText title = txt("GRAFIKEINSTELLUNGEN", 20, COL_TEXT);
|
float px = (sw - pw) / 2f, py = (sh - ph) / 2f;
|
||||||
centerText(title, px, py + ph - 42, pw);
|
panel.attachChild(NinePatch.panel(app.getAssetManager()).build(px, py, pw, ph, -1));
|
||||||
panel.attachChild(title);
|
|
||||||
|
BitmapText title = txt(t("menu.graphics.title"), 20, COL_TEXT);
|
||||||
String[] labels = {"Auflösung", "Vollbild", "VSync", "Kantenglättung"};
|
centerText(title, px, py + ph - 42, pw);
|
||||||
float lblX = px + 30;
|
panel.attachChild(title);
|
||||||
float vx = px + pw - 270;
|
|
||||||
float vw = 190;
|
String[] labelKeys = {
|
||||||
float startY = py + ph - 100;
|
"menu.graphics.row.resolution",
|
||||||
float step = 60;
|
"menu.graphics.row.fullscreen",
|
||||||
|
"menu.graphics.row.vsync",
|
||||||
for (int i = 0; i < 4; i++) {
|
"menu.graphics.row.aa"
|
||||||
float ry = startY - i * step;
|
};
|
||||||
|
float lblX = px + 30;
|
||||||
BitmapText lbl = txt(labels[i], 16, COL_TEXT);
|
float vx = px + pw - 270;
|
||||||
lbl.setLocalTranslation(lblX, ry + cellH - 8, 0);
|
float vw = 190;
|
||||||
panel.attachChild(lbl);
|
float startY = py + ph - 100;
|
||||||
|
float step = 60;
|
||||||
// Left arrow
|
|
||||||
addQuad(panel, vx - arrW - 6, ry, arrW, cellH, COL_ARROW, 0);
|
for (int i = 0; i < 4; i++) {
|
||||||
BitmapText lt = txt("<", 16, COL_TEXT);
|
float ry = startY - i * step;
|
||||||
lt.setLocalTranslation(vx - arrW - 6 + (arrW - lt.getLineWidth()) / 2f, ry + cellH - 8, 1);
|
|
||||||
panel.attachChild(lt);
|
BitmapText lbl = txt(t(labelKeys[i]), 16, COL_TEXT);
|
||||||
|
lbl.setLocalTranslation(lblX, ry + cellH - 8, 0);
|
||||||
// Value cell
|
panel.attachChild(lbl);
|
||||||
addQuad(panel, vx, ry, vw, cellH, COL_ROW, 0);
|
|
||||||
|
panel.attachChild(NinePatch.buttonArrow(app.getAssetManager()).build(vx - arrW - 6, ry, arrW, cellH, 0));
|
||||||
// Right arrow
|
BitmapText lt = txt("<", 16, COL_TEXT);
|
||||||
addQuad(panel, vx + vw + 6, ry, arrW, cellH, COL_ARROW, 0);
|
lt.setLocalTranslation(vx - arrW - 6 + (arrW - lt.getLineWidth()) / 2f, ry + cellH - 8, 1);
|
||||||
BitmapText rt = txt(">", 16, COL_TEXT);
|
panel.attachChild(lt);
|
||||||
rt.setLocalTranslation(vx + vw + 6 + (arrW - rt.getLineWidth()) / 2f, ry + cellH - 8, 1);
|
|
||||||
panel.attachChild(rt);
|
panel.attachChild(NinePatch.button(app.getAssetManager()).build(vx, ry, vw, cellH, 0));
|
||||||
|
|
||||||
BitmapText vt = txt("", 16, COL_TEXT_VAL);
|
panel.attachChild(NinePatch.buttonArrow(app.getAssetManager()).build(vx + vw + 6, ry, arrW, cellH, 0));
|
||||||
panel.attachChild(vt);
|
BitmapText rt = txt(">", 16, COL_TEXT);
|
||||||
valTexts[i] = vt;
|
rt.setLocalTranslation(vx + vw + 6 + (arrW - rt.getLineWidth()) / 2f, ry + cellH - 8, 1);
|
||||||
|
panel.attachChild(rt);
|
||||||
cellX[i] = vx; cellY[i] = ry; cellW[i] = vw;
|
|
||||||
leftX[i] = vx - arrW - 6;
|
BitmapText vt = txt("", 16, COL_TEXT_VAL);
|
||||||
rightX[i] = vx + vw + 6;
|
panel.attachChild(vt);
|
||||||
}
|
valTexts[i] = vt;
|
||||||
|
|
||||||
for (int i = 0; i < 4; i++) refreshText(i);
|
cellX[i] = vx; cellY[i] = ry; cellW[i] = vw;
|
||||||
|
leftX[i] = vx - arrW - 6;
|
||||||
float bw = 160, bh = 42;
|
rightX[i] = vx + vw + 6;
|
||||||
okW = bw; okH = bh;
|
}
|
||||||
okX = px + pw / 2f - bw - 10;
|
|
||||||
okY = py + 22;
|
for (int i = 0; i < 4; i++) refreshText(i);
|
||||||
cancelX = px + pw / 2f + 10;
|
|
||||||
cancelY = py + 22;
|
float bw = 160, bh = 42;
|
||||||
|
okW = bw; okH = bh;
|
||||||
addQuad(panel, okX, okY, bw, bh, COL_BTN_OK, 0);
|
okX = px + pw / 2f - bw - 10;
|
||||||
BitmapText okLbl = txt("Übernehmen", 16, COL_TEXT);
|
okY = py + 22;
|
||||||
centerText(okLbl, okX, okY + bh - 10, bw);
|
cancelX = px + pw / 2f + 10;
|
||||||
panel.attachChild(okLbl);
|
cancelY = py + 22;
|
||||||
|
|
||||||
addQuad(panel, cancelX, cancelY, bw, bh, COL_BTN_CANCEL, 0);
|
panel.attachChild(NinePatch.buttonSave(app.getAssetManager()).build(okX, okY, bw, bh, 0));
|
||||||
BitmapText cancelLbl = txt("Abbrechen", 16, COL_TEXT);
|
BitmapText okLbl = txt(t("menu.graphics.btn.apply"), 16, COL_TEXT);
|
||||||
centerText(cancelLbl, cancelX, cancelY + bh - 10, bw);
|
centerText(okLbl, okX, okY + bh - 10, bw);
|
||||||
panel.attachChild(cancelLbl);
|
panel.attachChild(okLbl);
|
||||||
|
|
||||||
guiNode.attachChild(panel);
|
panel.attachChild(NinePatch.buttonQuit(app.getAssetManager()).build(cancelX, cancelY, bw, bh, 0));
|
||||||
}
|
BitmapText cancelLbl = txt(t("menu.graphics.btn.cancel"), 16, COL_TEXT);
|
||||||
|
centerText(cancelLbl, cancelX, cancelY + bh - 10, bw);
|
||||||
private void refreshText(int row) {
|
panel.attachChild(cancelLbl);
|
||||||
String val = switch (row) {
|
|
||||||
case ROW_RES -> RESOLUTIONS[resIdx][0] + "x" + RESOLUTIONS[resIdx][1];
|
canvasNode.attachChild(panel);
|
||||||
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";
|
private void refreshText(int row) {
|
||||||
default -> "";
|
String val = switch (row) {
|
||||||
};
|
case ROW_RES -> RESOLUTIONS[resIdx][0] + "x" + RESOLUTIONS[resIdx][1];
|
||||||
BitmapText vt = valTexts[row];
|
case ROW_FULL -> edit.fullscreen ? t("menu.graphics.val.on") : t("menu.graphics.val.off");
|
||||||
vt.setText(val);
|
case ROW_VSYNC -> edit.vsync ? t("menu.graphics.val.on") : t("menu.graphics.val.off");
|
||||||
vt.setLocalTranslation(
|
case ROW_AA -> SAMPLES[samplesIdx] == 0
|
||||||
cellX[row] + (cellW[row] - vt.getLineWidth()) / 2f,
|
? t("menu.graphics.val.off")
|
||||||
cellY[row] + cellH - 8,
|
: SAMPLES[samplesIdx] + "x MSAA";
|
||||||
1
|
default -> "";
|
||||||
);
|
};
|
||||||
}
|
BitmapText vt = valTexts[row];
|
||||||
|
vt.setText(val);
|
||||||
private final ActionListener clickListener = (name, isPressed, tpf) -> {
|
vt.setLocalTranslation(
|
||||||
if (!isPressed) return;
|
cellX[row] + (cellW[row] - vt.getLineWidth()) / 2f,
|
||||||
Vector2f c = app.getInputManager().getCursorPosition();
|
cellY[row] + cellH - 8,
|
||||||
|
1
|
||||||
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; }
|
|
||||||
}
|
private final ActionListener clickListener = (name, isPressed, tpf) -> {
|
||||||
if (hits(c, okX, okY, okW, okH)) { applyAndSave(); return; }
|
if (!isPressed) return;
|
||||||
if (hits(c, cancelX, cancelY, okW, okH)) { close(); }
|
float[] v = toVirtual(app.getInputManager().getCursorPosition());
|
||||||
};
|
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
private void cycleRow(int row, int dir) {
|
if (hits(v, leftX[i], cellY[i], arrW, cellH)) { cycleRow(i, -1); return; }
|
||||||
switch (row) {
|
if (hits(v, rightX[i], cellY[i], arrW, cellH)) { cycleRow(i, +1); return; }
|
||||||
case ROW_RES:
|
}
|
||||||
resIdx = (resIdx + dir + RESOLUTIONS.length) % RESOLUTIONS.length;
|
if (hits(v, okX, okY, okW, okH)) { applyAndSave(); return; }
|
||||||
edit.width = RESOLUTIONS[resIdx][0];
|
if (hits(v, cancelX, cancelY, okW, okH)) { close(); }
|
||||||
edit.height = RESOLUTIONS[resIdx][1];
|
};
|
||||||
break;
|
|
||||||
case ROW_FULL:
|
private void cycleRow(int row, int dir) {
|
||||||
edit.fullscreen = !edit.fullscreen;
|
switch (row) {
|
||||||
break;
|
case ROW_RES:
|
||||||
case ROW_VSYNC:
|
resIdx = (resIdx + dir + RESOLUTIONS.length) % RESOLUTIONS.length;
|
||||||
edit.vsync = !edit.vsync;
|
edit.width = RESOLUTIONS[resIdx][0];
|
||||||
break;
|
edit.height = RESOLUTIONS[resIdx][1];
|
||||||
case ROW_AA:
|
break;
|
||||||
samplesIdx = (samplesIdx + dir + SAMPLES.length) % SAMPLES.length;
|
case ROW_FULL:
|
||||||
edit.samples = SAMPLES[samplesIdx];
|
edit.fullscreen = !edit.fullscreen;
|
||||||
break;
|
break;
|
||||||
}
|
case ROW_VSYNC:
|
||||||
refreshText(row);
|
edit.vsync = !edit.vsync;
|
||||||
}
|
break;
|
||||||
|
case ROW_AA:
|
||||||
private void applyAndSave() {
|
samplesIdx = (samplesIdx + dir + SAMPLES.length) % SAMPLES.length;
|
||||||
live.width = edit.width; live.height = edit.height;
|
edit.samples = SAMPLES[samplesIdx];
|
||||||
live.fullscreen = edit.fullscreen;
|
break;
|
||||||
live.vsync = edit.vsync;
|
}
|
||||||
live.samples = edit.samples;
|
refreshText(row);
|
||||||
|
}
|
||||||
GraphicsStore.save(live);
|
|
||||||
|
private void applyAndSave() {
|
||||||
AppSettings s = app.getContext().getSettings();
|
live.width = edit.width; live.height = edit.height;
|
||||||
s.setResolution(live.width, live.height);
|
live.fullscreen = edit.fullscreen;
|
||||||
s.setFullscreen(live.fullscreen);
|
live.vsync = edit.vsync;
|
||||||
s.setVSync(live.vsync);
|
live.samples = edit.samples;
|
||||||
s.setSamples(live.samples);
|
|
||||||
app.setSettings(s);
|
GraphicsStore.save(live);
|
||||||
|
|
||||||
close();
|
AppSettings s = app.getContext().getSettings();
|
||||||
app.restart();
|
s.setResolution(live.width, live.height);
|
||||||
}
|
s.setFullscreen(live.fullscreen);
|
||||||
|
s.setVSync(live.vsync);
|
||||||
private void close() {
|
s.setSamples(live.samples);
|
||||||
setEnabled(false);
|
app.setSettings(s);
|
||||||
if (onClose != null) onClose.run();
|
|
||||||
}
|
close();
|
||||||
|
app.restart();
|
||||||
// -----------------------------------------------------------------------
|
}
|
||||||
|
|
||||||
private Geometry addQuad(Node parent, float x, float y, float w, float h, ColorRGBA color, float z) {
|
private void close() {
|
||||||
Geometry geo = new Geometry("q", new Quad(w, h));
|
setEnabled(false);
|
||||||
Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
|
if (onClose != null) onClose.run();
|
||||||
mat.setColor("Color", color.clone());
|
}
|
||||||
if (color.a < 1f) {
|
|
||||||
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
|
private float[] toVirtual(Vector2f screen) {
|
||||||
geo.setQueueBucket(RenderQueue.Bucket.Transparent);
|
float scale = Math.min(app.getCamera().getWidth() / MenuCanvas.REF_W,
|
||||||
}
|
app.getCamera().getHeight() / MenuCanvas.REF_H);
|
||||||
geo.setMaterial(mat);
|
float ox = (app.getCamera().getWidth() - MenuCanvas.REF_W * scale) / 2f;
|
||||||
geo.setLocalTranslation(x, y, z);
|
float oy = (app.getCamera().getHeight() - MenuCanvas.REF_H * scale) / 2f;
|
||||||
parent.attachChild(geo);
|
return new float[]{ (screen.x - ox) / scale, (screen.y - oy) / scale };
|
||||||
return geo;
|
}
|
||||||
}
|
|
||||||
|
private static String t(String id) { return TextResolver.get().resolveId(id); }
|
||||||
private BitmapText txt(String s, int size, ColorRGBA color) {
|
|
||||||
BitmapText t = new BitmapText(font);
|
private BitmapText txt(String s, int size, ColorRGBA color) {
|
||||||
t.setSize(size); t.setColor(color); t.setText(s);
|
BitmapText t = new BitmapText(font);
|
||||||
return t;
|
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 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;
|
private boolean hits(float[] v, float x, float y, float w, float h) {
|
||||||
}
|
return v[0] >= x && v[0] <= x + w && v[1] >= y && v[1] <= y + h;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,174 +1,193 @@
|
|||||||
package de.blight.game.config;
|
package de.blight.game.config;
|
||||||
|
|
||||||
import com.jme3.app.Application;
|
import com.jme3.app.Application;
|
||||||
import com.jme3.app.SimpleApplication;
|
import com.jme3.app.SimpleApplication;
|
||||||
import com.jme3.app.state.BaseAppState;
|
import com.jme3.app.state.BaseAppState;
|
||||||
import com.jme3.font.BitmapFont;
|
import com.jme3.font.BitmapFont;
|
||||||
import com.jme3.font.BitmapText;
|
import com.jme3.font.BitmapText;
|
||||||
import com.jme3.input.MouseInput;
|
import com.jme3.input.MouseInput;
|
||||||
import com.jme3.input.controls.ActionListener;
|
import com.jme3.input.controls.ActionListener;
|
||||||
import com.jme3.input.controls.MouseButtonTrigger;
|
import com.jme3.input.controls.MouseButtonTrigger;
|
||||||
import com.jme3.material.Material;
|
import com.jme3.material.Material;
|
||||||
import com.jme3.material.RenderState;
|
import com.jme3.material.RenderState;
|
||||||
import com.jme3.math.ColorRGBA;
|
import com.jme3.math.ColorRGBA;
|
||||||
import com.jme3.math.Vector2f;
|
import com.jme3.math.Vector2f;
|
||||||
import com.jme3.renderer.queue.RenderQueue;
|
import com.jme3.post.FilterPostProcessor;
|
||||||
import com.jme3.scene.Geometry;
|
import com.jme3.renderer.queue.RenderQueue;
|
||||||
import com.jme3.scene.Node;
|
import com.jme3.scene.Geometry;
|
||||||
import com.jme3.scene.shape.Quad;
|
import com.jme3.scene.Node;
|
||||||
|
import com.jme3.scene.shape.Quad;
|
||||||
public class PauseMenu extends BaseAppState {
|
import de.blight.game.post.GaussianBlurFilter;
|
||||||
|
import de.blight.game.scene.WorldScene;
|
||||||
private static final ColorRGBA COL_BG = new ColorRGBA(0.05f, 0.05f, 0.08f, 0.88f);
|
import de.blight.lang.TextResolver;
|
||||||
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);
|
public class PauseMenu extends BaseAppState {
|
||||||
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 = ColorRGBA.White;
|
|
||||||
private static final ColorRGBA COL_TEXT_DIS = new ColorRGBA(0.40f, 0.40f, 0.40f, 1.00f);
|
private static final int BTN_GRAFIK = 0;
|
||||||
private static final ColorRGBA COL_TEXT_SUB = new ColorRGBA(0.35f, 0.35f, 0.35f, 1.00f);
|
private static final int BTN_AUDIO = 1;
|
||||||
|
private static final int BTN_STEUERUNG = 2;
|
||||||
private static final int BTN_GRAFIK = 0;
|
private static final int BTN_SPEICHERN = 3;
|
||||||
private static final int BTN_AUDIO = 1;
|
private static final int BTN_BEENDEN = 4;
|
||||||
private static final int BTN_STEUERUNG = 2;
|
|
||||||
private static final int BTN_SPEICHERN = 3;
|
private SimpleApplication app;
|
||||||
private static final int BTN_BEENDEN = 4;
|
private Node guiNode;
|
||||||
|
private BitmapFont font;
|
||||||
private static final ColorRGBA COL_BTN_SAVE = new ColorRGBA(0.12f, 0.28f, 0.14f, 1.00f);
|
private Node panel;
|
||||||
|
private Node bgLayer;
|
||||||
private SimpleApplication app;
|
private Node canvasNode;
|
||||||
private Node guiNode;
|
|
||||||
private BitmapFont font;
|
private Runnable onSave;
|
||||||
private Node panel;
|
private Runnable onGraphics;
|
||||||
|
private Runnable onAudio;
|
||||||
private Runnable onGraphics;
|
private Runnable onControls;
|
||||||
private Runnable onControls;
|
|
||||||
private Runnable onSave;
|
private GaussianBlurFilter blurFilter;
|
||||||
|
|
||||||
// [x, y, w, h] per button
|
private final float[][] btnBounds = new float[5][4];
|
||||||
private final float[][] btnBounds = new float[5][4];
|
|
||||||
|
public PauseMenu(Runnable onSave, Runnable onGraphics, Runnable onAudio, Runnable onControls) {
|
||||||
public PauseMenu(Runnable onSave, Runnable onGraphics, Runnable onControls) {
|
this.onSave = onSave;
|
||||||
this.onSave = onSave;
|
this.onGraphics = onGraphics;
|
||||||
this.onGraphics = onGraphics;
|
this.onAudio = onAudio;
|
||||||
this.onControls = onControls;
|
this.onControls = onControls;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void initialize(Application app) {
|
protected void initialize(Application app) {
|
||||||
this.app = (SimpleApplication) app;
|
this.app = (SimpleApplication) app;
|
||||||
this.guiNode = this.app.getGuiNode();
|
this.guiNode = this.app.getGuiNode();
|
||||||
this.font = app.getAssetManager().loadFont("Interface/Fonts/Default.fnt");
|
this.font = app.getAssetManager().loadFont("Interface/Fonts/Default.fnt");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onEnable() {
|
protected void onEnable() {
|
||||||
buildUI();
|
buildUI();
|
||||||
app.getInputManager().setCursorVisible(true);
|
app.getInputManager().setCursorVisible(true);
|
||||||
app.getInputManager().addMapping("_PauseClick", new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
|
app.getInputManager().addMapping("_PauseClick", new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
|
||||||
app.getInputManager().addListener(clickListener, "_PauseClick");
|
app.getInputManager().addListener(clickListener, "_PauseClick");
|
||||||
}
|
addBlur();
|
||||||
|
}
|
||||||
@Override
|
|
||||||
protected void onDisable() {
|
@Override
|
||||||
if (panel != null) { guiNode.detachChild(panel); panel = null; }
|
protected void onDisable() {
|
||||||
app.getInputManager().deleteMapping("_PauseClick");
|
removeBlur();
|
||||||
app.getInputManager().setCursorVisible(false);
|
if (bgLayer != null) { guiNode.detachChild(bgLayer); bgLayer = null; }
|
||||||
}
|
if (canvasNode != null) { guiNode.detachChild(canvasNode); canvasNode = null; }
|
||||||
|
panel = null;
|
||||||
@Override protected void cleanup(Application app) {}
|
app.getInputManager().deleteMapping("_PauseClick");
|
||||||
|
app.getInputManager().setCursorVisible(false);
|
||||||
private void buildUI() {
|
}
|
||||||
float sw = app.getCamera().getWidth();
|
|
||||||
float sh = app.getCamera().getHeight();
|
private void addBlur() {
|
||||||
panel = new Node("pause-panel");
|
WorldScene ws = getApplication().getStateManager().getState(WorldScene.class);
|
||||||
addQuad(panel, 0, 0, sw, sh, COL_BG, -2);
|
if (ws == null) return;
|
||||||
|
FilterPostProcessor fpp = ws.getSharedFPP();
|
||||||
float pw = 320, ph = 430;
|
if (fpp == null) return;
|
||||||
float px = (sw - pw) / 2f, py = (sh - ph) / 2f;
|
blurFilter = new GaussianBlurFilter(6f);
|
||||||
addQuad(panel, px, py, pw, ph, COL_PANEL, -1);
|
fpp.addFilter(blurFilter);
|
||||||
|
}
|
||||||
BitmapText title = txt("PAUSE", 26, COL_TEXT);
|
|
||||||
centerText(title, px, py + ph - 48, pw);
|
private void removeBlur() {
|
||||||
panel.attachChild(title);
|
if (blurFilter == null) return;
|
||||||
|
WorldScene ws = getApplication().getStateManager().getState(WorldScene.class);
|
||||||
String[] labels = {"Grafik", "Audio", "Steuerung", "Speichern", "Beenden"};
|
if (ws != null) {
|
||||||
boolean[] enabled = {true, false, true, true, true};
|
FilterPostProcessor fpp = ws.getSharedFPP();
|
||||||
ColorRGBA[] bgCols = {COL_BTN, COL_BTN_DIS, COL_BTN, COL_BTN_SAVE, COL_BTN_QUIT};
|
if (fpp != null) fpp.removeFilter(blurFilter);
|
||||||
|
}
|
||||||
float bw = 260, bh = 52;
|
blurFilter = null;
|
||||||
float bx = px + (pw - bw) / 2f;
|
}
|
||||||
float startY = py + ph - 112;
|
|
||||||
float step = 62;
|
@Override protected void cleanup(Application app) {}
|
||||||
|
|
||||||
for (int i = 0; i < 5; i++) {
|
private void buildUI() {
|
||||||
float by = startY - i * step;
|
float sw = MenuCanvas.REF_W;
|
||||||
ColorRGBA txCol = enabled[i] ? COL_TEXT : COL_TEXT_DIS;
|
float sh = MenuCanvas.REF_H;
|
||||||
|
|
||||||
addQuad(panel, bx, by, bw, bh, bgCols[i], 0);
|
bgLayer = MenuCanvas.createPauseBgLayer(app.getAssetManager(), app.getCamera());
|
||||||
|
canvasNode = MenuCanvas.createFixedCanvas(app.getCamera());
|
||||||
BitmapText lbl = txt(labels[i], 18, txCol);
|
guiNode.attachChild(bgLayer);
|
||||||
if (!enabled[i]) {
|
guiNode.attachChild(canvasNode);
|
||||||
lbl.setLocalTranslation(bx + (bw - lbl.getLineWidth()) / 2f, by + bh - 12, 1);
|
|
||||||
BitmapText hint = txt("Bald verfügbar", 12, COL_TEXT_SUB);
|
panel = new Node("pause-panel");
|
||||||
hint.setLocalTranslation(bx + (bw - hint.getLineWidth()) / 2f, by + 14, 1);
|
|
||||||
panel.attachChild(hint);
|
float pw = 320, ph = 430;
|
||||||
} else {
|
float px = (sw - pw) / 2f, py = (sh - ph) / 2f;
|
||||||
centerText(lbl, bx, by + bh - 16, bw);
|
panel.attachChild(NinePatch.panel(app.getAssetManager()).build(px, py, pw, ph, -1));
|
||||||
}
|
|
||||||
panel.attachChild(lbl);
|
BitmapText title = txt(t("menu.pause.title"), 26, COL_TEXT);
|
||||||
|
centerText(title, px, py + ph - 48, pw);
|
||||||
btnBounds[i][0] = bx; btnBounds[i][1] = by;
|
panel.attachChild(title);
|
||||||
btnBounds[i][2] = bw; btnBounds[i][3] = bh;
|
|
||||||
}
|
String[] labelKeys = {
|
||||||
|
"menu.pause.btn.graphics",
|
||||||
guiNode.attachChild(panel);
|
"menu.pause.btn.audio",
|
||||||
}
|
"menu.pause.btn.controls",
|
||||||
|
"menu.pause.btn.save",
|
||||||
private final ActionListener clickListener = (name, isPressed, tpf) -> {
|
"menu.pause.btn.quit"
|
||||||
if (!isPressed) return;
|
};
|
||||||
Vector2f c = app.getInputManager().getCursorPosition();
|
|
||||||
|
float bw = 260, bh = 52;
|
||||||
for (int i = 0; i < 5; i++) {
|
float bx = px + (pw - bw) / 2f;
|
||||||
if (!hits(c, btnBounds[i][0], btnBounds[i][1], btnBounds[i][2], btnBounds[i][3])) continue;
|
float startY = py + ph - 112;
|
||||||
switch (i) {
|
float step = 62;
|
||||||
case BTN_GRAFIK -> { if (onGraphics != null) onGraphics.run(); }
|
|
||||||
case BTN_AUDIO -> { /* Bald verfügbar */ }
|
for (int i = 0; i < 5; i++) {
|
||||||
case BTN_STEUERUNG -> { if (onControls != null) onControls.run(); }
|
float by = startY - i * step;
|
||||||
case BTN_SPEICHERN -> { if (onSave != null) onSave.run(); }
|
NinePatch btnPatch = switch (i) {
|
||||||
case BTN_BEENDEN -> app.stop();
|
case 3 -> NinePatch.buttonSave(app.getAssetManager());
|
||||||
}
|
case 4 -> NinePatch.buttonQuit(app.getAssetManager());
|
||||||
return;
|
default -> NinePatch.button(app.getAssetManager());
|
||||||
}
|
};
|
||||||
};
|
panel.attachChild(btnPatch.build(bx, by, bw, bh, 0));
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
BitmapText lbl = txt(t(labelKeys[i]), 18, COL_TEXT);
|
||||||
|
centerText(lbl, bx, by + bh - 16, bw);
|
||||||
private Geometry addQuad(Node parent, float x, float y, float w, float h, ColorRGBA color, float z) {
|
panel.attachChild(lbl);
|
||||||
Geometry geo = new Geometry("q", new Quad(w, h));
|
|
||||||
Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
|
btnBounds[i][0] = bx; btnBounds[i][1] = by;
|
||||||
mat.setColor("Color", color.clone());
|
btnBounds[i][2] = bw; btnBounds[i][3] = bh;
|
||||||
if (color.a < 1f) {
|
}
|
||||||
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
|
|
||||||
geo.setQueueBucket(RenderQueue.Bucket.Transparent);
|
canvasNode.attachChild(panel);
|
||||||
}
|
}
|
||||||
geo.setMaterial(mat);
|
|
||||||
geo.setLocalTranslation(x, y, z);
|
private final ActionListener clickListener = (name, isPressed, tpf) -> {
|
||||||
parent.attachChild(geo);
|
if (!isPressed) return;
|
||||||
return geo;
|
Vector2f c = app.getInputManager().getCursorPosition();
|
||||||
}
|
// Kein Scaling – einfach den Zentrumversatz abziehen
|
||||||
|
float ox = (app.getCamera().getWidth() - MenuCanvas.REF_W) / 2f;
|
||||||
private BitmapText txt(String s, int size, ColorRGBA color) {
|
float oy = (app.getCamera().getHeight() - MenuCanvas.REF_H) / 2f;
|
||||||
BitmapText t = new BitmapText(font);
|
float vx = c.x - ox;
|
||||||
t.setSize(size); t.setColor(color); t.setText(s);
|
float vy = c.y - oy;
|
||||||
return t;
|
|
||||||
}
|
for (int i = 0; i < 5; i++) {
|
||||||
|
if (!hits(vx, vy, btnBounds[i][0], btnBounds[i][1], btnBounds[i][2], btnBounds[i][3])) continue;
|
||||||
private void centerText(BitmapText t, float x, float y, float width) {
|
switch (i) {
|
||||||
t.setLocalTranslation(x + (width - t.getLineWidth()) / 2f, y, 1);
|
case BTN_GRAFIK -> { if (onGraphics != null) onGraphics.run(); }
|
||||||
}
|
case BTN_AUDIO -> { if (onAudio != null) onAudio.run(); }
|
||||||
|
case BTN_STEUERUNG -> { if (onControls != null) onControls.run(); }
|
||||||
private boolean hits(Vector2f p, float x, float y, float w, float h) {
|
case BTN_SPEICHERN -> { if (onSave != null) onSave.run(); }
|
||||||
return p.x >= x && p.x <= x + w && p.y >= y && p.y <= y + h;
|
case BTN_BEENDEN -> app.stop();
|
||||||
}
|
}
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private static String t(String id) { return TextResolver.get().resolveId(id); }
|
||||||
|
|
||||||
|
private BitmapText txt(String s, int size, ColorRGBA color) {
|
||||||
|
BitmapText t = new BitmapText(font);
|
||||||
|
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(float px, float py, float x, float y, float w, float h) {
|
||||||
|
return px >= x && px <= x + w && py >= y && py <= y + h;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -56,6 +56,19 @@ public class PlayerInputControl {
|
|||||||
private String runningClip;
|
private String runningClip;
|
||||||
|
|
||||||
private com.jme3.anim.SkinningControl skinningControl = null;
|
private com.jme3.anim.SkinningControl skinningControl = null;
|
||||||
|
private com.jme3.anim.Armature armature = null;
|
||||||
|
|
||||||
|
// ── Fußgeräusche ─────────────────────────────────────────────────────────
|
||||||
|
// Bone-Namen nach Phase-0-Bone-Log eintragen:
|
||||||
|
private static final String BONE_LEFT_FOOT = "mixamorig:LeftFoot";
|
||||||
|
private static final String BONE_RIGHT_FOOT = "mixamorig:RightFoot";
|
||||||
|
// Y-Schwelle im Model-Space: Fuß gilt als am Boden wenn Y < FOOT_GROUND_Y
|
||||||
|
private static final float FOOT_GROUND_Y = 0.05f;
|
||||||
|
|
||||||
|
private de.blight.game.audio.FootstepSystem footstepSystem;
|
||||||
|
private java.util.function.BiFunction<Float, Float, float[]> surfaceQuery;
|
||||||
|
private float lastLeftFootY = Float.MAX_VALUE;
|
||||||
|
private float lastRightFootY = Float.MAX_VALUE;
|
||||||
|
|
||||||
// ── Anim-Offsets ─────────────────────────────────────────────────────────
|
// ── Anim-Offsets ─────────────────────────────────────────────────────────
|
||||||
private Map<String, AnimOffset> animOffsets = new LinkedHashMap<>();
|
private Map<String, AnimOffset> animOffsets = new LinkedHashMap<>();
|
||||||
@@ -141,6 +154,10 @@ public class PlayerInputControl {
|
|||||||
log.info("[AnimCtx] AnimComposer gefunden: {}", animComposer != null);
|
log.info("[AnimCtx] AnimComposer gefunden: {}", animComposer != null);
|
||||||
skinningControl = findSkinningControl(visual);
|
skinningControl = findSkinningControl(visual);
|
||||||
log.info("[AnimCtx] SkinningControl gefunden: {}", skinningControl != null);
|
log.info("[AnimCtx] SkinningControl gefunden: {}", skinningControl != null);
|
||||||
|
if (skinningControl != null) {
|
||||||
|
armature = skinningControl.getArmature();
|
||||||
|
logJointNames(armature);
|
||||||
|
}
|
||||||
if (visual != null) {
|
if (visual != null) {
|
||||||
visualBaseTranslation = visual.getLocalTranslation().clone();
|
visualBaseTranslation = visual.getLocalTranslation().clone();
|
||||||
}
|
}
|
||||||
@@ -281,6 +298,76 @@ public class PlayerInputControl {
|
|||||||
|
|
||||||
public boolean isLockedInPlace() { return lockedInPlace; }
|
public boolean isLockedInPlace() { return lockedInPlace; }
|
||||||
|
|
||||||
|
// ── Neues-Spiel-Intro ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private boolean inputsBlocked = false;
|
||||||
|
private com.jme3.anim.tween.action.Action frozenReviveAction = null;
|
||||||
|
|
||||||
|
/** Setzt Blickrichtung (Yaw in Grad, 0=+Z, 90=+X, Uhrzeigersinn von oben). */
|
||||||
|
public void setInitialFacing(float yawDegrees) {
|
||||||
|
if (visual == null) return;
|
||||||
|
float rad = -yawDegrees * com.jme3.math.FastMath.DEG_TO_RAD;
|
||||||
|
visual.setLocalRotation(new Quaternion().fromAngles(0f, rad, 0f));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Blockiert alle Eingaben für das Neues-Spiel-Intro (ohne Animation zu wechseln).
|
||||||
|
* Wird in NewGameIntroState.initialize() aufgerufen, damit der Spieler
|
||||||
|
* während der BLACK-Phase nicht steuern kann.
|
||||||
|
*/
|
||||||
|
public void blockForIntro() {
|
||||||
|
lockInPlace();
|
||||||
|
inputsBlocked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setzt die REVIVE-Animation und friert sie bei Frame 0 ein (speed=0).
|
||||||
|
* Wird unmittelbar vor dem Einblenden aufgerufen (BLACK→FADING_IN).
|
||||||
|
*/
|
||||||
|
public void startFrozenRevive(String reviveClip) {
|
||||||
|
lockInPlace();
|
||||||
|
inputsBlocked = true;
|
||||||
|
if (animComposer != null) {
|
||||||
|
if (animLib != null && visual != null) animLib.ensureApplied(reviveClip, visual);
|
||||||
|
// transitionLength=0: verhindert den 0.4 s Überblend-Effekt bei speed=0.
|
||||||
|
// Ohne diesen Fix wäre transitionWeight=time/0.4=0 → Charakter zeigt Idle-Pose.
|
||||||
|
com.jme3.anim.tween.action.Action reviveAction = animComposer.action(reviveClip);
|
||||||
|
if (reviveAction instanceof com.jme3.anim.tween.action.BlendableAction ba) {
|
||||||
|
ba.setTransitionLength(0.0);
|
||||||
|
}
|
||||||
|
frozenReviveAction = animComposer.setCurrentAction(reviveClip);
|
||||||
|
if (frozenReviveAction != null) {
|
||||||
|
frozenReviveAction.setSpeed(0f);
|
||||||
|
log.info("[Intro] REVIVE '{}' eingefroren (frame 0)", reviveClip);
|
||||||
|
} else {
|
||||||
|
log.warn("[Intro] setCurrentAction('{}') → null – REVIVE nicht eingefroren", reviveClip);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.warn("[Intro] animComposer null – REVIVE kann nicht eingefroren werden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gibt die REVIVE-Animation mit normaler Geschwindigkeit frei. */
|
||||||
|
public void unfreezeRevive() {
|
||||||
|
if (frozenReviveAction != null) {
|
||||||
|
frozenReviveAction.setSpeed(1f);
|
||||||
|
frozenReviveAction = null;
|
||||||
|
log.info("[Intro] REVIVE freigegeben – Animation läuft");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Hebt die Input-Blockade auf und gibt Bewegungseingaben wieder frei. */
|
||||||
|
public void unblockInputs() {
|
||||||
|
inputsBlocked = false;
|
||||||
|
unlockFromPlace();
|
||||||
|
currentAnim = null; // erzwingt Neubewertung in update() (z.B. IDLE nach REVIVE)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Liefert die Clipdauer der REVIVE-Animation in Sekunden, oder 3 s als Fallback. */
|
||||||
|
public float getReviveClipLength() {
|
||||||
|
return resolveClipLength(AnimationAction.REVIVE, 3f);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Startet die Navigation zum angegebenen Welt-Punkt.
|
* Startet die Navigation zum angegebenen Welt-Punkt.
|
||||||
* Während der Navigation werden WASD-Eingaben ignoriert.
|
* Während der Navigation werden WASD-Eingaben ignoriert.
|
||||||
@@ -363,6 +450,8 @@ public class PlayerInputControl {
|
|||||||
visual.setLocalTranslation(visualBaseTranslation.clone().addLocal(animOffsetCurrent));
|
visual.setLocalTranslation(visualBaseTranslation.clone().addLocal(animOffsetCurrent));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (inputsBlocked) return;
|
||||||
|
|
||||||
if (paused) {
|
if (paused) {
|
||||||
if (autopilotDir != null) {
|
if (autopilotDir != null) {
|
||||||
autopilotDir = null;
|
autopilotDir = null;
|
||||||
@@ -502,6 +591,8 @@ public class PlayerInputControl {
|
|||||||
playAction(target);
|
playAction(target);
|
||||||
currentAnim = target;
|
currentAnim = target;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pollFootsteps();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void playAction(AnimationAction action) {
|
private void playAction(AnimationAction action) {
|
||||||
@@ -521,6 +612,69 @@ public class PlayerInputControl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setFootstepSystem(de.blight.game.audio.FootstepSystem fs,
|
||||||
|
java.util.function.BiFunction<Float, Float, float[]> query) {
|
||||||
|
this.footstepSystem = fs;
|
||||||
|
this.surfaceQuery = query;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void logJointNames(com.jme3.anim.Armature arm) {
|
||||||
|
if (arm == null) return;
|
||||||
|
StringBuilder sb = new StringBuilder("[Footstep] Joint-Namen (").append(arm.getJointCount()).append("):");
|
||||||
|
for (int i = 0; i < arm.getJointCount(); i++) {
|
||||||
|
sb.append("\n [").append(i).append("] ").append(arm.getJoint(i).getName());
|
||||||
|
}
|
||||||
|
log.info("{}", sb);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void pollFootsteps() {
|
||||||
|
if (armature == null || footstepSystem == null || surfaceQuery == null) return;
|
||||||
|
if (!physicsChar.onGround() || blockingAnimActive || lockedInPlace) {
|
||||||
|
lastLeftFootY = Float.MAX_VALUE;
|
||||||
|
lastRightFootY = Float.MAX_VALUE;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
float speed = physicsChar.getWalkDirection().length();
|
||||||
|
if (speed < 0.001f) {
|
||||||
|
lastLeftFootY = Float.MAX_VALUE;
|
||||||
|
lastRightFootY = Float.MAX_VALUE;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String gait = currentAnimToGait(currentAnim);
|
||||||
|
if (gait == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
com.jme3.anim.Joint lf = armature.getJoint(BONE_LEFT_FOOT);
|
||||||
|
com.jme3.anim.Joint rf = armature.getJoint(BONE_RIGHT_FOOT);
|
||||||
|
if (lf != null) {
|
||||||
|
float y = lf.getModelTransform().getTranslation().y;
|
||||||
|
if (y < FOOT_GROUND_Y && lastLeftFootY >= FOOT_GROUND_Y) {
|
||||||
|
triggerStep(gait);
|
||||||
|
}
|
||||||
|
lastLeftFootY = y;
|
||||||
|
}
|
||||||
|
if (rf != null) {
|
||||||
|
float y = rf.getModelTransform().getTranslation().y;
|
||||||
|
if (y < FOOT_GROUND_Y && lastRightFootY >= FOOT_GROUND_Y) {
|
||||||
|
triggerStep(gait);
|
||||||
|
}
|
||||||
|
lastRightFootY = y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String currentAnimToGait(AnimationAction anim) {
|
||||||
|
if (anim == AnimationAction.WALK) return "walking";
|
||||||
|
if (anim == AnimationAction.RUN) return "running";
|
||||||
|
if (anim == AnimationAction.SPRINT) return "sprinting";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void triggerStep(String gait) {
|
||||||
|
Vector3f pos = physicsChar.getPhysicsLocation();
|
||||||
|
float[] weights = surfaceQuery.apply(pos.x, pos.z);
|
||||||
|
footstepSystem.play(weights, gait);
|
||||||
|
}
|
||||||
|
|
||||||
/** Durchsucht den Szenegraphen rekursiv nach dem ersten SkinningControl. */
|
/** Durchsucht den Szenegraphen rekursiv nach dem ersten SkinningControl. */
|
||||||
private com.jme3.anim.SkinningControl findSkinningControl(Spatial s) {
|
private com.jme3.anim.SkinningControl findSkinningControl(Spatial s) {
|
||||||
if (s == null) {
|
if (s == null) {
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package de.blight.game.post;
|
||||||
|
|
||||||
|
import com.jme3.asset.AssetManager;
|
||||||
|
import com.jme3.material.Material;
|
||||||
|
import com.jme3.post.Filter;
|
||||||
|
import com.jme3.renderer.RenderManager;
|
||||||
|
import com.jme3.renderer.ViewPort;
|
||||||
|
|
||||||
|
/** Einfacher 5×5-Gauß-Unschärfe-Filter für das Pause-/Inventar-Menü. */
|
||||||
|
public class GaussianBlurFilter extends Filter {
|
||||||
|
|
||||||
|
private final float blurScale;
|
||||||
|
|
||||||
|
public GaussianBlurFilter(float blurScale) {
|
||||||
|
super("GaussianBlur");
|
||||||
|
this.blurScale = blurScale;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initFilter(AssetManager manager, RenderManager renderManager,
|
||||||
|
ViewPort vp, int w, int h) {
|
||||||
|
material = new Material(manager, "MatDefs/GaussianBlur.j3md");
|
||||||
|
material.setFloat("BlurScale", blurScale);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Material getMaterial() {
|
||||||
|
return material;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -44,10 +44,12 @@ import de.blight.game.state.SculptedMeshState;
|
|||||||
import de.blight.game.state.WaterBodyState;
|
import de.blight.game.state.WaterBodyState;
|
||||||
import de.blight.game.state.DayNightState;
|
import de.blight.game.state.DayNightState;
|
||||||
import de.blight.game.state.WeatherState;
|
import de.blight.game.state.WeatherState;
|
||||||
|
import de.blight.game.state.DialogHudState;
|
||||||
import de.blight.game.state.InteractionHudState;
|
import de.blight.game.state.InteractionHudState;
|
||||||
import de.blight.game.state.InventoryState;
|
import de.blight.game.state.InventoryState;
|
||||||
import de.blight.game.state.WorldInteractableState;
|
import de.blight.game.state.WorldInteractableState;
|
||||||
import de.blight.game.state.WorldItemsState;
|
import de.blight.game.state.WorldItemsState;
|
||||||
|
import de.blight.game.state.WorldNpcsState;
|
||||||
import de.blight.game.state.StoneWorldState;
|
import de.blight.game.state.StoneWorldState;
|
||||||
import de.blight.game.state.WorldObjectsState;
|
import de.blight.game.state.WorldObjectsState;
|
||||||
import de.blight.game.state.WorldLightState;
|
import de.blight.game.state.WorldLightState;
|
||||||
@@ -83,15 +85,89 @@ public class WorldScene extends BaseAppState {
|
|||||||
private CharacterControl physicsChar;
|
private CharacterControl physicsChar;
|
||||||
private boolean animContextReady = false;
|
private boolean animContextReady = false;
|
||||||
private boolean physicsCharPending = false;
|
private boolean physicsCharPending = false;
|
||||||
private float spawnX = 0f;
|
private float spawnX = 0f;
|
||||||
private float spawnY = 5f;
|
private float spawnY = 5f;
|
||||||
private float spawnZ = 0f;
|
private float spawnZ = 0f;
|
||||||
|
private float spawnYaw = 0f;
|
||||||
|
private de.blight.game.state.OceanSoundState oceanSound;
|
||||||
|
private de.blight.game.state.AmbientSoundSystem ambientSounds;
|
||||||
|
private de.blight.game.audio.FootstepSystem footstepSystem;
|
||||||
|
|
||||||
public WorldScene(KeyBindings keyBindings) {
|
public WorldScene(KeyBindings keyBindings) {
|
||||||
this.keyBindings = keyBindings;
|
this.keyBindings = keyBindings;
|
||||||
}
|
}
|
||||||
|
|
||||||
public InventoryState getInventoryState() { return inventoryState; }
|
public InventoryState getInventoryState() { return inventoryState; }
|
||||||
|
public com.jme3.post.FilterPostProcessor getSharedFPP() { return sharedFPP; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt für jede SurfaceType einen normierten Blend-Anteil (0..1) zurück,
|
||||||
|
* proportional zu den Splatmap-Gewichten an dieser Position.
|
||||||
|
* Indiziert über SurfaceType.ordinal().
|
||||||
|
*/
|
||||||
|
public float[] querySurfaceWeights(float worldX, float worldZ) {
|
||||||
|
float[] result = new float[de.blight.game.audio.SurfaceType.values().length];
|
||||||
|
if (loadedMapData == null) return result;
|
||||||
|
|
||||||
|
int size = de.blight.common.MapData.SPLAT_SIZE;
|
||||||
|
int px = Math.max(0, Math.min(size - 1, Math.round((worldX + 2048f) / 2f)));
|
||||||
|
// Z ist im Splatmap gespiegelt (Editor: pz = (size-1) - round((z+2048)/2))
|
||||||
|
int pz = Math.max(0, Math.min(size - 1, (size - 1) - Math.round((worldZ + 2048f) / 2f)));
|
||||||
|
int idx = pz * size + px;
|
||||||
|
|
||||||
|
// Upper/Third-Splatmap unterdrücken die Basistextur visuell (Overlay-Modell).
|
||||||
|
int maxOverlay = Math.max(
|
||||||
|
Math.max(loadedMapData.upperSplatR[idx] & 0xFF, loadedMapData.upperSplatG[idx] & 0xFF),
|
||||||
|
Math.max(
|
||||||
|
Math.max(loadedMapData.upperSplatB[idx] & 0xFF, loadedMapData.upperSplatA[idx] & 0xFF),
|
||||||
|
Math.max(
|
||||||
|
Math.max(loadedMapData.thirdSplatR[idx] & 0xFF, loadedMapData.thirdSplatG[idx] & 0xFF),
|
||||||
|
Math.max(loadedMapData.thirdSplatB[idx] & 0xFF, loadedMapData.thirdSplatA[idx] & 0xFF)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
float baseScale = Math.max(0f, (255 - maxOverlay) / 255f);
|
||||||
|
|
||||||
|
int splatR = loadedMapData.splatR[idx] & 0xFF;
|
||||||
|
if (splatR == 0) splatR = 255; // Alte Maps: wie im Shader auf 255 normieren
|
||||||
|
|
||||||
|
int[] slotW = {
|
||||||
|
(int)(splatR * baseScale), // slot 0: gras1
|
||||||
|
(int)((loadedMapData.splatG[idx] & 0xFF) * baseScale), // slot 1
|
||||||
|
(int)((loadedMapData.splatB[idx] & 0xFF) * baseScale), // slot 2
|
||||||
|
(int)((loadedMapData.splatA[idx] & 0xFF) * baseScale), // slot 3
|
||||||
|
loadedMapData.upperSplatR[idx] & 0xFF, // slot 4
|
||||||
|
loadedMapData.upperSplatG[idx] & 0xFF, // slot 5
|
||||||
|
loadedMapData.upperSplatB[idx] & 0xFF, // slot 6
|
||||||
|
loadedMapData.upperSplatA[idx] & 0xFF, // slot 7
|
||||||
|
loadedMapData.thirdSplatR[idx] & 0xFF, // slot 8
|
||||||
|
loadedMapData.thirdSplatG[idx] & 0xFF, // slot 9
|
||||||
|
loadedMapData.thirdSplatB[idx] & 0xFF, // slot 10
|
||||||
|
loadedMapData.thirdSplatA[idx] & 0xFF, // slot 11
|
||||||
|
};
|
||||||
|
|
||||||
|
// Gewichte auf SurfaceTypes akkumulieren
|
||||||
|
int total = 0;
|
||||||
|
for (int i = 0; i < slotW.length; i++) {
|
||||||
|
if (slotW[i] <= 0) continue;
|
||||||
|
String path;
|
||||||
|
if (i < 4) {
|
||||||
|
path = (loadedMapData.terrainTextures.length > i) ? loadedMapData.terrainTextures[i] : "";
|
||||||
|
} else if (i < 8) {
|
||||||
|
path = (loadedMapData.upperTextures.length > i - 4) ? loadedMapData.upperTextures[i - 4] : "";
|
||||||
|
} else {
|
||||||
|
path = (loadedMapData.thirdTextures.length > i - 8) ? loadedMapData.thirdTextures[i - 8] : "";
|
||||||
|
}
|
||||||
|
de.blight.game.audio.SurfaceType st = de.blight.game.audio.SurfaceType.fromTexturePath(path);
|
||||||
|
result[st.ordinal()] += slotW[i];
|
||||||
|
total += slotW[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (total > 0) {
|
||||||
|
for (int i = 0; i < result.length; i++) result[i] /= total;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
/** Wird von ConfigScreen nach dem Speichern aufgerufen. */
|
/** Wird von ConfigScreen nach dem Speichern aufgerufen. */
|
||||||
public void reloadBindings(KeyBindings kb) {
|
public void reloadBindings(KeyBindings kb) {
|
||||||
@@ -173,6 +249,12 @@ public class WorldScene extends BaseAppState {
|
|||||||
playerInput.setPhysicsCharacter(physicsChar);
|
playerInput.setPhysicsCharacter(physicsChar);
|
||||||
playerInput.setVisual(characterVisual != null ? characterVisual : character);
|
playerInput.setVisual(characterVisual != null ? characterVisual : character);
|
||||||
|
|
||||||
|
de.blight.game.state.AudioSettingsState audioSettings =
|
||||||
|
app.getStateManager().getState(de.blight.game.state.AudioSettingsState.class);
|
||||||
|
footstepSystem = new de.blight.game.audio.FootstepSystem(assetManager, rootNode, audioSettings,
|
||||||
|
AnimationLibrary.findAssetRoot());
|
||||||
|
playerInput.setFootstepSystem(footstepSystem, this::querySurfaceWeights);
|
||||||
|
|
||||||
// Navigation: PathFinder + Terrain bereitstellen (Navigator wird in setAnimationContext erstellt)
|
// Navigation: PathFinder + Terrain bereitstellen (Navigator wird in setAnimationContext erstellt)
|
||||||
try {
|
try {
|
||||||
de.blight.game.navigation.PathFinder pf = de.blight.game.navigation.PathFinder.load();
|
de.blight.game.navigation.PathFinder pf = de.blight.game.navigation.PathFinder.load();
|
||||||
@@ -197,6 +279,9 @@ public class WorldScene extends BaseAppState {
|
|||||||
new WorldItemsState(keyBindings, physicsChar, mc, playerInput));
|
new WorldItemsState(keyBindings, physicsChar, mc, playerInput));
|
||||||
app.getStateManager().attach(
|
app.getStateManager().attach(
|
||||||
new WorldInteractableState(keyBindings, physicsChar, playerInput));
|
new WorldInteractableState(keyBindings, physicsChar, playerInput));
|
||||||
|
app.getStateManager().attach(new DialogHudState());
|
||||||
|
app.getStateManager().attach(
|
||||||
|
new WorldNpcsState(keyBindings, physicsChar, playerInput, mc));
|
||||||
app.getStateManager().attach(new InteractionHudState());
|
app.getStateManager().attach(new InteractionHudState());
|
||||||
inventoryState = new InventoryState(mc, keyBindings);
|
inventoryState = new InventoryState(mc, keyBindings);
|
||||||
inventoryState.setEnabled(false);
|
inventoryState.setEnabled(false);
|
||||||
@@ -225,12 +310,26 @@ public class WorldScene extends BaseAppState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!animContextReady && animLib != null && animLib.isInitialized()) {
|
if (!animContextReady && animLib != null && animLib.isInitialized()) {
|
||||||
setupAnimationContext();
|
animContextReady = true; // zuerst setzen – verhindert endlose Wiederholung bei Exception
|
||||||
animContextReady = true;
|
try {
|
||||||
|
setupAnimationContext();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[WorldScene] setupAnimationContext() fehlgeschlagen – Character trotzdem eingeblendet", e);
|
||||||
|
if (characterVisual != null) characterVisual.setCullHint(Spatial.CullHint.Inherit);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
playerInput.update(tpf);
|
playerInput.update(tpf);
|
||||||
thirdPersonCam.update(tpf);
|
thirdPersonCam.update(tpf);
|
||||||
|
|
||||||
|
if (physicsChar != null) {
|
||||||
|
com.jme3.math.Vector3f pos = physicsChar.getPhysicsLocation();
|
||||||
|
// Audio-Listener folgt dem Spieler (Ohr-Position + Kamera-Orientierung)
|
||||||
|
app.getListener().setLocation(pos);
|
||||||
|
app.getListener().setRotation(app.getCamera().getRotation());
|
||||||
|
if (oceanSound != null) oceanSound.setPlayerPosition(pos);
|
||||||
|
if (ambientSounds != null) ambientSounds.setPlayerPosition(pos);
|
||||||
|
}
|
||||||
|
|
||||||
// Terrain-Shader mit DayNightState-Licht synchronisieren (Richtung + Farben)
|
// Terrain-Shader mit DayNightState-Licht synchronisieren (Richtung + Farben)
|
||||||
if (terrainMaterial != null && dayNight != null
|
if (terrainMaterial != null && dayNight != null
|
||||||
&& dayNight.getSunLight() != null) {
|
&& dayNight.getSunLight() != null) {
|
||||||
@@ -319,6 +418,16 @@ public class WorldScene extends BaseAppState {
|
|||||||
if (characterVisual != null) {
|
if (characterVisual != null) {
|
||||||
characterVisual.setCullHint(Spatial.CullHint.Inherit);
|
characterVisual.setCullHint(Spatial.CullHint.Inherit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
playerInput.setInitialFacing(spawnYaw);
|
||||||
|
|
||||||
|
if ("true".equals(System.getProperty("blight.new.game"))) {
|
||||||
|
String reviveClip = de.blight.game.animation.AnimationLibrary.getClipForAction(
|
||||||
|
AnimationLibrary.findAssetRoot(), setName, de.blight.game.animation.AnimationAction.REVIVE);
|
||||||
|
float reviveLength = playerInput.getReviveClipLength();
|
||||||
|
app.getStateManager().attach(
|
||||||
|
new de.blight.game.state.NewGameIntroState(playerInput, reviveClip, reviveLength));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CharacterControl setzt den Spatial auf den Kapsel-Mittelpunkt: radius=0.4, halfCyl=0.5 → 0.9m über dem Boden.
|
// CharacterControl setzt den Spatial auf den Kapsel-Mittelpunkt: radius=0.4, halfCyl=0.5 → 0.9m über dem Boden.
|
||||||
@@ -500,22 +609,28 @@ public class WorldScene extends BaseAppState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Spawn-Priorität: 1) Editor-Property 2) Spielstand 3) Karten-Default
|
// Spawn-Priorität: 1) Temp-Editor-Property 2) Spielstand (außer Neues Spiel) 3) Karten-Default
|
||||||
String propX = System.getProperty("blight.temp.spawn.x");
|
String propX = System.getProperty("blight.temp.spawn.x");
|
||||||
String propZ = System.getProperty("blight.temp.spawn.z");
|
String propZ = System.getProperty("blight.temp.spawn.z");
|
||||||
|
String propYaw = System.getProperty("blight.temp.spawn.yaw");
|
||||||
|
boolean isNewGame = "true".equals(System.getProperty("blight.new.game"));
|
||||||
|
|
||||||
if (propX != null) {
|
if (propX != null) {
|
||||||
spawnX = Float.parseFloat(propX);
|
spawnX = Float.parseFloat(propX);
|
||||||
spawnZ = propZ != null ? Float.parseFloat(propZ) : (loadedMapData != null ? loadedMapData.spawnZ : 0f);
|
spawnZ = propZ != null ? Float.parseFloat(propZ) : (loadedMapData != null ? loadedMapData.spawnZ : 0f);
|
||||||
|
spawnYaw = propYaw != null ? Float.parseFloat(propYaw) : 0f;
|
||||||
} else {
|
} else {
|
||||||
de.blight.game.state.SaveGameState saveState =
|
de.blight.game.state.SaveGameState saveState = isNewGame ? null :
|
||||||
app.getStateManager().getState(de.blight.game.state.SaveGameState.class);
|
app.getStateManager().getState(de.blight.game.state.SaveGameState.class);
|
||||||
if (saveState != null && saveState.getSave().character.positionSaved) {
|
if (saveState != null && saveState.getSave().character.positionSaved) {
|
||||||
spawnX = saveState.getSave().character.x;
|
spawnX = saveState.getSave().character.x;
|
||||||
spawnY = saveState.getSave().character.y;
|
spawnY = saveState.getSave().character.y;
|
||||||
spawnZ = saveState.getSave().character.z;
|
spawnZ = saveState.getSave().character.z;
|
||||||
|
spawnYaw = loadedMapData != null ? loadedMapData.spawnYaw : 0f;
|
||||||
} else if (loadedMapData != null) {
|
} else if (loadedMapData != null) {
|
||||||
spawnX = loadedMapData.spawnX;
|
spawnX = loadedMapData.spawnX;
|
||||||
spawnZ = loadedMapData.spawnZ;
|
spawnZ = loadedMapData.spawnZ;
|
||||||
|
spawnYaw = loadedMapData.spawnYaw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.info("[WorldScene] SpawnXZ: X={} Z={}", spawnX, spawnZ);
|
log.info("[WorldScene] SpawnXZ: X={} Z={}", spawnX, spawnZ);
|
||||||
@@ -545,6 +660,12 @@ public class WorldScene extends BaseAppState {
|
|||||||
terrainChunkState.setSpawnHint(spawnX, spawnZ);
|
terrainChunkState.setSpawnHint(spawnX, spawnZ);
|
||||||
|
|
||||||
app.getStateManager().attach(new SculptedMeshState(bulletAppState, loadedMapData));
|
app.getStateManager().attach(new SculptedMeshState(bulletAppState, loadedMapData));
|
||||||
|
|
||||||
|
oceanSound = new de.blight.game.state.OceanSoundState(terrainChunkState);
|
||||||
|
app.getStateManager().attach(oceanSound);
|
||||||
|
|
||||||
|
ambientSounds = new de.blight.game.state.AmbientSoundSystem();
|
||||||
|
app.getStateManager().attach(ambientSounds);
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
@@ -816,7 +937,11 @@ public class WorldScene extends BaseAppState {
|
|||||||
for (int i = 0; i < 4; i++) diffFb[4+i] = new ColorRGBA(0.45f, 0.32f, 0.25f, 1f);
|
for (int i = 0; i < 4; i++) diffFb[4+i] = new ColorRGBA(0.45f, 0.32f, 0.25f, 1f);
|
||||||
for (int i = 0; i < 4; i++) diffFb[8+i] = new ColorRGBA(0.45f, 0.32f, 0.25f, 1f);
|
for (int i = 0; i < 4; i++) diffFb[8+i] = new ColorRGBA(0.45f, 0.32f, 0.25f, 1f);
|
||||||
debugSlot0Path = diffPaths[0];
|
debugSlot0Path = diffPaths[0];
|
||||||
log.info("[Terrain] Slot-0 Textur = '{}' Slot-1 = '{}'", diffPaths[0], diffPaths[1]);
|
for (int i = 0; i < 12; i++) {
|
||||||
|
if (diffPaths[i] != null && !diffPaths[i].isEmpty()) {
|
||||||
|
log.info("[Terrain] Slot-{} = '{}'", i, diffPaths[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
mat.setParam("DiffuseArray", com.jme3.shader.VarType.TextureArray,
|
mat.setParam("DiffuseArray", com.jme3.shader.VarType.TextureArray,
|
||||||
buildTextureArray(diffPaths, diffFb, assetManager));
|
buildTextureArray(diffPaths, diffFb, assetManager));
|
||||||
|
|
||||||
|
|||||||
@@ -18,23 +18,29 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Distanzbasierter Ambient-Sound pro Polygon-Bereich.
|
* Distanzbasierter Ambient-Sound pro Polygon-Bereich, jetzt mit Stereo-Panning:
|
||||||
* Innerhalb des Polygons: volle Lautstärke.
|
* - Innerhalb des Polygons: nicht-positional (Rundum-Klang).
|
||||||
* Außerhalb: lineares Fade bis zur Reichweite (volume * CROSSFADE_SCALE Einheiten).
|
* - Außerhalb: AudioNode am nächsten Polygon-Rand → OpenAL panst aus der richtigen Richtung.
|
||||||
|
* Lautstärke-Fading bleibt manuell; OpenAL's Distanz-Dämpfung wird mit großem
|
||||||
|
* refDistance deaktiviert, damit es keine Doppeldämpfung gibt.
|
||||||
*/
|
*/
|
||||||
public class AmbientSoundSystem extends BaseAppState {
|
public class AmbientSoundSystem extends BaseAppState {
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(AmbientSoundSystem.class);
|
private static final Logger log = LoggerFactory.getLogger(AmbientSoundSystem.class);
|
||||||
private static final float CROSSFADE_SCALE = 30f; // Einheiten Reichweite außerhalb bei volume=1.0
|
private static final float CROSSFADE_SCALE = 30f;
|
||||||
private static final float FADE_DURATION = 2f; // Sekunden für vollen Lautstärke-Hub
|
private static final float FADE_DURATION = 2f;
|
||||||
|
/** RefDistance > jede denkbare Spieler-AudioNode-Distanz → OpenAL dämpft nicht. */
|
||||||
|
private static final float NO_ATTN_REF = 1000f;
|
||||||
|
private static final float NO_ATTN_MAX = 2000f;
|
||||||
|
|
||||||
private SimpleApplication app;
|
private SimpleApplication app;
|
||||||
private AssetManager assets;
|
private AssetManager assets;
|
||||||
private Node rootNode;
|
private Node rootNode;
|
||||||
|
|
||||||
private final List<PlacedSoundArea> data = new ArrayList<>();
|
private final List<PlacedSoundArea> data = new ArrayList<>();
|
||||||
private final List<AudioNode> sounds = new ArrayList<>();
|
private final List<AudioNode> sounds = new ArrayList<>();
|
||||||
private final List<Boolean> attached = new ArrayList<>();
|
private final List<Boolean> attached = new ArrayList<>();
|
||||||
|
private final List<Boolean> wasInside = new ArrayList<>();
|
||||||
|
|
||||||
private final Vector3f playerPos = new Vector3f();
|
private final Vector3f playerPos = new Vector3f();
|
||||||
|
|
||||||
@@ -51,10 +57,14 @@ public class AmbientSoundSystem extends BaseAppState {
|
|||||||
AudioNode node = new AudioNode(assets, area.soundPath(), AudioData.DataType.Stream);
|
AudioNode node = new AudioNode(assets, area.soundPath(), AudioData.DataType.Stream);
|
||||||
node.setLooping(true);
|
node.setLooping(true);
|
||||||
node.setVolume(0f);
|
node.setVolume(0f);
|
||||||
node.setPositional(false);
|
// Positional-Modus: refDistance sehr groß → OpenAL dämpft nicht selbst
|
||||||
|
node.setPositional(true);
|
||||||
|
node.setRefDistance(NO_ATTN_REF);
|
||||||
|
node.setMaxDistance(NO_ATTN_MAX);
|
||||||
data.add(area);
|
data.add(area);
|
||||||
sounds.add(node);
|
sounds.add(node);
|
||||||
attached.add(false);
|
attached.add(false);
|
||||||
|
wasInside.add(false);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("[AmbientSoundSystem] Sound nicht ladbar '{}': {}", area.soundPath(), e.getMessage());
|
log.warn("[AmbientSoundSystem] Sound nicht ladbar '{}': {}", area.soundPath(), e.getMessage());
|
||||||
}
|
}
|
||||||
@@ -77,6 +87,7 @@ public class AmbientSoundSystem extends BaseAppState {
|
|||||||
data.clear();
|
data.clear();
|
||||||
sounds.clear();
|
sounds.clear();
|
||||||
attached.clear();
|
attached.clear();
|
||||||
|
wasInside.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override protected void onEnable() {}
|
@Override protected void onEnable() {}
|
||||||
@@ -95,9 +106,8 @@ public class AmbientSoundSystem extends BaseAppState {
|
|||||||
AudioNode node = sounds.get(i);
|
AudioNode node = sounds.get(i);
|
||||||
float target = computeTarget(area);
|
float target = computeTarget(area);
|
||||||
float cur = node.getVolume();
|
float cur = node.getVolume();
|
||||||
boolean wasOn = attached.get(i);
|
|
||||||
|
|
||||||
if (target > 0f && !wasOn) {
|
if (target > 0f && !attached.get(i)) {
|
||||||
node.setVolume(0f);
|
node.setVolume(0f);
|
||||||
rootNode.attachChild(node);
|
rootNode.attachChild(node);
|
||||||
node.play();
|
node.play();
|
||||||
@@ -106,6 +116,9 @@ public class AmbientSoundSystem extends BaseAppState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (attached.get(i)) {
|
if (attached.get(i)) {
|
||||||
|
// Stereo-Panning: AudioNode zur richtigen Richtung verschieben
|
||||||
|
updateNodePosition(node, area, i);
|
||||||
|
|
||||||
float step = area.volume() * tpf / FADE_DURATION;
|
float step = area.volume() * tpf / FADE_DURATION;
|
||||||
float nv = target > cur
|
float nv = target > cur
|
||||||
? Math.min(cur + step, target)
|
? Math.min(cur + step, target)
|
||||||
@@ -122,11 +135,33 @@ public class AmbientSoundSystem extends BaseAppState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// ── AudioNode-Position für Stereo-Panning ────────────────────────────────
|
||||||
* Zielvolumen basierend auf signiertem Abstand zur Polygongrenze.
|
|
||||||
* Innen (≥0): volle Lautstärke.
|
private void updateNodePosition(AudioNode node, PlacedSoundArea area, int i) {
|
||||||
* Außen: linear von voll (Grenze) auf null (hearRange = volume * CROSSFADE_SCALE).
|
float px = playerPos.x, pz = playerPos.z;
|
||||||
*/
|
boolean inside = pointInPolygon(px, pz, area.pointsX(), area.pointsZ());
|
||||||
|
|
||||||
|
if (inside) {
|
||||||
|
// Innerhalb: nicht-positional → Rundum-Klang
|
||||||
|
if (!wasInside.get(i)) {
|
||||||
|
node.setPositional(false);
|
||||||
|
wasInside.set(i, true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Außerhalb: positional am nächsten Rand-Punkt
|
||||||
|
if (wasInside.get(i)) {
|
||||||
|
node.setPositional(true);
|
||||||
|
node.setRefDistance(NO_ATTN_REF);
|
||||||
|
node.setMaxDistance(NO_ATTN_MAX);
|
||||||
|
wasInside.set(i, false);
|
||||||
|
}
|
||||||
|
float[] nearest = nearestPointOnPolygon(px, pz, area.pointsX(), area.pointsZ());
|
||||||
|
node.setLocalTranslation(nearest[0], playerPos.y, nearest[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Polygon-Berechnungen ─────────────────────────────────────────────────
|
||||||
|
|
||||||
private float computeTarget(PlacedSoundArea area) {
|
private float computeTarget(PlacedSoundArea area) {
|
||||||
float signedDist = signedDistToPolygon(playerPos.x, playerPos.z, area.pointsX(), area.pointsZ());
|
float signedDist = signedDistToPolygon(playerPos.x, playerPos.z, area.pointsX(), area.pointsZ());
|
||||||
if (signedDist >= 0f) return area.volume();
|
if (signedDist >= 0f) return area.volume();
|
||||||
@@ -135,18 +170,38 @@ public class AmbientSoundSystem extends BaseAppState {
|
|||||||
return area.volume() * (1f + signedDist / hearRange);
|
return area.volume() * (1f + signedDist / hearRange);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Positiv = Spieler ist innen, Negativ = Spieler ist außen. */
|
||||||
* Positiv = Spieler ist innen (Distanz zur nächsten Kante).
|
|
||||||
* Negativ = Spieler ist außen (negierte Distanz zur nächsten Kante).
|
|
||||||
*/
|
|
||||||
private static float signedDistToPolygon(float px, float pz, float[] xs, float[] zs) {
|
private static float signedDistToPolygon(float px, float pz, float[] xs, float[] zs) {
|
||||||
float edgeDist = minDistToPolygonEdge(px, pz, xs, zs);
|
float edgeDist = minDistToPolygonEdge(px, pz, xs, zs);
|
||||||
return pointInPolygon(px, pz, xs, zs) ? edgeDist : -edgeDist;
|
return pointInPolygon(px, pz, xs, zs) ? edgeDist : -edgeDist;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static float minDistToPolygonEdge(float px, float pz, float[] xs, float[] zs) {
|
/** Nächster Punkt auf einer Polygon-Kante (x, z) als float[2]. */
|
||||||
|
private static float[] nearestPointOnPolygon(float px, float pz, float[] xs, float[] zs) {
|
||||||
int n = xs.length;
|
int n = xs.length;
|
||||||
float minD2 = Float.MAX_VALUE;
|
float minD2 = Float.MAX_VALUE;
|
||||||
|
float bestX = xs[0], bestZ = zs[0];
|
||||||
|
for (int i = 0, j = n - 1; i < n; j = i++) {
|
||||||
|
float ax = xs[j], az = zs[j];
|
||||||
|
float bx = xs[i], bz = zs[i];
|
||||||
|
float dx = bx - ax, dz = bz - az;
|
||||||
|
float lenSq = dx * dx + dz * dz;
|
||||||
|
float t = lenSq == 0f ? 0f : Math.max(0f, Math.min(1f, ((px - ax) * dx + (pz - az) * dz) / lenSq));
|
||||||
|
float cx = ax + t * dx;
|
||||||
|
float cz = az + t * dz;
|
||||||
|
float d2 = (cx - px) * (cx - px) + (cz - pz) * (cz - pz);
|
||||||
|
if (d2 < minD2) {
|
||||||
|
minD2 = d2;
|
||||||
|
bestX = cx;
|
||||||
|
bestZ = cz;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new float[]{bestX, bestZ};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float minDistToPolygonEdge(float px, float pz, float[] xs, float[] zs) {
|
||||||
|
int n = xs.length;
|
||||||
|
float minD2 = Float.MAX_VALUE;
|
||||||
for (int i = 0, j = n - 1; i < n; j = i++) {
|
for (int i = 0, j = n - 1; i < n; j = i++) {
|
||||||
float d2 = pointToSegmentDist2(px, pz, xs[j], zs[j], xs[i], zs[i]);
|
float d2 = pointToSegmentDist2(px, pz, xs[j], zs[j], xs[i], zs[i]);
|
||||||
if (d2 < minD2) minD2 = d2;
|
if (d2 < minD2) minD2 = d2;
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package de.blight.game.state;
|
||||||
|
|
||||||
|
import com.jme3.app.Application;
|
||||||
|
import com.jme3.app.state.BaseAppState;
|
||||||
|
import de.blight.game.config.AudioSettings;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hält die Audio-Einstellungen zur Laufzeit. Sound-Systeme lesen daraus
|
||||||
|
* ihre effektiven Lautstärken (master × kategorie).
|
||||||
|
*/
|
||||||
|
public class AudioSettingsState extends BaseAppState {
|
||||||
|
|
||||||
|
private final AudioSettings settings;
|
||||||
|
|
||||||
|
public AudioSettingsState(AudioSettings settings) {
|
||||||
|
this.settings = settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override protected void initialize(Application application) {}
|
||||||
|
@Override protected void cleanup(Application application) {}
|
||||||
|
@Override protected void onEnable() {}
|
||||||
|
@Override protected void onDisable() {}
|
||||||
|
|
||||||
|
public AudioSettings getSettings() { return settings; }
|
||||||
|
|
||||||
|
public float getMaster() { return clamp(settings.master); }
|
||||||
|
public float getMusic() { return clamp(settings.music); }
|
||||||
|
public float getSpeech() { return clamp(settings.speech); }
|
||||||
|
public float getEffects() { return clamp(settings.effects); }
|
||||||
|
public float getAmbient() { return clamp(settings.ambient); }
|
||||||
|
|
||||||
|
public float effectiveAmbient() { return getMaster() * getAmbient(); }
|
||||||
|
public float effectiveMusic() { return getMaster() * getMusic(); }
|
||||||
|
public float effectiveEffects() { return getMaster() * getEffects(); }
|
||||||
|
public float effectiveSpeech() { return getMaster() * getSpeech(); }
|
||||||
|
|
||||||
|
private static float clamp(float v) { return Math.max(0f, Math.min(1f, v)); }
|
||||||
|
}
|
||||||
@@ -0,0 +1,578 @@
|
|||||||
|
package de.blight.game.state;
|
||||||
|
|
||||||
|
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.controls.ActionListener;
|
||||||
|
import com.jme3.input.controls.KeyTrigger;
|
||||||
|
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.Spatial;
|
||||||
|
import com.jme3.scene.shape.Quad;
|
||||||
|
import de.blight.common.model.DialogOption;
|
||||||
|
import de.blight.common.model.MainCharacter;
|
||||||
|
import de.blight.common.model.NPC;
|
||||||
|
import de.blight.common.model.TextReference;
|
||||||
|
import de.blight.game.config.MenuCanvas;
|
||||||
|
import de.blight.game.config.NinePatch;
|
||||||
|
import de.blight.lang.TextResolver;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dialog-HUD: zeigt am unteren Bildschirmrand Gesprächstexte und wählbare
|
||||||
|
* Antwortoptionen im Stil des Pausemenüs an.
|
||||||
|
*
|
||||||
|
* <h2>Phasen</h2>
|
||||||
|
* <pre>
|
||||||
|
* HIDDEN
|
||||||
|
* → TEXT_NPC : Begrüßungstext des NPC (Default-Message) oder NPC-Antwort
|
||||||
|
* → TEXT_HERO : Was der Spieler sagt (textHero der gewählten Option)
|
||||||
|
* → OPTIONS : Auswahl der verfügbaren Dialog-Optionen
|
||||||
|
* → HIDDEN : nach "Verlassen"
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* Navigation: Pfeiltasten hoch/runter + Enter ODER Mausklick.
|
||||||
|
* Rechtsklick: Text überspringen / zur nächsten Seite.
|
||||||
|
*/
|
||||||
|
public class DialogHudState extends BaseAppState {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(DialogHudState.class);
|
||||||
|
|
||||||
|
// ── Layout-Konstanten (virtuelle Koordinaten 1376 × 768) ─────────────────
|
||||||
|
|
||||||
|
private static final float PNL_X = 63f;
|
||||||
|
private static final float PNL_Y = 20f;
|
||||||
|
private static final float PNL_W = 1250f;
|
||||||
|
private static final float PNL_H = 255f;
|
||||||
|
|
||||||
|
private static final float MARGIN_X = 20f;
|
||||||
|
private static final float FONT_NAME = 18f;
|
||||||
|
private static final float FONT_TEXT = 16f;
|
||||||
|
private static final float FONT_OPT = 15f;
|
||||||
|
private static final float LINE_H_TXT = 26f;
|
||||||
|
private static final float LINE_H_OPT = 26f;
|
||||||
|
|
||||||
|
private static final int MAX_TEXT_LINES = 3;
|
||||||
|
private static final int MAX_CHARS_LINE = 82;
|
||||||
|
private static final int MAX_OPTIONS = 5;
|
||||||
|
|
||||||
|
private static final ColorRGBA COL_NAME = new ColorRGBA(1.00f, 0.90f, 0.55f, 1f);
|
||||||
|
private static final ColorRGBA COL_TEXT = ColorRGBA.White;
|
||||||
|
private static final ColorRGBA COL_OPT = new ColorRGBA(0.80f, 0.80f, 0.80f, 1f);
|
||||||
|
private static final ColorRGBA COL_OPT_SEL = new ColorRGBA(1.00f, 0.90f, 0.45f, 1f);
|
||||||
|
private static final ColorRGBA COL_HINT = new ColorRGBA(0.55f, 0.55f, 0.55f, 1f);
|
||||||
|
|
||||||
|
private static final String EXIT_KEY = "dialog.exit";
|
||||||
|
|
||||||
|
// ── Input-Action-Namen ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static final String ACT_UP = "_DlgUp";
|
||||||
|
private static final String ACT_DOWN = "_DlgDown";
|
||||||
|
private static final String ACT_CONFIRM = "_DlgConfirm";
|
||||||
|
private static final String ACT_SKIP = "_DlgSkip";
|
||||||
|
private static final String ACT_CLICK = "_DlgClick";
|
||||||
|
|
||||||
|
// ── Zustände ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private enum Phase { HIDDEN, TEXT_NPC, TEXT_HERO, OPTIONS }
|
||||||
|
|
||||||
|
private Phase phase = Phase.HIDDEN;
|
||||||
|
|
||||||
|
// ── Dialog-Daten ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private NPC currentNpc;
|
||||||
|
private MainCharacter mainChar;
|
||||||
|
private Runnable onClose;
|
||||||
|
|
||||||
|
/** Alle Seiten des aktuellen Textes. */
|
||||||
|
private List<List<String>> textPages = new ArrayList<>();
|
||||||
|
private int pageIdx = 0;
|
||||||
|
|
||||||
|
/** Aktuelle Option, die nach dem Text gezeigt werden soll (für Option-Text). */
|
||||||
|
private DialogOption pendingOption;
|
||||||
|
|
||||||
|
/** Verfügbare Optionen (inkl. Exit). */
|
||||||
|
private List<DisplayOption> displayOptions = new ArrayList<>();
|
||||||
|
private int selectedOpt = 0;
|
||||||
|
|
||||||
|
/** Panel-Bounds für Maus-Hit-Tests. */
|
||||||
|
private float[][] optBounds = new float[MAX_OPTIONS][4]; // x,y,w,h
|
||||||
|
|
||||||
|
// ── JME3 UI-Knoten ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private SimpleApplication app;
|
||||||
|
private BitmapFont font;
|
||||||
|
private Node guiNode;
|
||||||
|
private Node canvasNode;
|
||||||
|
private Node panel;
|
||||||
|
|
||||||
|
private BitmapText nameText;
|
||||||
|
private BitmapText[] lineTexts = new BitmapText[MAX_TEXT_LINES];
|
||||||
|
private BitmapText hintText;
|
||||||
|
private BitmapText[] optTexts = new BitmapText[MAX_OPTIONS];
|
||||||
|
|
||||||
|
// ── 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() {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDisable() {
|
||||||
|
if (phase != Phase.HIDDEN) closePanel();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override protected void cleanup(Application app) {}
|
||||||
|
|
||||||
|
// ── Public API ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Startet den Dialog mit dem gegebenen NPC.
|
||||||
|
*
|
||||||
|
* @param npc der angesprochene NPC
|
||||||
|
* @param mc der Hauptcharakter
|
||||||
|
* @param onCloseCallback wird aufgerufen wenn der Dialog endet
|
||||||
|
*/
|
||||||
|
public void startDialog(NPC npc, MainCharacter mc, Runnable onCloseCallback) {
|
||||||
|
this.currentNpc = npc;
|
||||||
|
this.mainChar = mc;
|
||||||
|
this.onClose = onCloseCallback;
|
||||||
|
|
||||||
|
buildPanel();
|
||||||
|
registerInput();
|
||||||
|
|
||||||
|
// Erst: Default-Message anzeigen (falls vorhanden), dann Optionen
|
||||||
|
TextReference greeting = npc.getDefaultMessage();
|
||||||
|
String greetText = greeting != null ? TextResolver.get().resolve(greeting) : null;
|
||||||
|
|
||||||
|
List<DialogOption> available = resolveOptions(npc, mc);
|
||||||
|
|
||||||
|
if (greetText != null && !greetText.isBlank()) {
|
||||||
|
showText(Phase.TEXT_NPC, resolveNpcName(npc), greetText, () -> {
|
||||||
|
if (available.isEmpty()) {
|
||||||
|
closeDialog();
|
||||||
|
} else {
|
||||||
|
showOptions(available);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (!available.isEmpty()) {
|
||||||
|
showOptions(available);
|
||||||
|
} else {
|
||||||
|
// Keine Nachricht, keine Optionen → sofort schließen
|
||||||
|
closeDialog();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isActive() {
|
||||||
|
return phase != Phase.HIDDEN;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Text-Anzeige ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private Runnable afterText;
|
||||||
|
|
||||||
|
private void showText(Phase textPhase, String speaker, String rawText, Runnable after) {
|
||||||
|
this.phase = textPhase;
|
||||||
|
this.afterText = after;
|
||||||
|
this.textPages = paginate(wrapText(rawText, MAX_CHARS_LINE), MAX_TEXT_LINES);
|
||||||
|
this.pageIdx = 0;
|
||||||
|
renderCurrentTextPage(speaker);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void renderCurrentTextPage(String speaker) {
|
||||||
|
nameText.setText(speaker);
|
||||||
|
nameText.setCullHint(Spatial.CullHint.Inherit);
|
||||||
|
|
||||||
|
List<String> page = (pageIdx < textPages.size()) ? textPages.get(pageIdx) : List.of();
|
||||||
|
for (int i = 0; i < MAX_TEXT_LINES; i++) {
|
||||||
|
lineTexts[i].setText(i < page.size() ? page.get(i) : "");
|
||||||
|
lineTexts[i].setCullHint(Spatial.CullHint.Inherit);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean more = (pageIdx + 1) < textPages.size();
|
||||||
|
hintText.setText(more
|
||||||
|
? t("dialog.hint.advance")
|
||||||
|
: t("dialog.hint.continue"));
|
||||||
|
hintText.setCullHint(Spatial.CullHint.Inherit);
|
||||||
|
|
||||||
|
for (BitmapText o : optTexts) o.setCullHint(Spatial.CullHint.Always);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Optionen-Anzeige ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void showOptions(List<DialogOption> options) {
|
||||||
|
phase = Phase.OPTIONS;
|
||||||
|
displayOptions.clear();
|
||||||
|
selectedOpt = 0;
|
||||||
|
|
||||||
|
for (DialogOption opt : options) {
|
||||||
|
String label = TextResolver.get().resolveId(opt.getLabel() != null ? opt.getLabel() : "");
|
||||||
|
if (label.isBlank()) label = opt.getId() != null
|
||||||
|
? opt.getId().substring(0, Math.min(opt.getId().length(), 20)) : "?";
|
||||||
|
displayOptions.add(new DisplayOption(label, opt));
|
||||||
|
}
|
||||||
|
displayOptions.add(new DisplayOption(t(EXIT_KEY), null));
|
||||||
|
|
||||||
|
nameText.setText(resolveNpcName(currentNpc));
|
||||||
|
nameText.setCullHint(Spatial.CullHint.Inherit);
|
||||||
|
for (BitmapText l : lineTexts) l.setCullHint(Spatial.CullHint.Always);
|
||||||
|
hintText.setCullHint(Spatial.CullHint.Always);
|
||||||
|
|
||||||
|
renderOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void renderOptions() {
|
||||||
|
float baseY = PNL_Y + PNL_H - 80f;
|
||||||
|
|
||||||
|
for (int i = 0; i < MAX_OPTIONS; i++) {
|
||||||
|
if (i < displayOptions.size()) {
|
||||||
|
DisplayOption do_ = displayOptions.get(i);
|
||||||
|
boolean sel = (i == selectedOpt);
|
||||||
|
String prefix = sel ? "► " : " ";
|
||||||
|
optTexts[i].setText(prefix + (i + 1) + ". " + do_.label());
|
||||||
|
optTexts[i].setColor(sel ? COL_OPT_SEL : COL_OPT);
|
||||||
|
optTexts[i].setCullHint(Spatial.CullHint.Inherit);
|
||||||
|
|
||||||
|
float ty = baseY - i * LINE_H_OPT;
|
||||||
|
optTexts[i].setLocalTranslation(PNL_X + MARGIN_X, ty, 2f);
|
||||||
|
|
||||||
|
optBounds[i][0] = PNL_X + MARGIN_X;
|
||||||
|
optBounds[i][1] = ty - FONT_OPT;
|
||||||
|
optBounds[i][2] = PNL_W - MARGIN_X * 2;
|
||||||
|
optBounds[i][3] = FONT_OPT + 4;
|
||||||
|
} else {
|
||||||
|
optTexts[i].setCullHint(Spatial.CullHint.Always);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Input-Handler ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void onUp() {
|
||||||
|
if (phase != Phase.OPTIONS) return;
|
||||||
|
selectedOpt = Math.max(0, selectedOpt - 1);
|
||||||
|
renderOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onDown() {
|
||||||
|
if (phase != Phase.OPTIONS) return;
|
||||||
|
selectedOpt = Math.min(displayOptions.size() - 1, selectedOpt + 1);
|
||||||
|
renderOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onConfirm() {
|
||||||
|
if (phase == Phase.TEXT_NPC || phase == Phase.TEXT_HERO) {
|
||||||
|
advanceText();
|
||||||
|
} else if (phase == Phase.OPTIONS) {
|
||||||
|
confirmOption(selectedOpt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onSkip() {
|
||||||
|
if (phase == Phase.TEXT_NPC || phase == Phase.TEXT_HERO) {
|
||||||
|
// Alle verbleibenden Seiten überspringen
|
||||||
|
pageIdx = textPages.size();
|
||||||
|
if (afterText != null) { Runnable cb = afterText; afterText = null; cb.run(); }
|
||||||
|
} else if (phase == Phase.OPTIONS) {
|
||||||
|
confirmOption(selectedOpt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onMouseClick(Vector2f cursor) {
|
||||||
|
if (phase != Phase.OPTIONS) { onConfirm(); return; }
|
||||||
|
|
||||||
|
// Kursorenanpassung auf virtuelle Koordinaten
|
||||||
|
float ox = (app.getCamera().getWidth() - MenuCanvas.REF_W) / 2f;
|
||||||
|
float oy = (app.getCamera().getHeight() - MenuCanvas.REF_H) / 2f;
|
||||||
|
float scale = Math.min(
|
||||||
|
app.getCamera().getWidth() / MenuCanvas.REF_W,
|
||||||
|
app.getCamera().getHeight() / MenuCanvas.REF_H);
|
||||||
|
|
||||||
|
float ox2 = (app.getCamera().getWidth() - MenuCanvas.REF_W * scale) / 2f;
|
||||||
|
float oy2 = (app.getCamera().getHeight() - MenuCanvas.REF_H * scale) / 2f;
|
||||||
|
float vx = (cursor.x - ox2) / scale;
|
||||||
|
float vy = (cursor.y - oy2) / scale;
|
||||||
|
|
||||||
|
for (int i = 0; i < displayOptions.size() && i < MAX_OPTIONS; i++) {
|
||||||
|
float bx = optBounds[i][0], by = optBounds[i][1];
|
||||||
|
float bw = optBounds[i][2], bh = optBounds[i][3];
|
||||||
|
if (vx >= bx && vx <= bx + bw && vy >= by && vy <= by + bh) {
|
||||||
|
selectedOpt = i;
|
||||||
|
renderOptions();
|
||||||
|
confirmOption(i);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void advanceText() {
|
||||||
|
if (pageIdx + 1 < textPages.size()) {
|
||||||
|
pageIdx++;
|
||||||
|
renderCurrentTextPage(nameText.getText());
|
||||||
|
} else if (afterText != null) {
|
||||||
|
Runnable cb = afterText;
|
||||||
|
afterText = null;
|
||||||
|
cb.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void confirmOption(int idx) {
|
||||||
|
if (idx < 0 || idx >= displayOptions.size()) return;
|
||||||
|
DisplayOption selected = displayOptions.get(idx);
|
||||||
|
|
||||||
|
if (selected.option() == null) {
|
||||||
|
// Exit
|
||||||
|
closeDialog();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DialogOption opt = selected.option();
|
||||||
|
|
||||||
|
// Held-Text
|
||||||
|
String heroText = opt.getTextHero() != null
|
||||||
|
? TextResolver.get().resolve(opt.getTextHero()) : null;
|
||||||
|
// NPC-Text
|
||||||
|
String npcText = opt.getTextNpc() != null
|
||||||
|
? TextResolver.get().resolve(opt.getTextNpc()) : null;
|
||||||
|
|
||||||
|
// Option-Effekte anwenden (Optionen aktualisieren, Quest, etc.)
|
||||||
|
applyOption(opt);
|
||||||
|
|
||||||
|
List<DialogOption> nextOpts = resolveOptions(currentNpc, mainChar);
|
||||||
|
|
||||||
|
if (heroText != null && !heroText.isBlank()) {
|
||||||
|
showText(Phase.TEXT_HERO, t("dialog.speaker.player"), heroText, () -> {
|
||||||
|
if (npcText != null && !npcText.isBlank()) {
|
||||||
|
showText(Phase.TEXT_NPC, resolveNpcName(currentNpc), npcText, () -> {
|
||||||
|
if (nextOpts.isEmpty()) closeDialog(); else showOptions(nextOpts);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (nextOpts.isEmpty()) closeDialog(); else showOptions(nextOpts);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (npcText != null && !npcText.isBlank()) {
|
||||||
|
showText(Phase.TEXT_NPC, resolveNpcName(currentNpc), npcText, () -> {
|
||||||
|
if (nextOpts.isEmpty()) closeDialog(); else showOptions(nextOpts);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (nextOpts.isEmpty()) closeDialog(); else showOptions(nextOpts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Optionen-Auflösung ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private List<DialogOption> resolveOptions(NPC npc, MainCharacter mc) {
|
||||||
|
if (npc.getCurrentOptions() == null || mc == null) return List.of();
|
||||||
|
try {
|
||||||
|
return npc.getAvailableOptions(mc);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[DialogHud] Optionen-Auflösung fehlgeschlagen: {}", e.getMessage());
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Wendet die Dialog-Option an (Optionen-Listen aktualisieren, Quests etc.). */
|
||||||
|
private void applyOption(DialogOption opt) {
|
||||||
|
if (currentNpc.getCurrentOptions() == null) return;
|
||||||
|
currentNpc.getCurrentOptions().remove(opt);
|
||||||
|
if (opt.getDisablesOptions() != null)
|
||||||
|
currentNpc.getCurrentOptions().removeAll(opt.getDisablesOptions());
|
||||||
|
if (opt.getNextOptions() != null)
|
||||||
|
currentNpc.getCurrentOptions().addAll(opt.getNextOptions());
|
||||||
|
if (opt.isEnablesTrade()) currentNpc.setTrader(true);
|
||||||
|
if (mainChar != null) mainChar.handleDialogOption(opt);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Dialog beenden ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void closeDialog() {
|
||||||
|
closePanel();
|
||||||
|
if (onClose != null) { Runnable cb = onClose; onClose = null; cb.run(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── UI-Aufbau / -Abbau ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void buildPanel() {
|
||||||
|
canvasNode = MenuCanvas.createFixedCanvas(app.getCamera());
|
||||||
|
guiNode.attachChild(canvasNode);
|
||||||
|
|
||||||
|
panel = new Node("dialog-panel");
|
||||||
|
|
||||||
|
// Hintergrundpanel
|
||||||
|
panel.attachChild(NinePatch.panel(app.getAssetManager())
|
||||||
|
.build(PNL_X, PNL_Y, PNL_W, PNL_H, -1f));
|
||||||
|
|
||||||
|
// NPC-Name
|
||||||
|
nameText = makeTxt("", FONT_NAME, COL_NAME);
|
||||||
|
nameText.setLocalTranslation(PNL_X + MARGIN_X, PNL_Y + PNL_H - 22f, 2f);
|
||||||
|
nameText.setCullHint(Spatial.CullHint.Always);
|
||||||
|
panel.attachChild(nameText);
|
||||||
|
|
||||||
|
// Trennlinie (dünnes Quad)
|
||||||
|
panel.attachChild(makeSeparator(PNL_X + MARGIN_X, PNL_Y + PNL_H - 46f, PNL_W - MARGIN_X * 2));
|
||||||
|
|
||||||
|
// Dialog-Textzeilen
|
||||||
|
float textStartY = PNL_Y + PNL_H - 72f;
|
||||||
|
for (int i = 0; i < MAX_TEXT_LINES; i++) {
|
||||||
|
lineTexts[i] = makeTxt("", FONT_TEXT, COL_TEXT);
|
||||||
|
lineTexts[i].setLocalTranslation(PNL_X + MARGIN_X, textStartY - i * LINE_H_TXT, 2f);
|
||||||
|
lineTexts[i].setCullHint(Spatial.CullHint.Always);
|
||||||
|
panel.attachChild(lineTexts[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hint-Text (Rechtsklick-Weiter)
|
||||||
|
hintText = makeTxt("", FONT_TEXT - 2f, COL_HINT);
|
||||||
|
hintText.setLocalTranslation(PNL_X + PNL_W - MARGIN_X - 300f, PNL_Y + 28f, 2f);
|
||||||
|
hintText.setCullHint(Spatial.CullHint.Always);
|
||||||
|
panel.attachChild(hintText);
|
||||||
|
|
||||||
|
// Optionszeilen
|
||||||
|
for (int i = 0; i < MAX_OPTIONS; i++) {
|
||||||
|
optTexts[i] = makeTxt("", FONT_OPT, COL_OPT);
|
||||||
|
optTexts[i].setCullHint(Spatial.CullHint.Always);
|
||||||
|
panel.attachChild(optTexts[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
canvasNode.attachChild(panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void closePanel() {
|
||||||
|
phase = Phase.HIDDEN;
|
||||||
|
unregisterInput();
|
||||||
|
if (canvasNode != null) {
|
||||||
|
guiNode.detachChild(canvasNode);
|
||||||
|
canvasNode = null;
|
||||||
|
panel = null;
|
||||||
|
}
|
||||||
|
textPages.clear();
|
||||||
|
displayOptions.clear();
|
||||||
|
pendingOption = null;
|
||||||
|
afterText = null;
|
||||||
|
currentNpc = null;
|
||||||
|
mainChar = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Input-Registrierung ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void registerInput() {
|
||||||
|
var im = app.getInputManager();
|
||||||
|
im.addMapping(ACT_UP, new KeyTrigger(KeyInput.KEY_UP));
|
||||||
|
im.addMapping(ACT_DOWN, new KeyTrigger(KeyInput.KEY_DOWN));
|
||||||
|
im.addMapping(ACT_CONFIRM, new KeyTrigger(KeyInput.KEY_RETURN),
|
||||||
|
new KeyTrigger(KeyInput.KEY_NUMPADENTER));
|
||||||
|
im.addMapping(ACT_SKIP, new MouseButtonTrigger(MouseInput.BUTTON_RIGHT));
|
||||||
|
im.addMapping(ACT_CLICK, new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
|
||||||
|
im.addListener(inputListener, ACT_UP, ACT_DOWN, ACT_CONFIRM, ACT_SKIP, ACT_CLICK);
|
||||||
|
im.setCursorVisible(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void unregisterInput() {
|
||||||
|
var im = app.getInputManager();
|
||||||
|
try { im.removeListener(inputListener); } catch (Exception ignored) {}
|
||||||
|
for (String a : new String[]{ACT_UP, ACT_DOWN, ACT_CONFIRM, ACT_SKIP, ACT_CLICK}) {
|
||||||
|
try { im.deleteMapping(a); } catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
im.setCursorVisible(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final ActionListener inputListener = (name, isPressed, tpf) -> {
|
||||||
|
if (!isPressed) return;
|
||||||
|
switch (name) {
|
||||||
|
case ACT_UP -> onUp();
|
||||||
|
case ACT_DOWN -> onDown();
|
||||||
|
case ACT_CONFIRM -> onConfirm();
|
||||||
|
case ACT_SKIP -> onSkip();
|
||||||
|
case ACT_CLICK -> onMouseClick(app.getInputManager().getCursorPosition());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Hilfsmethoden ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private BitmapText makeTxt(String s, float size, ColorRGBA color) {
|
||||||
|
BitmapText t = new BitmapText(font);
|
||||||
|
t.setSize(size);
|
||||||
|
t.setColor(color);
|
||||||
|
t.setText(s);
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Geometry makeSeparator(float x, float y, float w) {
|
||||||
|
Geometry g = new Geometry("sep", new Quad(w, 1f));
|
||||||
|
Material m = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
|
||||||
|
m.setColor("Color", new ColorRGBA(0.4f, 0.4f, 0.4f, 0.8f));
|
||||||
|
m.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
|
||||||
|
g.setMaterial(m);
|
||||||
|
g.setQueueBucket(RenderQueue.Bucket.Transparent);
|
||||||
|
g.setLocalTranslation(x, y, 1f);
|
||||||
|
return g;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveNpcName(NPC npc) {
|
||||||
|
if (npc == null) return "?";
|
||||||
|
String lk = npc.getLabelKey();
|
||||||
|
if (!lk.isBlank()) {
|
||||||
|
String resolved = TextResolver.get().resolveId(lk);
|
||||||
|
if (!resolved.startsWith("[")) return resolved;
|
||||||
|
}
|
||||||
|
return npc.getCharacterId() != null ? npc.getCharacterId() : "NPC";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String t(String id) { return TextResolver.get().resolveId(id); }
|
||||||
|
|
||||||
|
/** Bricht Text an Wortgrenzen auf. */
|
||||||
|
private static List<String> wrapText(String text, int maxChars) {
|
||||||
|
List<String> lines = new ArrayList<>();
|
||||||
|
if (text == null || text.isBlank()) return lines;
|
||||||
|
// Zuerst explizite Zeilenumbrüche beachten
|
||||||
|
for (String paragraph : text.split("\n")) {
|
||||||
|
String[] words = paragraph.split("\\s+");
|
||||||
|
StringBuilder cur = new StringBuilder();
|
||||||
|
for (String w : words) {
|
||||||
|
if (w.isBlank()) continue;
|
||||||
|
if (cur.length() > 0 && cur.length() + 1 + w.length() > maxChars) {
|
||||||
|
lines.add(cur.toString());
|
||||||
|
cur = new StringBuilder(w);
|
||||||
|
} else {
|
||||||
|
if (cur.length() > 0) cur.append(' ');
|
||||||
|
cur.append(w);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (cur.length() > 0) lines.add(cur.toString());
|
||||||
|
}
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gruppiert Zeilen in Seiten der Größe {@code linesPerPage}. */
|
||||||
|
private static List<List<String>> paginate(List<String> lines, int linesPerPage) {
|
||||||
|
List<List<String>> pages = new ArrayList<>();
|
||||||
|
for (int i = 0; i < lines.size(); i += linesPerPage) {
|
||||||
|
pages.add(lines.subList(i, Math.min(i + linesPerPage, lines.size())));
|
||||||
|
}
|
||||||
|
if (pages.isEmpty()) pages.add(List.of());
|
||||||
|
return pages;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Hilfsrecord ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private record DisplayOption(String label, DialogOption option) {}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import com.jme3.math.Vector3f;
|
|||||||
import com.jme3.renderer.Camera;
|
import com.jme3.renderer.Camera;
|
||||||
import com.jme3.scene.Node;
|
import com.jme3.scene.Node;
|
||||||
import com.jme3.scene.Spatial;
|
import com.jme3.scene.Spatial;
|
||||||
|
import de.blight.lang.TextResolver;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@@ -23,9 +24,12 @@ public class InteractionHudState extends BaseAppState {
|
|||||||
|
|
||||||
private static final float Y_OFFSET = 0.6f;
|
private static final float Y_OFFSET = 0.6f;
|
||||||
|
|
||||||
private Camera cam;
|
private Camera cam;
|
||||||
private Node guiNode;
|
private Node guiNode;
|
||||||
private WorldItemsState worldItems;
|
private WorldItemsState worldItems;
|
||||||
|
private WorldInteractableState worldInteractables;
|
||||||
|
private WorldNpcsState worldNpcs;
|
||||||
|
private DialogHudState dialogHud;
|
||||||
|
|
||||||
private BitmapText labelText;
|
private BitmapText labelText;
|
||||||
|
|
||||||
@@ -47,9 +51,14 @@ public class InteractionHudState extends BaseAppState {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onEnable() {
|
protected void onEnable() {
|
||||||
worldItems = getStateManager().getState(WorldItemsState.class);
|
worldItems = getStateManager().getState(WorldItemsState.class);
|
||||||
|
worldInteractables = getStateManager().getState(WorldInteractableState.class);
|
||||||
|
worldNpcs = getStateManager().getState(WorldNpcsState.class);
|
||||||
|
dialogHud = getStateManager().getState(DialogHudState.class);
|
||||||
if (worldItems == null)
|
if (worldItems == null)
|
||||||
log.warn("[InteractionHud] WorldItemsState nicht gefunden.");
|
log.warn("[InteractionHud] WorldItemsState nicht gefunden.");
|
||||||
|
if (worldInteractables == null)
|
||||||
|
log.warn("[InteractionHud] WorldInteractableState nicht gefunden.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -66,40 +75,54 @@ public class InteractionHudState extends BaseAppState {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void update(float tpf) {
|
public void update(float tpf) {
|
||||||
if (worldItems == null) {
|
// Label ausblenden wenn Dialog aktiv
|
||||||
|
if (dialogHud != null && dialogHud.isActive()) {
|
||||||
labelText.setCullHint(Spatial.CullHint.Always);
|
labelText.setCullHint(Spatial.CullHint.Always);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int idx = worldItems.getHoveredIdx();
|
String labelKey = null;
|
||||||
if (idx < 0) {
|
Vector3f worldPos = null;
|
||||||
|
|
||||||
|
// NPC hat höchste Priorität, dann Interactable, dann Item
|
||||||
|
if (worldNpcs != null) {
|
||||||
|
String key = worldNpcs.getHoveredLabelKey();
|
||||||
|
if (key != null && !key.isBlank()) {
|
||||||
|
labelKey = key;
|
||||||
|
worldPos = worldNpcs.getHoveredWorldPos();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interactable hat Vorrang vor Item
|
||||||
|
if (labelKey == null && worldInteractables != null) {
|
||||||
|
String key = worldInteractables.getHoveredLabelKey();
|
||||||
|
if (key != null && !key.isBlank()) {
|
||||||
|
labelKey = key;
|
||||||
|
worldPos = worldInteractables.getHoveredWorldPos();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (labelKey == null && worldItems != null && worldItems.getHoveredIdx() >= 0) {
|
||||||
|
String key = worldItems.getHoveredLabelKey();
|
||||||
|
if (key != null && !key.isBlank()) {
|
||||||
|
labelKey = key;
|
||||||
|
worldPos = worldItems.getHoveredWorldPos();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (labelKey == null || worldPos == null) {
|
||||||
labelText.setCullHint(Spatial.CullHint.Always);
|
labelText.setCullHint(Spatial.CullHint.Always);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String name = worldItems.getHoveredItemName();
|
Vector3f screenV = cam.getScreenCoordinates(worldPos.add(0f, Y_OFFSET, 0f));
|
||||||
if (name == null) {
|
|
||||||
labelText.setCullHint(Spatial.CullHint.Always);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
// Hinter der Kamera → nicht anzeigen
|
|
||||||
if (screenV.z >= 1f) {
|
if (screenV.z >= 1f) {
|
||||||
labelText.setCullHint(Spatial.CullHint.Always);
|
labelText.setCullHint(Spatial.CullHint.Always);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
labelText.setText(name);
|
String text = TextResolver.get().resolveId(labelKey);
|
||||||
|
labelText.setText(text);
|
||||||
float textW = labelText.getLineWidth();
|
float textW = labelText.getLineWidth();
|
||||||
labelText.setLocalTranslation(screenV.x - textW * 0.5f, screenV.y, 1f);
|
labelText.setLocalTranslation(screenV.x - textW * 0.5f, screenV.y, 1f);
|
||||||
labelText.setCullHint(Spatial.CullHint.Inherit);
|
labelText.setCullHint(Spatial.CullHint.Inherit);
|
||||||
|
|||||||
@@ -20,9 +20,13 @@ import com.jme3.renderer.queue.RenderQueue;
|
|||||||
import com.jme3.scene.Geometry;
|
import com.jme3.scene.Geometry;
|
||||||
import com.jme3.scene.Node;
|
import com.jme3.scene.Node;
|
||||||
import com.jme3.scene.shape.Quad;
|
import com.jme3.scene.shape.Quad;
|
||||||
|
import com.jme3.post.FilterPostProcessor;
|
||||||
import com.jme3.texture.Texture;
|
import com.jme3.texture.Texture;
|
||||||
import de.blight.common.model.*;
|
import de.blight.common.model.*;
|
||||||
import de.blight.game.config.KeyBindings;
|
import de.blight.game.config.KeyBindings;
|
||||||
|
import de.blight.game.config.MenuCanvas;
|
||||||
|
import de.blight.game.config.NinePatch;
|
||||||
|
import de.blight.game.post.GaussianBlurFilter;
|
||||||
import de.blight.game.scene.WorldScene;
|
import de.blight.game.scene.WorldScene;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
@@ -82,6 +86,9 @@ public class InventoryState extends BaseAppState {
|
|||||||
private Node guiNode;
|
private Node guiNode;
|
||||||
private Node panel;
|
private Node panel;
|
||||||
private Node gridNode;
|
private Node gridNode;
|
||||||
|
private Node bgLayer;
|
||||||
|
private Node canvasNode;
|
||||||
|
private GaussianBlurFilter blurFilter;
|
||||||
|
|
||||||
// ── Daten ─────────────────────────────────────────────────────────────────
|
// ── Daten ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -137,7 +144,14 @@ public class InventoryState extends BaseAppState {
|
|||||||
app.getInputManager().addListener(scrollListener, MAP_SCROLL_UP, MAP_SCROLL_DN);
|
app.getInputManager().addListener(scrollListener, MAP_SCROLL_UP, MAP_SCROLL_DN);
|
||||||
app.getInputManager().addListener(clickListener, MAP_CLICK);
|
app.getInputManager().addListener(clickListener, MAP_CLICK);
|
||||||
WorldScene ws = app.getStateManager().getState(WorldScene.class);
|
WorldScene ws = app.getStateManager().getState(WorldScene.class);
|
||||||
if (ws != null) ws.setPaused(true);
|
if (ws != null) {
|
||||||
|
ws.setPaused(true);
|
||||||
|
FilterPostProcessor fpp = ws.getSharedFPP();
|
||||||
|
if (fpp != null) {
|
||||||
|
blurFilter = new GaussianBlurFilter(6f);
|
||||||
|
fpp.addFilter(blurFilter);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -147,7 +161,14 @@ public class InventoryState extends BaseAppState {
|
|||||||
app.getInputManager().removeListener(clickListener);
|
app.getInputManager().removeListener(clickListener);
|
||||||
app.getInputManager().setCursorVisible(false);
|
app.getInputManager().setCursorVisible(false);
|
||||||
WorldScene ws = app.getStateManager().getState(WorldScene.class);
|
WorldScene ws = app.getStateManager().getState(WorldScene.class);
|
||||||
if (ws != null) ws.setPaused(false);
|
if (ws != null) {
|
||||||
|
ws.setPaused(false);
|
||||||
|
if (blurFilter != null) {
|
||||||
|
FilterPostProcessor fpp = ws.getSharedFPP();
|
||||||
|
if (fpp != null) fpp.removeFilter(blurFilter);
|
||||||
|
blurFilter = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -173,10 +194,10 @@ public class InventoryState extends BaseAppState {
|
|||||||
|
|
||||||
private final ActionListener clickListener = (name, pressed, tpf) -> {
|
private final ActionListener clickListener = (name, pressed, tpf) -> {
|
||||||
if (!pressed || panel == null || tabBounds == null) return;
|
if (!pressed || panel == null || tabBounds == null) return;
|
||||||
Vector2f c = app.getInputManager().getCursorPosition();
|
float[] v = toVirtual(app.getInputManager().getCursorPosition());
|
||||||
for (int i = 0; i < tabBounds.length; i++) {
|
for (int i = 0; i < tabBounds.length; i++) {
|
||||||
float[] b = tabBounds[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]) {
|
if (v[0] >= b[0] && v[0] <= b[0] + b[2] && v[1] >= b[1] && v[1] <= b[1] + b[3]) {
|
||||||
switchTab(tabOrder[i]);
|
switchTab(tabOrder[i]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -186,8 +207,13 @@ public class InventoryState extends BaseAppState {
|
|||||||
// ── Haupt-Panel aufbauen ──────────────────────────────────────────────────
|
// ── Haupt-Panel aufbauen ──────────────────────────────────────────────────
|
||||||
|
|
||||||
private void buildPanel() {
|
private void buildPanel() {
|
||||||
float sw = app.getCamera().getWidth();
|
float sw = MenuCanvas.REF_W;
|
||||||
float sh = app.getCamera().getHeight();
|
float sh = MenuCanvas.REF_H;
|
||||||
|
|
||||||
|
bgLayer = MenuCanvas.createBgLayer(assetManager, app.getCamera());
|
||||||
|
canvasNode = MenuCanvas.createCanvas(app.getCamera());
|
||||||
|
guiNode.attachChild(bgLayer);
|
||||||
|
guiNode.attachChild(canvasNode);
|
||||||
|
|
||||||
// Panelgröße: 5 Spalten + Ränder
|
// Panelgröße: 5 Spalten + Ränder
|
||||||
float pw = COLS * (CELL_W + CELL_GAP) + CELL_GAP + 2 * PAD;
|
float pw = COLS * (CELL_W + CELL_GAP) + CELL_GAP + 2 * PAD;
|
||||||
@@ -196,8 +222,7 @@ public class InventoryState extends BaseAppState {
|
|||||||
float py = (sh - ph) / 2f;
|
float py = (sh - ph) / 2f;
|
||||||
|
|
||||||
panel = new Node("inv-panel");
|
panel = new Node("inv-panel");
|
||||||
quad(panel, 0, 0, sw, sh, COL_OVERLAY, -20); // Verdunkelung
|
panel.attachChild(NinePatch.panel(assetManager).build(px, py, pw, ph, -19));
|
||||||
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
|
quad(panel, px, py + ph - HDR_H, pw, HDR_H, COL_HDR, -18); // Header-Balken
|
||||||
|
|
||||||
// Titel
|
// Titel
|
||||||
@@ -213,7 +238,7 @@ public class InventoryState extends BaseAppState {
|
|||||||
// Tabs aufbauen
|
// Tabs aufbauen
|
||||||
buildTabs(px, py, pw, ph);
|
buildTabs(px, py, pw, ph);
|
||||||
|
|
||||||
guiNode.attachChild(panel);
|
canvasNode.attachChild(panel);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void buildTabs(float px, float py, float pw, float ph) {
|
private void buildTabs(float px, float py, float pw, float ph) {
|
||||||
@@ -229,7 +254,8 @@ public class InventoryState extends BaseAppState {
|
|||||||
for (int i = 0; i < tabOrder.length; i++) {
|
for (int i = 0; i < tabOrder.length; i++) {
|
||||||
boolean active = tabOrder[i] == activeTab;
|
boolean active = tabOrder[i] == activeTab;
|
||||||
float tx = tabX0 + i * tabW;
|
float tx = tabX0 + i * tabW;
|
||||||
quad(panel, tx, tabY, tabW - 4, TAB_H, active ? COL_TAB_ON : COL_TAB_OFF, -18);
|
NinePatch tabPatch = active ? NinePatch.tabActive(assetManager) : NinePatch.tabInactive(assetManager);
|
||||||
|
panel.attachChild(tabPatch.build(tx, tabY, tabW - 4, TAB_H, -18));
|
||||||
BitmapText lbl = txt(catLabel(tabOrder[i]), 13, active ? COL_WHITE : COL_MUTED);
|
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);
|
lbl.setLocalTranslation(tx + (tabW - 4 - lbl.getLineWidth()) / 2f, tabY + TAB_H - 8, -17);
|
||||||
panel.attachChild(lbl);
|
panel.attachChild(lbl);
|
||||||
@@ -341,9 +367,7 @@ public class InventoryState extends BaseAppState {
|
|||||||
// ── Einzel-Zelle ──────────────────────────────────────────────────────────
|
// ── Einzel-Zelle ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private void buildCell(Node parent, Item item, int count, float x, float y) {
|
private void buildCell(Node parent, Item item, int count, float x, float y) {
|
||||||
// Zell-Hintergrund + Rand
|
parent.attachChild(NinePatch.itemCell(assetManager).build(x, y, CELL_W, CELL_H, -18));
|
||||||
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
|
// Thumbnail
|
||||||
float thumbX = x + (CELL_W - THUMB_SZ) / 2f;
|
float thumbX = x + (CELL_W - THUMB_SZ) / 2f;
|
||||||
@@ -415,8 +439,18 @@ public class InventoryState extends BaseAppState {
|
|||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private float[] toVirtual(Vector2f screen) {
|
||||||
|
float scale = Math.min(app.getCamera().getWidth() / MenuCanvas.REF_W,
|
||||||
|
app.getCamera().getHeight() / MenuCanvas.REF_H);
|
||||||
|
float ox = (app.getCamera().getWidth() - MenuCanvas.REF_W * scale) / 2f;
|
||||||
|
float oy = (app.getCamera().getHeight() - MenuCanvas.REF_H * scale) / 2f;
|
||||||
|
return new float[]{ (screen.x - ox) / scale, (screen.y - oy) / scale };
|
||||||
|
}
|
||||||
|
|
||||||
private void destroyPanel() {
|
private void destroyPanel() {
|
||||||
if (panel != null) { guiNode.detachChild(panel); panel = null; gridNode = null; }
|
if (bgLayer != null) { guiNode.detachChild(bgLayer); bgLayer = null; }
|
||||||
|
if (canvasNode != null) { guiNode.detachChild(canvasNode); canvasNode = null; }
|
||||||
|
panel = null; gridNode = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Geometry quad(Node parent, float x, float y, float w, float h, ColorRGBA col, float z) {
|
private Geometry quad(Node parent, float x, float y, float w, float h, ColorRGBA col, float z) {
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
package de.blight.game.state;
|
||||||
|
|
||||||
|
import com.jme3.app.Application;
|
||||||
|
import com.jme3.app.SimpleApplication;
|
||||||
|
import com.jme3.app.state.BaseAppState;
|
||||||
|
import com.jme3.material.Material;
|
||||||
|
import com.jme3.material.RenderState;
|
||||||
|
import com.jme3.math.ColorRGBA;
|
||||||
|
import com.jme3.renderer.queue.RenderQueue;
|
||||||
|
import com.jme3.scene.Geometry;
|
||||||
|
import com.jme3.scene.shape.Quad;
|
||||||
|
import de.blight.game.control.PlayerInputControl;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Neues-Spiel-Intro: Schwarzer Bildschirm → Einblenden (Ton + Bild) → REVIVE-Animation → IDLE.
|
||||||
|
*
|
||||||
|
* Phasen:
|
||||||
|
* BLACK (3 s) – voller schwarzer Schirm, keine Eingaben (IDLE läuft, nicht sichtbar)
|
||||||
|
* FADING_IN (3 s) – Alpha und Listener-Lautstärke 0→1
|
||||||
|
* REVIVE_PLAYING – schwarzer Schirm weg, Animation läuft durch
|
||||||
|
* DONE – Eingaben freigegeben, State entfernt sich selbst
|
||||||
|
*/
|
||||||
|
public class NewGameIntroState extends BaseAppState {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(NewGameIntroState.class);
|
||||||
|
|
||||||
|
private static final float BLACK_DURATION = 3f;
|
||||||
|
private static final float FADE_DURATION = 3f;
|
||||||
|
|
||||||
|
private enum Phase { BLACK, FADING_IN, REVIVE_PLAYING, DONE }
|
||||||
|
|
||||||
|
private final PlayerInputControl playerInput;
|
||||||
|
private final String reviveClip;
|
||||||
|
private final float reviveLength;
|
||||||
|
|
||||||
|
private SimpleApplication app;
|
||||||
|
private Geometry overlay;
|
||||||
|
private Material overlayMat;
|
||||||
|
|
||||||
|
private Phase phase;
|
||||||
|
private float timer;
|
||||||
|
|
||||||
|
public NewGameIntroState(PlayerInputControl playerInput, String reviveClip, float reviveLength) {
|
||||||
|
this.playerInput = playerInput;
|
||||||
|
this.reviveClip = reviveClip;
|
||||||
|
this.reviveLength = reviveLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initialize(Application application) {
|
||||||
|
app = (SimpleApplication) application;
|
||||||
|
|
||||||
|
float w = app.getCamera().getWidth();
|
||||||
|
float h = app.getCamera().getHeight();
|
||||||
|
Quad quad = new Quad(w, h);
|
||||||
|
overlay = new Geometry("intro_overlay", quad);
|
||||||
|
overlayMat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
|
||||||
|
overlayMat.setColor("Color", new ColorRGBA(0f, 0f, 0f, 1f));
|
||||||
|
overlayMat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
|
||||||
|
overlay.setMaterial(overlayMat);
|
||||||
|
overlay.setQueueBucket(RenderQueue.Bucket.Gui);
|
||||||
|
app.getGuiNode().attachChild(overlay);
|
||||||
|
|
||||||
|
app.getListener().setVolume(0f);
|
||||||
|
|
||||||
|
if (reviveClip != null) {
|
||||||
|
playerInput.blockForIntro();
|
||||||
|
phase = Phase.BLACK;
|
||||||
|
timer = BLACK_DURATION;
|
||||||
|
log.info("[NewGameIntro] Gestartet – REVIVE='{}' ({} s), Inputs blockiert", reviveClip, reviveLength);
|
||||||
|
} else {
|
||||||
|
log.warn("[NewGameIntro] Kein REVIVE-Clip – Intro übersprungen.");
|
||||||
|
app.getGuiNode().detachChild(overlay);
|
||||||
|
app.getListener().setVolume(1f);
|
||||||
|
playerInput.unblockInputs();
|
||||||
|
phase = Phase.DONE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void update(float tpf) {
|
||||||
|
switch (phase) {
|
||||||
|
case BLACK -> {
|
||||||
|
timer -= tpf;
|
||||||
|
if (timer <= 0f) {
|
||||||
|
// REVIVE unmittelbar vor dem Einblenden einfrieren
|
||||||
|
playerInput.startFrozenRevive(reviveClip);
|
||||||
|
phase = Phase.FADING_IN;
|
||||||
|
timer = FADE_DURATION;
|
||||||
|
log.info("[NewGameIntro] BLACK fertig – REVIVE eingefroren, starte FADING_IN ({} s)", FADE_DURATION);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case FADING_IN -> {
|
||||||
|
timer -= tpf;
|
||||||
|
float alpha = Math.max(0f, timer / FADE_DURATION);
|
||||||
|
overlayMat.setColor("Color", new ColorRGBA(0f, 0f, 0f, alpha));
|
||||||
|
app.getListener().setVolume(1f - alpha);
|
||||||
|
|
||||||
|
if (timer <= 0f) {
|
||||||
|
app.getGuiNode().detachChild(overlay);
|
||||||
|
overlay = null;
|
||||||
|
app.getListener().setVolume(1f);
|
||||||
|
playerInput.unfreezeRevive();
|
||||||
|
timer = reviveLength;
|
||||||
|
phase = Phase.REVIVE_PLAYING;
|
||||||
|
log.info("[NewGameIntro] Eingeblendet – REVIVE läuft ({} s)", reviveLength);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case REVIVE_PLAYING -> {
|
||||||
|
timer -= tpf;
|
||||||
|
if (timer <= 0f) {
|
||||||
|
playerInput.unblockInputs();
|
||||||
|
phase = Phase.DONE;
|
||||||
|
log.info("[NewGameIntro] REVIVE abgeschlossen – Eingaben freigegeben");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case DONE -> getApplication().getStateManager().detach(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void cleanup(Application application) {
|
||||||
|
if (overlay != null && overlay.getParent() != null) {
|
||||||
|
app.getGuiNode().detachChild(overlay);
|
||||||
|
}
|
||||||
|
app.getListener().setVolume(1f);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override protected void onEnable() {}
|
||||||
|
@Override protected void onDisable() {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
package de.blight.game.state;
|
||||||
|
|
||||||
|
import com.jme3.app.Application;
|
||||||
|
import com.jme3.app.SimpleApplication;
|
||||||
|
import com.jme3.app.state.BaseAppState;
|
||||||
|
import com.jme3.audio.AudioData;
|
||||||
|
import com.jme3.audio.AudioNode;
|
||||||
|
import com.jme3.math.Vector3f;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spielt Wellensounds positional ab. Die AudioNodes werden EINMALIG nach dem
|
||||||
|
* ersten gültigen Scan positioniert (vermeidet den initialen Sprung von (0,0,0)
|
||||||
|
* zur Ozean-Kante, der OpenAL-Knistern verursacht). Danach wird die Position
|
||||||
|
* nur noch alle SCAN_INTERVAL Sekunden aktualisiert, wenn sich der Ozean
|
||||||
|
* signifikant verschoben hat.
|
||||||
|
*/
|
||||||
|
public class OceanSoundState extends BaseAppState {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(OceanSoundState.class);
|
||||||
|
|
||||||
|
private static final float MAX_DIST = 60f;
|
||||||
|
private static final float REF_DIST = 8f;
|
||||||
|
private static final float WIND_THRESHOLD = 20f;
|
||||||
|
private static final float FADE_RATE = 1f / 3f;
|
||||||
|
private static final float SCAN_INTERVAL = 0.25f;
|
||||||
|
private static final float SCAN_STEP = 5f;
|
||||||
|
/** Mindestverschiebung (m²) bevor die Node-Position aktualisiert wird. */
|
||||||
|
private static final float POS_UPDATE_SQ = 4f * 4f; // 4 Meter
|
||||||
|
|
||||||
|
private static final float[] DIR_X = { 0f, 0.707f, 1f, 0.707f, 0f, -0.707f, -1f, -0.707f };
|
||||||
|
private static final float[] DIR_Z = { 1f, 0.707f, 0f, -0.707f, -1f, -0.707f, 0f, 0.707f };
|
||||||
|
|
||||||
|
private final TerrainChunkState terrain;
|
||||||
|
|
||||||
|
private SimpleApplication app;
|
||||||
|
private AudioNode nodeCalmSound;
|
||||||
|
private AudioNode nodeStormySound;
|
||||||
|
|
||||||
|
private final Vector3f playerPos = new Vector3f();
|
||||||
|
private final Vector3f targetPos = new Vector3f();
|
||||||
|
private final Vector3f nodePos = new Vector3f(); // zuletzt gesetzte Node-Position
|
||||||
|
|
||||||
|
/** true bis der erste gültige Scan die Nodes positioniert und abgespielt hat. */
|
||||||
|
private boolean firstScan = true;
|
||||||
|
private boolean playing = false;
|
||||||
|
|
||||||
|
private float calmVol = 0f;
|
||||||
|
private float stormyVol = 0f;
|
||||||
|
private float scanTimer = 0f;
|
||||||
|
private boolean oceanInRange = false;
|
||||||
|
private float oceanDist = Float.MAX_VALUE;
|
||||||
|
|
||||||
|
public OceanSoundState(TerrainChunkState terrain) {
|
||||||
|
this.terrain = terrain;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initialize(Application application) {
|
||||||
|
app = (SimpleApplication) application;
|
||||||
|
nodeCalmSound = loadLoop("audio/ambient/water/waves_calm.ogg", "waves_calm");
|
||||||
|
nodeStormySound = loadLoop("audio/ambient/water/waves_stormy.ogg", "waves_stormy");
|
||||||
|
// Noch NICHT abspielen – erst wenn wir eine gültige Position haben (firstScan).
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void cleanup(Application application) {
|
||||||
|
stop(nodeCalmSound);
|
||||||
|
stop(nodeStormySound);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override protected void onEnable() {}
|
||||||
|
@Override protected void onDisable() {}
|
||||||
|
|
||||||
|
public void setPlayerPosition(Vector3f pos) {
|
||||||
|
playerPos.set(pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void update(float tpf) {
|
||||||
|
if (nodeCalmSound == null && nodeStormySound == null) return;
|
||||||
|
|
||||||
|
// Periodisch nächste Ozean-Position berechnen
|
||||||
|
scanTimer -= tpf;
|
||||||
|
if (scanTimer <= 0f) {
|
||||||
|
scanTimer = SCAN_INTERVAL;
|
||||||
|
scanOceanSource();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lautstärke
|
||||||
|
WeatherState weather = getApplication().getStateManager().getState(WeatherState.class);
|
||||||
|
float wind = weather != null ? weather.getWindSpeed() : 4f;
|
||||||
|
|
||||||
|
AudioSettingsState audioSettings = getApplication().getStateManager().getState(AudioSettingsState.class);
|
||||||
|
float scale = audioSettings != null ? audioSettings.effectiveAmbient() : 0.5f;
|
||||||
|
|
||||||
|
float distScale = oceanInRange
|
||||||
|
? Math.max(0f, 1f - Math.max(0f, oceanDist - REF_DIST) / (MAX_DIST - REF_DIST))
|
||||||
|
: 0f;
|
||||||
|
|
||||||
|
float calmTarget = (oceanInRange && wind < WIND_THRESHOLD) ? scale * distScale : 0f;
|
||||||
|
float stormyTarget = (oceanInRange && wind >= WIND_THRESHOLD) ? scale * distScale : 0f;
|
||||||
|
|
||||||
|
calmVol = approach(calmVol, calmTarget, FADE_RATE * tpf);
|
||||||
|
stormyVol = approach(stormyVol, stormyTarget, FADE_RATE * tpf);
|
||||||
|
|
||||||
|
if (nodeCalmSound != null) nodeCalmSound.setVolume(calmVol);
|
||||||
|
if (nodeStormySound != null) nodeStormySound.setVolume(stormyVol);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Scan ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void scanOceanSource() {
|
||||||
|
float px = playerPos.x;
|
||||||
|
float pz = playerPos.z;
|
||||||
|
|
||||||
|
if (terrain.getHeightAt(px, pz) < 0f) {
|
||||||
|
oceanInRange = true;
|
||||||
|
oceanDist = 0f;
|
||||||
|
applyTarget(px, pz);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
float minDist = Float.MAX_VALUE;
|
||||||
|
float bestX = px, bestZ = pz;
|
||||||
|
|
||||||
|
for (int d = 0; d < 8; d++) {
|
||||||
|
float dx = DIR_X[d];
|
||||||
|
float dz = DIR_Z[d];
|
||||||
|
for (float r = SCAN_STEP; r <= MAX_DIST + SCAN_STEP; r += SCAN_STEP) {
|
||||||
|
if (terrain.getHeightAt(px + dx * r, pz + dz * r) < 0f) {
|
||||||
|
float dist = r - SCAN_STEP;
|
||||||
|
if (dist < minDist) {
|
||||||
|
minDist = dist;
|
||||||
|
bestX = px + dx * r;
|
||||||
|
bestZ = pz + dz * r;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (minDist >= MAX_DIST) {
|
||||||
|
oceanInRange = false;
|
||||||
|
oceanDist = Float.MAX_VALUE;
|
||||||
|
} else {
|
||||||
|
oceanInRange = true;
|
||||||
|
oceanDist = minDist;
|
||||||
|
applyTarget(bestX, bestZ);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zielposition setzen. Beim ersten Scan: Nodes positionieren und Wiedergabe starten.
|
||||||
|
* Danach: Node nur aktualisieren wenn Verschiebung > POS_UPDATE_SQ.
|
||||||
|
*/
|
||||||
|
private void applyTarget(float x, float z) {
|
||||||
|
targetPos.set(x, 0f, z);
|
||||||
|
|
||||||
|
if (firstScan) {
|
||||||
|
// Beim ersten gültigen Scan: Nodes korrekt platzieren, dann erst abspielen.
|
||||||
|
firstScan = false;
|
||||||
|
nodePos.set(targetPos);
|
||||||
|
if (nodeCalmSound != null) {
|
||||||
|
app.getRootNode().attachChild(nodeCalmSound);
|
||||||
|
nodeCalmSound.setLocalTranslation(nodePos);
|
||||||
|
nodeCalmSound.play();
|
||||||
|
}
|
||||||
|
if (nodeStormySound != null) {
|
||||||
|
app.getRootNode().attachChild(nodeStormySound);
|
||||||
|
nodeStormySound.setLocalTranslation(nodePos);
|
||||||
|
nodeStormySound.play();
|
||||||
|
}
|
||||||
|
playing = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!playing) return;
|
||||||
|
|
||||||
|
// Nur aktualisieren wenn sich die Zielposition signifikant geändert hat
|
||||||
|
float dxSq = (targetPos.x - nodePos.x);
|
||||||
|
float dzSq = (targetPos.z - nodePos.z);
|
||||||
|
if (dxSq * dxSq + dzSq * dzSq < POS_UPDATE_SQ) return;
|
||||||
|
|
||||||
|
nodePos.set(targetPos);
|
||||||
|
if (nodeCalmSound != null) nodeCalmSound.setLocalTranslation(nodePos);
|
||||||
|
if (nodeStormySound != null) nodeStormySound.setLocalTranslation(nodePos);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Hilfsmethoden ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private AudioNode loadLoop(String path, String label) {
|
||||||
|
try {
|
||||||
|
AudioNode n = new AudioNode(app.getAssetManager(), path, AudioData.DataType.Stream);
|
||||||
|
n.setLooping(true);
|
||||||
|
n.setVolume(0f);
|
||||||
|
n.setPositional(true);
|
||||||
|
n.setRefDistance(REF_DIST);
|
||||||
|
n.setMaxDistance(MAX_DIST);
|
||||||
|
return n;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[OceanSound] {} nicht ladbar: {}", label, e.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stop(AudioNode node) {
|
||||||
|
if (node != null) {
|
||||||
|
node.stop();
|
||||||
|
if (node.getParent() != null) app.getRootNode().detachChild(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float approach(float cur, float target, float maxStep) {
|
||||||
|
float d = target - cur;
|
||||||
|
return Math.abs(d) <= maxStep ? target : cur + Math.signum(d) * maxStep;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import com.jme3.input.controls.ActionListener;
|
|||||||
import com.jme3.input.controls.KeyTrigger;
|
import com.jme3.input.controls.KeyTrigger;
|
||||||
import com.jme3.input.controls.MouseButtonTrigger;
|
import com.jme3.input.controls.MouseButtonTrigger;
|
||||||
import com.jme3.math.Vector3f;
|
import com.jme3.math.Vector3f;
|
||||||
|
import com.jme3.renderer.Camera;
|
||||||
import com.jme3.scene.Spatial;
|
import com.jme3.scene.Spatial;
|
||||||
import de.blight.common.PlacedModel;
|
import de.blight.common.PlacedModel;
|
||||||
import de.blight.common.PlacedModelIO;
|
import de.blight.common.PlacedModelIO;
|
||||||
@@ -82,7 +83,8 @@ public class WorldInteractableState extends BaseAppState {
|
|||||||
private record InteractableEntry(
|
private record InteractableEntry(
|
||||||
float worldX, float worldY, float worldZ,
|
float worldX, float worldY, float worldZ,
|
||||||
InteractableType type,
|
InteractableType type,
|
||||||
String interactableId
|
String interactableId,
|
||||||
|
String labelKey
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private final List<InteractableEntry> entries = new ArrayList<>();
|
private final List<InteractableEntry> entries = new ArrayList<>();
|
||||||
@@ -98,6 +100,9 @@ public class WorldInteractableState extends BaseAppState {
|
|||||||
WALKING_BACK
|
WALKING_BACK
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Camera cam;
|
||||||
|
private int hoveredIdx = -1;
|
||||||
|
|
||||||
private Phase phase = Phase.IDLE;
|
private Phase phase = Phase.IDLE;
|
||||||
private int targetIdx = -1;
|
private int targetIdx = -1;
|
||||||
private float walkTimer = 0f;
|
private float walkTimer = 0f;
|
||||||
@@ -122,13 +127,20 @@ public class WorldInteractableState extends BaseAppState {
|
|||||||
@Override
|
@Override
|
||||||
protected void initialize(Application app) {
|
protected void initialize(Application app) {
|
||||||
this.inputManager = app.getInputManager();
|
this.inputManager = app.getInputManager();
|
||||||
|
this.cam = app.getCamera();
|
||||||
try {
|
try {
|
||||||
List<PlacedModel> models = PlacedModelIO.load();
|
List<PlacedModel> models = PlacedModelIO.load();
|
||||||
for (PlacedModel m : models) {
|
for (PlacedModel m : models) {
|
||||||
if (m.interactableType() == null || m.interactableType().isBlank()) continue;
|
if (m.interactableType() == null || m.interactableType().isBlank()) continue;
|
||||||
InteractableType t = InteractableType.fromString(m.interactableType());
|
InteractableType t = InteractableType.fromString(m.interactableType());
|
||||||
if (t == InteractableType.BED || t == InteractableType.BENCH) {
|
if (t == InteractableType.BENCH) {
|
||||||
entries.add(new InteractableEntry(m.x(), m.y(), m.z(), t, m.interactableId()));
|
Bench b = BenchIO.load(m.interactableId()).orElse(null);
|
||||||
|
String lk = b != null ? b.getLabelKey() : "interactable.bench.name";
|
||||||
|
entries.add(new InteractableEntry(m.x(), m.y(), m.z(), t, m.interactableId(), lk));
|
||||||
|
} else if (t == InteractableType.BED) {
|
||||||
|
Bed b = BedIO.load(m.interactableId()).orElse(null);
|
||||||
|
String lk = b != null ? b.getLabelKey() : "interactable.bed.name";
|
||||||
|
entries.add(new InteractableEntry(m.x(), m.y(), m.z(), t, m.interactableId(), lk));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.info("[WorldInteractable] {} Interactables geladen.", entries.size());
|
log.info("[WorldInteractable] {} Interactables geladen.", entries.size());
|
||||||
@@ -158,6 +170,7 @@ public class WorldInteractableState extends BaseAppState {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void update(float tpf) {
|
public void update(float tpf) {
|
||||||
|
updateHover();
|
||||||
if (phase == Phase.WALKING_BACK) walkTimer += tpf;
|
if (phase == Phase.WALKING_BACK) walkTimer += tpf;
|
||||||
if (benchPendingId != null) {
|
if (benchPendingId != null) {
|
||||||
benchPendingTimer += tpf;
|
benchPendingTimer += tpf;
|
||||||
@@ -183,6 +196,58 @@ public class WorldInteractableState extends BaseAppState {
|
|||||||
startGetUp();
|
startGetUp();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ── Hover-Erkennung ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void updateHover() {
|
||||||
|
if (phase != Phase.IDLE || cam == null) {
|
||||||
|
hoveredIdx = -1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector3f charPos = physicsChar.getPhysicsLocation();
|
||||||
|
float centerX = cam.getWidth() * 0.5f;
|
||||||
|
float hMargin = cam.getWidth() * 0.10f;
|
||||||
|
float screenH = cam.getHeight();
|
||||||
|
|
||||||
|
int bestIdx = -1;
|
||||||
|
float bestDist = Float.MAX_VALUE;
|
||||||
|
|
||||||
|
for (int i = 0; i < entries.size(); i++) {
|
||||||
|
InteractableEntry e = entries.get(i);
|
||||||
|
float dx = e.worldX() - charPos.x;
|
||||||
|
float dz = e.worldZ() - charPos.z;
|
||||||
|
float d = (float) Math.sqrt(dx * dx + dz * dz);
|
||||||
|
float range = (e.type() == InteractableType.BENCH) ? BENCH_RANGE : BED_RANGE;
|
||||||
|
if (d > range) continue;
|
||||||
|
|
||||||
|
Vector3f worldPos = new Vector3f(e.worldX(), e.worldY(), e.worldZ());
|
||||||
|
Vector3f camToEntry = worldPos.subtract(cam.getLocation());
|
||||||
|
if (camToEntry.dot(cam.getDirection()) <= 0f) continue;
|
||||||
|
|
||||||
|
Vector3f screen = cam.getScreenCoordinates(worldPos);
|
||||||
|
if (Math.abs(screen.x - centerX) > hMargin) continue;
|
||||||
|
if (screen.y < 0f || screen.y > screenH) continue;
|
||||||
|
|
||||||
|
if (d < bestDist) {
|
||||||
|
bestDist = d;
|
||||||
|
bestIdx = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hoveredIdx = bestIdx;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getHoveredLabelKey() {
|
||||||
|
if (hoveredIdx < 0 || hoveredIdx >= entries.size()) return null;
|
||||||
|
return entries.get(hoveredIdx).labelKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vector3f getHoveredWorldPos() {
|
||||||
|
if (hoveredIdx < 0 || hoveredIdx >= entries.size()) return null;
|
||||||
|
InteractableEntry e = entries.get(hoveredIdx);
|
||||||
|
return new Vector3f(e.worldX(), e.worldY(), e.worldZ());
|
||||||
|
}
|
||||||
|
|
||||||
// ── Suche nächstes Interactable ───────────────────────────────────────────
|
// ── Suche nächstes Interactable ───────────────────────────────────────────
|
||||||
|
|
||||||
private int findNearestInRange() {
|
private int findNearestInRange() {
|
||||||
|
|||||||
@@ -342,6 +342,19 @@ public class WorldItemsState extends BaseAppState {
|
|||||||
return def != null ? def.getDisplayText() : pi.itemId();
|
return def != null ? def.getDisplayText() : pi.itemId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getHoveredLabelKey() {
|
||||||
|
if (hoveredIdx < 0 || hoveredIdx >= items.size()) return null;
|
||||||
|
PlacedItem pi = items.get(hoveredIdx);
|
||||||
|
Item def = itemDefs.get(pi.itemId());
|
||||||
|
return def != null ? def.getLabelKey() : (pi.itemId() != null ? "item." + pi.itemId() + ".name" : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vector3f getHoveredWorldPos() {
|
||||||
|
if (hoveredIdx < 0 || hoveredIdx >= items.size()) return null;
|
||||||
|
PlacedItem pi = items.get(hoveredIdx);
|
||||||
|
return new Vector3f(pi.x(), pi.y() + 0.25f, pi.z());
|
||||||
|
}
|
||||||
|
|
||||||
// ── Pickup-Sequenz ────────────────────────────────────────────────────────
|
// ── Pickup-Sequenz ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private void updateWalking(float tpf) {
|
private void updateWalking(float tpf) {
|
||||||
|
|||||||
@@ -0,0 +1,392 @@
|
|||||||
|
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.asset.plugins.FileLocator;
|
||||||
|
import com.jme3.bullet.control.CharacterControl;
|
||||||
|
import com.jme3.input.controls.ActionListener;
|
||||||
|
import com.jme3.input.controls.KeyTrigger;
|
||||||
|
import com.jme3.material.Material;
|
||||||
|
import com.jme3.math.*;
|
||||||
|
import com.jme3.renderer.Camera;
|
||||||
|
import com.jme3.scene.*;
|
||||||
|
import com.jme3.scene.shape.Box;
|
||||||
|
import de.blight.common.PlacedModel;
|
||||||
|
import de.blight.common.PlacedModelIO;
|
||||||
|
import de.blight.common.model.*;
|
||||||
|
import de.blight.game.animation.AnimationLibrary;
|
||||||
|
import de.blight.game.config.KeyBindings;
|
||||||
|
import de.blight.game.control.PlayerInputControl;
|
||||||
|
import de.blight.game.state.TerrainChunkState;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Platziert NPCs in der Spielwelt anhand ihrer Tagesablauf-Routinen.
|
||||||
|
*
|
||||||
|
* Jeder NPC hat eine aktive Routine mit zeitgebundenen Aktivitäten.
|
||||||
|
* Der State ermittelt fortlaufend die Soll-Position jedes NPCs für die aktuelle
|
||||||
|
* Spielstunde und lädt/entlädt den visuellen Repräsentanten je nach Spieler-Nähe.
|
||||||
|
*/
|
||||||
|
public class WorldNpcsState extends BaseAppState {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(WorldNpcsState.class);
|
||||||
|
|
||||||
|
private static final float LOAD_RADIUS = 80f;
|
||||||
|
private static final float UNLOAD_RADIUS = 100f;
|
||||||
|
private static final float CHECK_INTERVAL = 2f;
|
||||||
|
private static final float INTERACT_RANGE = 4f;
|
||||||
|
private static final String INTERACT_ACTION = "InteractNpc";
|
||||||
|
|
||||||
|
// ── Abhängigkeiten ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private final KeyBindings keyBindings;
|
||||||
|
private final CharacterControl physicsChar;
|
||||||
|
private final PlayerInputControl playerInput;
|
||||||
|
private final MainCharacter mainCharacter;
|
||||||
|
|
||||||
|
private SimpleApplication app;
|
||||||
|
private AssetManager assets;
|
||||||
|
private Camera cam;
|
||||||
|
private Node rootNode;
|
||||||
|
private Node npcsRoot;
|
||||||
|
private DayNightState dayNight;
|
||||||
|
private TerrainChunkState terrainChunks;
|
||||||
|
|
||||||
|
// ── NPC-Daten ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Alle NPCs die im Spiel existieren (geladen aus .character-Dateien). */
|
||||||
|
private final List<NPC> allNpcs = new ArrayList<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Position-Lookup für objekt-gebundene Aktivitäten (WORK, SLEEP, SIT@Interactable).
|
||||||
|
* Schlüssel: interactableId des PlacedModel (= UUID der Bank/Bett/etc.).
|
||||||
|
*/
|
||||||
|
private final Map<String, Vector3f> interactablePositions = new HashMap<>();
|
||||||
|
|
||||||
|
/** Aktuell in der Welt gespawnte NPCs. */
|
||||||
|
private final List<SpawnedNpc> spawned = new ArrayList<>();
|
||||||
|
|
||||||
|
private record SpawnedNpc(NPC npc, Spatial visual, float worldX, float worldZ) {}
|
||||||
|
|
||||||
|
// ── Periodischer Check ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private float checkTimer = CHECK_INTERVAL; // sofortiger erster Check
|
||||||
|
private int lastHour = -1;
|
||||||
|
|
||||||
|
// ── Hover + Dialog ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private int hoveredIdx = -1;
|
||||||
|
private boolean dialogActive = false;
|
||||||
|
|
||||||
|
// ── Konstruktor ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public WorldNpcsState(KeyBindings keyBindings, CharacterControl physicsChar,
|
||||||
|
PlayerInputControl playerInput, MainCharacter mainCharacter) {
|
||||||
|
this.keyBindings = keyBindings;
|
||||||
|
this.physicsChar = physicsChar;
|
||||||
|
this.playerInput = playerInput;
|
||||||
|
this.mainCharacter = mainCharacter;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initialize(Application app) {
|
||||||
|
this.app = (SimpleApplication) app;
|
||||||
|
this.assets = app.getAssetManager();
|
||||||
|
this.cam = app.getCamera();
|
||||||
|
this.rootNode = this.app.getRootNode();
|
||||||
|
this.npcsRoot = new Node("npcsRoot");
|
||||||
|
|
||||||
|
try {
|
||||||
|
assets.registerLocator(
|
||||||
|
AnimationLibrary.findAssetRoot().toAbsolutePath().toString(),
|
||||||
|
FileLocator.class);
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onEnable() {
|
||||||
|
dayNight = getStateManager().getState(DayNightState.class);
|
||||||
|
terrainChunks = getStateManager().getState(TerrainChunkState.class);
|
||||||
|
|
||||||
|
loadAllNpcs();
|
||||||
|
buildInteractablePositions();
|
||||||
|
|
||||||
|
rootNode.attachChild(npcsRoot);
|
||||||
|
|
||||||
|
app.getInputManager().addMapping(INTERACT_ACTION, new KeyTrigger(keyBindings.interact));
|
||||||
|
app.getInputManager().addListener(interactListener, INTERACT_ACTION);
|
||||||
|
|
||||||
|
checkTimer = CHECK_INTERVAL; // sofortiger erster Check im nächsten Frame
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDisable() {
|
||||||
|
allNpcs.clear();
|
||||||
|
interactablePositions.clear();
|
||||||
|
spawned.clear();
|
||||||
|
npcsRoot.detachAllChildren();
|
||||||
|
npcsRoot.removeFromParent();
|
||||||
|
hoveredIdx = -1;
|
||||||
|
dialogActive = false;
|
||||||
|
try { app.getInputManager().removeListener(interactListener); } catch (Exception ignored) {}
|
||||||
|
try { app.getInputManager().deleteMapping(INTERACT_ACTION); } catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void cleanup(Application app) {}
|
||||||
|
|
||||||
|
// ── Update ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void update(float tpf) {
|
||||||
|
checkTimer += tpf;
|
||||||
|
|
||||||
|
int currentHour = dayNight != null ? dayNight.getDayTime().getHour() : 12;
|
||||||
|
boolean hourChanged = currentHour != lastHour;
|
||||||
|
|
||||||
|
if (checkTimer >= CHECK_INTERVAL || hourChanged) {
|
||||||
|
checkTimer = 0f;
|
||||||
|
lastHour = currentHour;
|
||||||
|
updateSpawnedNpcs(currentHour);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dialogActive) {
|
||||||
|
updateHover();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Spawn-Logik ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void updateSpawnedNpcs(int currentHour) {
|
||||||
|
Vector3f playerPos = physicsChar.getPhysicsLocation();
|
||||||
|
|
||||||
|
// Bestehende NPCs prüfen: Position aktualisieren oder entladen
|
||||||
|
List<SpawnedNpc> toRemove = new ArrayList<>();
|
||||||
|
for (SpawnedNpc s : spawned) {
|
||||||
|
Vector3f pos = resolveRoutinePosition(s.npc(), currentHour);
|
||||||
|
float dist = pos != null ? dist2d(playerPos, pos) : Float.MAX_VALUE;
|
||||||
|
|
||||||
|
if (pos == null || dist > UNLOAD_RADIUS) {
|
||||||
|
s.visual().removeFromParent();
|
||||||
|
toRemove.add(s);
|
||||||
|
} else {
|
||||||
|
// Position aktualisieren wenn sich die Stunde geändert hat
|
||||||
|
s.visual().setLocalTranslation(pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
spawned.removeAll(toRemove);
|
||||||
|
|
||||||
|
// IDs der bereits gespawnten NPCs für schnelle Prüfung
|
||||||
|
Set<String> spawnedIds = new HashSet<>();
|
||||||
|
for (SpawnedNpc s : spawned) {
|
||||||
|
spawnedIds.add(s.npc().getCharacterId());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Neue NPCs einladen wenn in Reichweite
|
||||||
|
for (NPC npc : allNpcs) {
|
||||||
|
if (spawnedIds.contains(npc.getCharacterId())) continue;
|
||||||
|
Vector3f pos = resolveRoutinePosition(npc, currentHour);
|
||||||
|
if (pos == null) continue;
|
||||||
|
if (dist2d(playerPos, pos) > LOAD_RADIUS) continue;
|
||||||
|
|
||||||
|
Spatial vis = buildVisual(npc);
|
||||||
|
vis.setLocalTranslation(pos);
|
||||||
|
npcsRoot.attachChild(vis);
|
||||||
|
spawned.add(new SpawnedNpc(npc, vis, pos.x, pos.z));
|
||||||
|
log.debug("[WorldNpcs] NPC '{}' gespawnt bei ({}, {})", npc.getCharacterId(), pos.x, pos.z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ermittelt die Weltposition eines NPCs für die angegebene Stunde
|
||||||
|
* anhand seiner aktiven Routine.
|
||||||
|
* Gibt null zurück wenn keine Position ermittelbar (keine Routine, keine Aktivität,
|
||||||
|
* oder Aktivitäts-Objekt nicht gefunden).
|
||||||
|
*/
|
||||||
|
private Vector3f resolveRoutinePosition(NPC npc, int hour) {
|
||||||
|
NpcRoutine routine = npc.getActiveRoutine();
|
||||||
|
if (routine == null || routine.getBlocks() == null) return null;
|
||||||
|
|
||||||
|
RoutineBlock block = null;
|
||||||
|
for (RoutineBlock b : routine.getBlocks()) {
|
||||||
|
if (b.covers(hour)) { block = b; break; }
|
||||||
|
}
|
||||||
|
if (block == null || block.getActivity() == null) return null;
|
||||||
|
|
||||||
|
RoutineActivity act = block.getActivity();
|
||||||
|
return switch (act.getType()) {
|
||||||
|
case STAND, TALK -> worldPointToVec(act.getPosition());
|
||||||
|
case SIT -> {
|
||||||
|
if (act.getObjectUuid() != null && !act.getObjectUuid().isBlank()) {
|
||||||
|
yield interactablePositions.get(act.getObjectUuid());
|
||||||
|
}
|
||||||
|
yield worldPointToVec(act.getPosition());
|
||||||
|
}
|
||||||
|
case PATROL -> {
|
||||||
|
List<WorldPoint> wps = act.getWaypoints();
|
||||||
|
if (wps == null || wps.isEmpty()) yield null;
|
||||||
|
// Einfache Näherung: ersten Wegpunkt nehmen
|
||||||
|
// TODO: NPC entlang der Wegpunkte animieren
|
||||||
|
yield worldPointToVec(wps.get(0));
|
||||||
|
}
|
||||||
|
case WORK, SLEEP -> {
|
||||||
|
if (act.getObjectUuid() == null || act.getObjectUuid().isBlank()) yield null;
|
||||||
|
yield interactablePositions.get(act.getObjectUuid());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Hover-Erkennung ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void updateHover() {
|
||||||
|
if (cam == null) { hoveredIdx = -1; return; }
|
||||||
|
|
||||||
|
Vector3f charPos = physicsChar.getPhysicsLocation();
|
||||||
|
float centerX = cam.getWidth() * 0.5f;
|
||||||
|
float hMargin = cam.getWidth() * 0.10f;
|
||||||
|
float screenH = cam.getHeight();
|
||||||
|
|
||||||
|
int bestIdx = -1;
|
||||||
|
float bestDist = Float.MAX_VALUE;
|
||||||
|
|
||||||
|
for (int i = 0; i < spawned.size(); i++) {
|
||||||
|
SpawnedNpc s = spawned.get(i);
|
||||||
|
Vector3f pos = s.visual().getLocalTranslation();
|
||||||
|
|
||||||
|
float dx = pos.x - charPos.x;
|
||||||
|
float dz = pos.z - charPos.z;
|
||||||
|
float d = (float) Math.sqrt(dx * dx + dz * dz);
|
||||||
|
if (d > INTERACT_RANGE) continue;
|
||||||
|
|
||||||
|
Vector3f headPos = pos.add(0f, 1.5f, 0f);
|
||||||
|
Vector3f camToHead = headPos.subtract(cam.getLocation());
|
||||||
|
if (camToHead.dot(cam.getDirection()) <= 0f) continue;
|
||||||
|
|
||||||
|
Vector3f screen = cam.getScreenCoordinates(headPos);
|
||||||
|
if (Math.abs(screen.x - centerX) > hMargin) continue;
|
||||||
|
if (screen.y < 0f || screen.y > screenH) continue;
|
||||||
|
|
||||||
|
if (d < bestDist) { bestDist = d; bestIdx = i; }
|
||||||
|
}
|
||||||
|
|
||||||
|
hoveredIdx = bestIdx;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Listener ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private final ActionListener interactListener = (name, isPressed, tpf) -> {
|
||||||
|
if (!isPressed || dialogActive || hoveredIdx < 0) return;
|
||||||
|
startDialog(hoveredIdx);
|
||||||
|
};
|
||||||
|
|
||||||
|
private void startDialog(int idx) {
|
||||||
|
SpawnedNpc entry = spawned.get(idx);
|
||||||
|
DialogHudState dialog = getApplication().getStateManager().getState(DialogHudState.class);
|
||||||
|
if (dialog == null) return;
|
||||||
|
|
||||||
|
dialogActive = true;
|
||||||
|
playerInput.lockInPlace();
|
||||||
|
|
||||||
|
dialog.startDialog(entry.npc(), mainCharacter, () -> {
|
||||||
|
dialogActive = false;
|
||||||
|
playerInput.unlockFromPlace();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Accessor für InteractionHudState ──────────────────────────────────────
|
||||||
|
|
||||||
|
public String getHoveredLabelKey() {
|
||||||
|
if (hoveredIdx < 0 || hoveredIdx >= spawned.size()) return null;
|
||||||
|
return spawned.get(hoveredIdx).npc().getLabelKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vector3f getHoveredWorldPos() {
|
||||||
|
if (hoveredIdx < 0 || hoveredIdx >= spawned.size()) return null;
|
||||||
|
Vector3f pos = spawned.get(hoveredIdx).visual().getLocalTranslation();
|
||||||
|
return pos.add(0f, 1.5f, 0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Hilfsmethoden ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void loadAllNpcs() {
|
||||||
|
allNpcs.clear();
|
||||||
|
try {
|
||||||
|
java.nio.file.Path charDir = AnimationLibrary.findAssetRoot().resolve("character");
|
||||||
|
for (GameCharacter gc : CharacterIO.loadAll(charDir)) {
|
||||||
|
if (gc instanceof NPC npc) {
|
||||||
|
allNpcs.add(npc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.info("[WorldNpcs] {} NPCs mit Routinen geladen.", allNpcs.size());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[WorldNpcs] Fehler beim Laden der NPCs: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void buildInteractablePositions() {
|
||||||
|
interactablePositions.clear();
|
||||||
|
try {
|
||||||
|
List<PlacedModel> models = PlacedModelIO.load();
|
||||||
|
for (PlacedModel m : models) {
|
||||||
|
String id = m.interactableId();
|
||||||
|
if (id != null && !id.isBlank()) {
|
||||||
|
interactablePositions.put(id, new Vector3f(m.x(), m.y(), m.z()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.debug("[WorldNpcs] {} Interactable-Positionen indexiert.", interactablePositions.size());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[WorldNpcs] Fehler beim Laden der Objekt-Positionen: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vector3f worldPointToVec(WorldPoint p) {
|
||||||
|
if (p == null) return null;
|
||||||
|
float y = p.y;
|
||||||
|
// y == 0 bedeutet "Terrain-Höhe" → per TerrainChunkState auflösen
|
||||||
|
if (y == 0f && terrainChunks != null) {
|
||||||
|
y = terrainChunks.getHeightAt(p.x, p.z);
|
||||||
|
}
|
||||||
|
return new Vector3f(p.x, y, p.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float dist2d(Vector3f a, Vector3f b) {
|
||||||
|
float dx = a.x - b.x;
|
||||||
|
float dz = a.z - b.z;
|
||||||
|
return (float) Math.sqrt(dx * dx + dz * dz);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Spatial buildVisual(NPC npc) {
|
||||||
|
String modelPath = npc.getModelPath();
|
||||||
|
if (modelPath != null && !modelPath.isBlank()) {
|
||||||
|
try {
|
||||||
|
Spatial model = assets.loadModel(modelPath);
|
||||||
|
model.setName("npc_" + npc.getCharacterId());
|
||||||
|
return model;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[WorldNpcs] Modell '{}' nicht ladbar – Platzhalter.", modelPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Platzhalter: Körper + Kopf
|
||||||
|
Node ph = new Node("npc_" + npc.getCharacterId());
|
||||||
|
Material bodyMat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||||
|
bodyMat.setColor("Color", new ColorRGBA(0.5f, 0.7f, 1.0f, 1f));
|
||||||
|
Geometry body = new Geometry("body", new Box(0.25f, 0.6f, 0.25f));
|
||||||
|
body.setMaterial(bodyMat);
|
||||||
|
body.setLocalTranslation(0f, 0.85f, 0f);
|
||||||
|
Material headMat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||||
|
headMat.setColor("Color", new ColorRGBA(1.0f, 0.85f, 0.7f, 1f));
|
||||||
|
Geometry head = new Geometry("head", new Box(0.2f, 0.2f, 0.2f));
|
||||||
|
head.setMaterial(headMat);
|
||||||
|
head.setLocalTranslation(0f, 1.7f, 0f);
|
||||||
|
ph.attachChild(body);
|
||||||
|
ph.attachChild(head);
|
||||||
|
return ph;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,13 @@
|
|||||||
|
# ── Dialog ───────────────────────────────────────────────────────────────────
|
||||||
|
dialog.exit=Gespräch beenden
|
||||||
|
dialog.hint.advance=► Rechtsklick für nächste Seite
|
||||||
|
dialog.hint.continue=► Rechtsklick zum Weiter
|
||||||
|
dialog.speaker.player=Spieler
|
||||||
|
|
||||||
|
# ── Interactables ─────────────────────────────────────────────────────────────
|
||||||
|
interactable.bench.name=Bank
|
||||||
|
interactable.bed.name=Bett
|
||||||
|
|
||||||
# ── Items ────────────────────────────────────────────────────────────────────
|
# ── Items ────────────────────────────────────────────────────────────────────
|
||||||
item.driftwood.name=Treibholz
|
item.driftwood.name=Treibholz
|
||||||
item.driftwood.description=Ein ans Ufer gespültes Stück Holz. Könnte nützlich sein.
|
item.driftwood.description=Ein ans Ufer gespültes Stück Holz. Könnte nützlich sein.
|
||||||
@@ -16,3 +26,56 @@ quest.intro.success=Du hast einen Unterschlupf gefunden. Ruh dich aus — morgen
|
|||||||
# ── Dialog ───────────────────────────────────────────────────────────────────
|
# ── Dialog ───────────────────────────────────────────────────────────────────
|
||||||
dialog.fisherman.greet.hero=Hallo? Ist da jemand?
|
dialog.fisherman.greet.hero=Hallo? Ist da jemand?
|
||||||
dialog.fisherman.greet.npc=Hier drüben! Dem Himmel sei Dank, du lebst noch.
|
dialog.fisherman.greet.npc=Hier drüben! Dem Himmel sei Dank, du lebst noch.
|
||||||
|
|
||||||
|
# ── Hauptmenü ────────────────────────────────────────────────────────────────
|
||||||
|
menu.main.btn.new_game=Neues Spiel
|
||||||
|
menu.main.btn.continue=Fortsetzen
|
||||||
|
menu.main.btn.load=Spiel laden
|
||||||
|
menu.main.btn.options=Optionen
|
||||||
|
menu.main.btn.quit=Beenden
|
||||||
|
|
||||||
|
# ── Pausemenü ────────────────────────────────────────────────────────────────
|
||||||
|
menu.pause.title=PAUSE
|
||||||
|
menu.pause.btn.graphics=Grafik
|
||||||
|
menu.pause.btn.audio=Audio
|
||||||
|
menu.pause.btn.controls=Steuerung
|
||||||
|
menu.pause.btn.save=Speichern
|
||||||
|
menu.pause.btn.quit=Beenden
|
||||||
|
|
||||||
|
# ── Grafikeinstellungen ───────────────────────────────────────────────────────
|
||||||
|
menu.graphics.title=GRAFIKEINSTELLUNGEN
|
||||||
|
menu.graphics.row.resolution=Auflösung
|
||||||
|
menu.graphics.row.fullscreen=Vollbild
|
||||||
|
menu.graphics.row.vsync=VSync
|
||||||
|
menu.graphics.row.aa=Kantenglättung
|
||||||
|
menu.graphics.val.on=An
|
||||||
|
menu.graphics.val.off=Aus
|
||||||
|
menu.graphics.btn.apply=Übernehmen
|
||||||
|
menu.graphics.btn.cancel=Abbrechen
|
||||||
|
|
||||||
|
# ── Audioeinstellungen ────────────────────────────────────────────────────────
|
||||||
|
menu.audio.title=AUDIOEINSTELLUNGEN
|
||||||
|
menu.audio.row.master=Gesamtlautstärke
|
||||||
|
menu.audio.row.music=Musik
|
||||||
|
menu.audio.row.speech=Sprache
|
||||||
|
menu.audio.row.effects=Effekte
|
||||||
|
menu.audio.row.ambient=Ambient
|
||||||
|
menu.audio.btn.apply=Übernehmen
|
||||||
|
menu.audio.btn.cancel=Abbrechen
|
||||||
|
|
||||||
|
# ── Tastenbelegung ────────────────────────────────────────────────────────────
|
||||||
|
menu.controls.title=TASTENBELEGUNG
|
||||||
|
menu.controls.hint=Klicke eine Taste um sie neu zu belegen
|
||||||
|
menu.controls.btn.save=Speichern
|
||||||
|
menu.controls.btn.cancel=Abbrechen
|
||||||
|
|
||||||
|
key.forward=Vorwärts
|
||||||
|
key.backward=Rückwärts
|
||||||
|
key.left=Links
|
||||||
|
key.right=Rechts
|
||||||
|
key.jump=Springen
|
||||||
|
key.sprint=Rennen
|
||||||
|
key.walk=Gehen
|
||||||
|
key.interact=Interagieren
|
||||||
|
key.inventory=Inventar
|
||||||
|
key.quicksave=Schnellspeichern
|
||||||
|
|||||||
@@ -1,3 +1,13 @@
|
|||||||
|
# ── Dialog ───────────────────────────────────────────────────────────────────
|
||||||
|
dialog.exit=End conversation
|
||||||
|
dialog.hint.advance=► Right-click for next page
|
||||||
|
dialog.hint.continue=► Right-click to continue
|
||||||
|
dialog.speaker.player=Player
|
||||||
|
|
||||||
|
# ── Interactables ─────────────────────────────────────────────────────────────
|
||||||
|
interactable.bench.name=Bench
|
||||||
|
interactable.bed.name=Bed
|
||||||
|
|
||||||
# ── Items ────────────────────────────────────────────────────────────────────
|
# ── Items ────────────────────────────────────────────────────────────────────
|
||||||
item.driftwood.name=Driftwood
|
item.driftwood.name=Driftwood
|
||||||
item.driftwood.description=A piece of wood washed ashore. Could be useful.
|
item.driftwood.description=A piece of wood washed ashore. Could be useful.
|
||||||
@@ -16,3 +26,56 @@ quest.intro.success=You have found shelter. Rest now — tomorrow brings new dan
|
|||||||
# ── Dialog ───────────────────────────────────────────────────────────────────
|
# ── Dialog ───────────────────────────────────────────────────────────────────
|
||||||
dialog.fisherman.greet.hero=Hello? Is someone there?
|
dialog.fisherman.greet.hero=Hello? Is someone there?
|
||||||
dialog.fisherman.greet.npc=Over here! Thank the gods you are alive.
|
dialog.fisherman.greet.npc=Over here! Thank the gods you are alive.
|
||||||
|
|
||||||
|
# ── Main Menu ─────────────────────────────────────────────────────────────────
|
||||||
|
menu.main.btn.new_game=New Game
|
||||||
|
menu.main.btn.continue=Continue
|
||||||
|
menu.main.btn.load=Load Game
|
||||||
|
menu.main.btn.options=Options
|
||||||
|
menu.main.btn.quit=Quit
|
||||||
|
|
||||||
|
# ── Pause Menu ────────────────────────────────────────────────────────────────
|
||||||
|
menu.pause.title=PAUSE
|
||||||
|
menu.pause.btn.graphics=Graphics
|
||||||
|
menu.pause.btn.audio=Audio
|
||||||
|
menu.pause.btn.controls=Controls
|
||||||
|
menu.pause.btn.save=Save
|
||||||
|
menu.pause.btn.quit=Quit
|
||||||
|
|
||||||
|
# ── Graphics Settings ─────────────────────────────────────────────────────────
|
||||||
|
menu.graphics.title=GRAPHICS SETTINGS
|
||||||
|
menu.graphics.row.resolution=Resolution
|
||||||
|
menu.graphics.row.fullscreen=Fullscreen
|
||||||
|
menu.graphics.row.vsync=VSync
|
||||||
|
menu.graphics.row.aa=Anti-aliasing
|
||||||
|
menu.graphics.val.on=On
|
||||||
|
menu.graphics.val.off=Off
|
||||||
|
menu.graphics.btn.apply=Apply
|
||||||
|
menu.graphics.btn.cancel=Cancel
|
||||||
|
|
||||||
|
# ── Audio Settings ────────────────────────────────────────────────────────────
|
||||||
|
menu.audio.title=AUDIO SETTINGS
|
||||||
|
menu.audio.row.master=Master Volume
|
||||||
|
menu.audio.row.music=Music
|
||||||
|
menu.audio.row.speech=Speech
|
||||||
|
menu.audio.row.effects=Effects
|
||||||
|
menu.audio.row.ambient=Ambient
|
||||||
|
menu.audio.btn.apply=Apply
|
||||||
|
menu.audio.btn.cancel=Cancel
|
||||||
|
|
||||||
|
# ── Controls ─────────────────────────────────────────────────────────────────
|
||||||
|
menu.controls.title=CONTROLS
|
||||||
|
menu.controls.hint=Click a key to rebind it
|
||||||
|
menu.controls.btn.save=Save
|
||||||
|
menu.controls.btn.cancel=Cancel
|
||||||
|
|
||||||
|
key.forward=Forward
|
||||||
|
key.backward=Backward
|
||||||
|
key.left=Left
|
||||||
|
key.right=Right
|
||||||
|
key.jump=Jump
|
||||||
|
key.sprint=Sprint
|
||||||
|
key.walk=Walk
|
||||||
|
key.interact=Interact
|
||||||
|
key.inventory=Inventory
|
||||||
|
key.quicksave=Quick Save
|
||||||
|
|||||||
@@ -1,4 +1 @@
|
|||||||
# x y z r g b intensity radius
|
# x y z r g b intensity radius
|
||||||
138.57341 1.10000 209.24336 0.80000 1.00000 0.80000 12.00000 50.00000
|
|
||||||
131.40373 1.10000 196.12698 0.80000 1.00000 0.80000 12.00000 50.00000
|
|
||||||
143.96100 1.10000 194.25346 0.80000 1.00000 0.80000 12.00000 50.00000
|
|
||||||
|
|||||||
@@ -2,7 +2,4 @@
|
|||||||
Models/imported/wooden+cabin+3d+model.j3o 105.61634 4.85395 58.69108 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
|
Models/imported/wooden+cabin+3d+model.j3o 105.61634 4.85395 58.69108 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
|
||||||
Models/trees/pine/medium/pine_medium_20260615_221703.j3o 96.87341 6.11813 51.96483 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
|
Models/trees/pine/medium/pine_medium_20260615_221703.j3o 96.87341 6.11813 51.96483 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
|
||||||
Models/trees/pine/medium/pine_medium_20260615_221707.j3o 110.91007 4.33700 50.56980 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
|
Models/trees/pine/medium/pine_medium_20260615_221707.j3o 110.91007 4.33700 50.56980 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
|
||||||
Models/imported/Höhlenkristall1.j3o 138.57341 1.00000 209.24336 0.64251 1.00000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000
|
|
||||||
Models/imported/Höhlenkristall1.j3o 131.40373 1.00000 196.12698 3.95943 1.00000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000
|
|
||||||
Models/imported/Höhlenkristall1.j3o 143.96100 1.00000 194.25346 4.02822 1.00000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000
|
|
||||||
Models/imported/bank1.j3o 106.96240 2.89958 70.01086 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 BENCH 5be97ffb-e413-42e5-9815-9d14d1e3b93f
|
Models/imported/bank1.j3o 106.96240 2.89958 70.01086 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 BENCH 5be97ffb-e413-42e5-9815-9d14d1e3b93f
|
||||||
|
|||||||