diff --git a/assets/imported/animations/sitting_cycle.glb b/assets/imported/animations/sitting_cycle.glb new file mode 100644 index 0000000..9e1d252 Binary files /dev/null and b/assets/imported/animations/sitting_cycle.glb differ diff --git a/blight-assets/src/main/resources/MatDefs/GaussianBlur.j3md b/blight-assets/src/main/resources/MatDefs/GaussianBlur.j3md new file mode 100644 index 0000000..8248b5b --- /dev/null +++ b/blight-assets/src/main/resources/MatDefs/GaussianBlur.j3md @@ -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 { + } + } +} diff --git a/blight-assets/src/main/resources/Shaders/GaussianBlur.frag b/blight-assets/src/main/resources/Shaders/GaussianBlur.frag new file mode 100644 index 0000000..93749bb --- /dev/null +++ b/blight-assets/src/main/resources/Shaders/GaussianBlur.frag @@ -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 +} diff --git a/blight-assets/src/main/resources/Textures/menu/background.png b/blight-assets/src/main/resources/Textures/menu/background.png new file mode 100644 index 0000000..06ca8d4 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/menu/background.png differ diff --git a/blight-assets/src/main/resources/Textures/menu/button.png b/blight-assets/src/main/resources/Textures/menu/button.png new file mode 100644 index 0000000..21513a4 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/menu/button.png differ diff --git a/blight-assets/src/main/resources/Textures/menu/button_arrow.png b/blight-assets/src/main/resources/Textures/menu/button_arrow.png new file mode 100644 index 0000000..598a8d2 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/menu/button_arrow.png differ diff --git a/blight-assets/src/main/resources/Textures/menu/button_disabled.png b/blight-assets/src/main/resources/Textures/menu/button_disabled.png new file mode 100644 index 0000000..1deab84 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/menu/button_disabled.png differ diff --git a/blight-assets/src/main/resources/Textures/menu/button_quit.png b/blight-assets/src/main/resources/Textures/menu/button_quit.png new file mode 100644 index 0000000..047bb42 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/menu/button_quit.png differ diff --git a/blight-assets/src/main/resources/Textures/menu/button_save.png b/blight-assets/src/main/resources/Textures/menu/button_save.png new file mode 100644 index 0000000..a7c1c98 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/menu/button_save.png differ diff --git a/blight-assets/src/main/resources/Textures/menu/item_cell.png b/blight-assets/src/main/resources/Textures/menu/item_cell.png new file mode 100644 index 0000000..fd261c4 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/menu/item_cell.png differ diff --git a/blight-assets/src/main/resources/Textures/menu/panel.png b/blight-assets/src/main/resources/Textures/menu/panel.png new file mode 100644 index 0000000..10f4e15 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/menu/panel.png differ diff --git a/blight-assets/src/main/resources/Textures/menu/tab_active.png b/blight-assets/src/main/resources/Textures/menu/tab_active.png new file mode 100644 index 0000000..3745055 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/menu/tab_active.png differ diff --git a/blight-assets/src/main/resources/Textures/menu/tab_inactive.png b/blight-assets/src/main/resources/Textures/menu/tab_inactive.png new file mode 100644 index 0000000..9fee774 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/menu/tab_inactive.png differ diff --git a/blight-assets/src/main/resources/animations/clips/Armature.001|Armature|Action.j3o b/blight-assets/src/main/resources/animations/clips/Armature.001|Armature|Action.j3o new file mode 100644 index 0000000..7f0a803 Binary files /dev/null and b/blight-assets/src/main/resources/animations/clips/Armature.001|Armature|Action.j3o differ diff --git a/blight-assets/src/main/resources/animations/clips/Armature.002|Armature|Action.j3o b/blight-assets/src/main/resources/animations/clips/Armature.002|Armature|Action.j3o new file mode 100644 index 0000000..30433a9 Binary files /dev/null and b/blight-assets/src/main/resources/animations/clips/Armature.002|Armature|Action.j3o differ diff --git a/blight-assets/src/main/resources/animations/clips/Armature|Armature|Action.j3o b/blight-assets/src/main/resources/animations/clips/Armature|Armature|Action.j3o new file mode 100644 index 0000000..8349e7e Binary files /dev/null and b/blight-assets/src/main/resources/animations/clips/Armature|Armature|Action.j3o differ diff --git a/blight-assets/src/main/resources/animations/clips/sit_down_bench.j3o b/blight-assets/src/main/resources/animations/clips/sit_down_bench.j3o deleted file mode 100644 index 5eccd21..0000000 Binary files a/blight-assets/src/main/resources/animations/clips/sit_down_bench.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/animations/clips/sitting_bench.j3o b/blight-assets/src/main/resources/animations/clips/sitting_bench.j3o deleted file mode 100644 index 13a7ac5..0000000 Binary files a/blight-assets/src/main/resources/animations/clips/sitting_bench.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/animations/clips/stand_up_bench.j3o b/blight-assets/src/main/resources/animations/clips/stand_up_bench.j3o deleted file mode 100644 index 45c4e30..0000000 Binary files a/blight-assets/src/main/resources/animations/clips/stand_up_bench.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/audio/ambient/water/waves_calm.ogg b/blight-assets/src/main/resources/audio/ambient/water/waves_calm.ogg new file mode 100644 index 0000000..6e67512 Binary files /dev/null and b/blight-assets/src/main/resources/audio/ambient/water/waves_calm.ogg differ diff --git a/blight-assets/src/main/resources/audio/ambient/water/waves_stormy.ogg b/blight-assets/src/main/resources/audio/ambient/water/waves_stormy.ogg new file mode 100644 index 0000000..ad89f40 Binary files /dev/null and b/blight-assets/src/main/resources/audio/ambient/water/waves_stormy.ogg differ diff --git a/blight-assets/src/main/resources/audio/footsteps/dirt/dirt1.ogg b/blight-assets/src/main/resources/audio/footsteps/dirt/dirt1.ogg new file mode 100644 index 0000000..335aa58 Binary files /dev/null and b/blight-assets/src/main/resources/audio/footsteps/dirt/dirt1.ogg differ diff --git a/blight-assets/src/main/resources/audio/footsteps/dirt/dirt2.ogg b/blight-assets/src/main/resources/audio/footsteps/dirt/dirt2.ogg new file mode 100644 index 0000000..d109904 Binary files /dev/null and b/blight-assets/src/main/resources/audio/footsteps/dirt/dirt2.ogg differ diff --git a/blight-assets/src/main/resources/audio/footsteps/dirt/dirt3.ogg b/blight-assets/src/main/resources/audio/footsteps/dirt/dirt3.ogg new file mode 100644 index 0000000..31224e1 Binary files /dev/null and b/blight-assets/src/main/resources/audio/footsteps/dirt/dirt3.ogg differ diff --git a/blight-assets/src/main/resources/audio/footsteps/dirt/dirt4.ogg b/blight-assets/src/main/resources/audio/footsteps/dirt/dirt4.ogg new file mode 100644 index 0000000..9a09d38 Binary files /dev/null and b/blight-assets/src/main/resources/audio/footsteps/dirt/dirt4.ogg differ diff --git a/blight-assets/src/main/resources/audio/footsteps/grass/gras1.ogg b/blight-assets/src/main/resources/audio/footsteps/grass/gras1.ogg new file mode 100644 index 0000000..e63c7a7 Binary files /dev/null and b/blight-assets/src/main/resources/audio/footsteps/grass/gras1.ogg differ diff --git a/blight-assets/src/main/resources/audio/footsteps/grass/gras2.ogg b/blight-assets/src/main/resources/audio/footsteps/grass/gras2.ogg new file mode 100644 index 0000000..1378c87 Binary files /dev/null and b/blight-assets/src/main/resources/audio/footsteps/grass/gras2.ogg differ diff --git a/blight-assets/src/main/resources/audio/footsteps/grass/gras3.ogg b/blight-assets/src/main/resources/audio/footsteps/grass/gras3.ogg new file mode 100644 index 0000000..49ef050 Binary files /dev/null and b/blight-assets/src/main/resources/audio/footsteps/grass/gras3.ogg differ diff --git a/blight-assets/src/main/resources/audio/footsteps/grass/gras4.ogg b/blight-assets/src/main/resources/audio/footsteps/grass/gras4.ogg new file mode 100644 index 0000000..aa4d4a5 Binary files /dev/null and b/blight-assets/src/main/resources/audio/footsteps/grass/gras4.ogg differ diff --git a/blight-assets/src/main/resources/audio/footsteps/rock/rock1.ogg b/blight-assets/src/main/resources/audio/footsteps/rock/rock1.ogg new file mode 100644 index 0000000..e1e5e89 Binary files /dev/null and b/blight-assets/src/main/resources/audio/footsteps/rock/rock1.ogg differ diff --git a/blight-assets/src/main/resources/audio/footsteps/rock/rock2.ogg b/blight-assets/src/main/resources/audio/footsteps/rock/rock2.ogg new file mode 100644 index 0000000..710cd7c Binary files /dev/null and b/blight-assets/src/main/resources/audio/footsteps/rock/rock2.ogg differ diff --git a/blight-assets/src/main/resources/audio/footsteps/rock/rock3.ogg b/blight-assets/src/main/resources/audio/footsteps/rock/rock3.ogg new file mode 100644 index 0000000..1021d0b Binary files /dev/null and b/blight-assets/src/main/resources/audio/footsteps/rock/rock3.ogg differ diff --git a/blight-assets/src/main/resources/audio/footsteps/rock/rock4.ogg b/blight-assets/src/main/resources/audio/footsteps/rock/rock4.ogg new file mode 100644 index 0000000..026a147 Binary files /dev/null and b/blight-assets/src/main/resources/audio/footsteps/rock/rock4.ogg differ diff --git a/blight-assets/src/main/resources/audio/footsteps/sand/sand1.ogg b/blight-assets/src/main/resources/audio/footsteps/sand/sand1.ogg new file mode 100644 index 0000000..b4f048b Binary files /dev/null and b/blight-assets/src/main/resources/audio/footsteps/sand/sand1.ogg differ diff --git a/blight-assets/src/main/resources/audio/footsteps/sand/sand2.ogg b/blight-assets/src/main/resources/audio/footsteps/sand/sand2.ogg new file mode 100644 index 0000000..e97160d Binary files /dev/null and b/blight-assets/src/main/resources/audio/footsteps/sand/sand2.ogg differ diff --git a/blight-assets/src/main/resources/audio/footsteps/sand/sand3.ogg b/blight-assets/src/main/resources/audio/footsteps/sand/sand3.ogg new file mode 100644 index 0000000..a6e0411 Binary files /dev/null and b/blight-assets/src/main/resources/audio/footsteps/sand/sand3.ogg differ diff --git a/blight-assets/src/main/resources/audio/footsteps/sand/sand4.ogg b/blight-assets/src/main/resources/audio/footsteps/sand/sand4.ogg new file mode 100644 index 0000000..2bd07e9 Binary files /dev/null and b/blight-assets/src/main/resources/audio/footsteps/sand/sand4.ogg differ diff --git a/blight-assets/src/main/resources/audio/footsteps/sand/sand5.ogg b/blight-assets/src/main/resources/audio/footsteps/sand/sand5.ogg new file mode 100644 index 0000000..cff90cb Binary files /dev/null and b/blight-assets/src/main/resources/audio/footsteps/sand/sand5.ogg differ diff --git a/blight-assets/src/main/resources/audio/footsteps/steps1.ogg b/blight-assets/src/main/resources/audio/footsteps/steps1.ogg new file mode 100644 index 0000000..756f559 Binary files /dev/null and b/blight-assets/src/main/resources/audio/footsteps/steps1.ogg differ diff --git a/blight-assets/src/main/resources/audio/footsteps/steps2.ogg b/blight-assets/src/main/resources/audio/footsteps/steps2.ogg new file mode 100644 index 0000000..ae7149d Binary files /dev/null and b/blight-assets/src/main/resources/audio/footsteps/steps2.ogg differ diff --git a/blight-assets/src/main/resources/audio/footsteps/steps3.ogg b/blight-assets/src/main/resources/audio/footsteps/steps3.ogg new file mode 100644 index 0000000..0b351e4 Binary files /dev/null and b/blight-assets/src/main/resources/audio/footsteps/steps3.ogg differ diff --git a/blight-assets/src/main/resources/audio/footsteps/steps4.ogg b/blight-assets/src/main/resources/audio/footsteps/steps4.ogg new file mode 100644 index 0000000..7d11663 Binary files /dev/null and b/blight-assets/src/main/resources/audio/footsteps/steps4.ogg differ diff --git a/blight-assets/src/main/resources/audio/footsteps/wood/wood1.ogg b/blight-assets/src/main/resources/audio/footsteps/wood/wood1.ogg new file mode 100644 index 0000000..27cfea6 Binary files /dev/null and b/blight-assets/src/main/resources/audio/footsteps/wood/wood1.ogg differ diff --git a/blight-assets/src/main/resources/audio/footsteps/wood/wood2.ogg b/blight-assets/src/main/resources/audio/footsteps/wood/wood2.ogg new file mode 100644 index 0000000..2377d7b Binary files /dev/null and b/blight-assets/src/main/resources/audio/footsteps/wood/wood2.ogg differ diff --git a/blight-assets/src/main/resources/audio/footsteps/wood/wood3.ogg b/blight-assets/src/main/resources/audio/footsteps/wood/wood3.ogg new file mode 100644 index 0000000..891b1dd Binary files /dev/null and b/blight-assets/src/main/resources/audio/footsteps/wood/wood3.ogg differ diff --git a/blight-assets/src/main/resources/audio/footsteps/wood/wood4.ogg b/blight-assets/src/main/resources/audio/footsteps/wood/wood4.ogg new file mode 100644 index 0000000..7034f8b Binary files /dev/null and b/blight-assets/src/main/resources/audio/footsteps/wood/wood4.ogg differ diff --git a/blight-assets/src/main/resources/character/silas.character b/blight-assets/src/main/resources/character/silas.character index be7453b..0825d78 100644 --- a/blight-assets/src/main/resources/character/silas.character +++ b/blight-assets/src/main/resources/character/silas.character @@ -1,5 +1,54 @@ { + "status": "NEUTRAL", "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", "name": { "id": "silas.name" diff --git a/blight-assets/src/main/resources/localization/de.json b/blight-assets/src/main/resources/localization/de.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/blight-assets/src/main/resources/localization/de.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/blight-assets/src/main/resources/quests/strandgut.quest b/blight-assets/src/main/resources/quests/strandgut.quest new file mode 100644 index 0000000..cedef06 --- /dev/null +++ b/blight-assets/src/main/resources/quests/strandgut.quest @@ -0,0 +1,15 @@ +{ + "count": 1, + "xp": 100, + "questId": "strandgut", + "text": { + "id": "strandgut.name" + }, + "description": { + "id": "strandgut.description" + }, + "successText": { + "id": "strandgut.successmassage" + }, + "type": "ITEM" +} \ No newline at end of file diff --git a/blight-common/src/main/java/de/blight/common/MapData.java b/blight-common/src/main/java/de/blight/common/MapData.java index ba53137..51c745c 100644 --- a/blight-common/src/main/java/de/blight/common/MapData.java +++ b/blight-common/src/main/java/de/blight/common/MapData.java @@ -129,6 +129,9 @@ public final class MapData { /** Spawnpunkt Z-Koordinate in Welteinheiten (default: 0 = Kartenmitte). */ 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. */ public int voxelFlatSlot = -1; /** Terrain-Slot (0-7) für steile Wände (TexSteep), -1 = nicht gesetzt. */ diff --git a/blight-common/src/main/java/de/blight/common/MapIO.java b/blight-common/src/main/java/de/blight/common/MapIO.java index b41c1b7..5dd907c 100644 --- a/blight-common/src/main/java/de/blight/common/MapIO.java +++ b/blight-common/src/main/java/de/blight/common/MapIO.java @@ -63,7 +63,7 @@ public final class MapIO { } 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 private static final int OLD_TERRAIN_VERTS = 4097; @@ -124,6 +124,8 @@ public final class MapIO { // v4: spawnpunkt out.writeFloat(data.spawnX); out.writeFloat(data.spawnZ); + // v15: spawn-blickrichtung + out.writeFloat(data.spawnYaw); // v5: splatA + texturpfade + gebirge-splatmap out.write(data.splatA); writeStrings(out, data.terrainTextures); @@ -232,6 +234,9 @@ public final class MapIO { data.spawnX = in.readFloat(); data.spawnZ = in.readFloat(); } + if (version >= 15) { + data.spawnYaw = in.readFloat(); + } if (version >= 5) { if (version >= 10) { in.readFully(data.splatA); diff --git a/blight-common/src/main/java/de/blight/common/ModelMeta.java b/blight-common/src/main/java/de/blight/common/ModelMeta.java index 69949b9..5870f18 100644 --- a/blight-common/src/main/java/de/blight/common/ModelMeta.java +++ b/blight-common/src/main/java/de/blight/common/ModelMeta.java @@ -32,7 +32,8 @@ public record ModelMeta( float interactableOffsetX, float interactableOffsetY, float interactableOffsetZ, - float interactableRotY + float interactableRotY, + String footstepSurface ) { /** Lichtquelle relativ zum Modell-Ursprung. */ public record AttachedLight( @@ -52,6 +53,6 @@ public record ModelMeta( "", "", 30f, 80f, 120f, List.of(), List.of(), de.blight.common.model.InteractableType.NONE, - 0f, 0.5f, 0f, 0f); + 0f, 0.5f, 0f, 0f, ""); } } diff --git a/blight-common/src/main/java/de/blight/common/ModelMetaIO.java b/blight-common/src/main/java/de/blight/common/ModelMetaIO.java index 9e1cef8..dd0426b 100644 --- a/blight-common/src/main/java/de/blight/common/ModelMetaIO.java +++ b/blight-common/src/main/java/de/blight/common/ModelMetaIO.java @@ -39,6 +39,7 @@ public final class ModelMetaIO { p.setProperty("interactableOffsetY", String.valueOf(m.interactableOffsetY())); p.setProperty("interactableOffsetZ", String.valueOf(m.interactableOffsetZ())); p.setProperty("interactableRotY", String.valueOf(m.interactableRotY())); + p.setProperty("footstepSurface", m.footstepSurface() != null ? m.footstepSurface() : ""); // Anhänge: Lichter List lights = m.attachedLights(); @@ -138,7 +139,8 @@ public final class ModelMetaIO { parseFloat(p, "interactableOffsetX", 0f), parseFloat(p, "interactableOffsetY", 0.5f), parseFloat(p, "interactableOffsetZ", 0f), - parseFloat(p, "interactableRotY", 0f) + parseFloat(p, "interactableRotY", 0f), + p.getProperty("footstepSurface", "") ); } diff --git a/blight-common/src/main/java/de/blight/common/model/Bed.java b/blight-common/src/main/java/de/blight/common/model/Bed.java index 0e6e2d9..3b4fcfd 100644 --- a/blight-common/src/main/java/de/blight/common/model/Bed.java +++ b/blight-common/src/main/java/de/blight/common/model/Bed.java @@ -40,4 +40,10 @@ public class Bed implements Interactable { public String getDisplayText() { 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"; + } } diff --git a/blight-common/src/main/java/de/blight/common/model/Bench.java b/blight-common/src/main/java/de/blight/common/model/Bench.java index 38150e3..344adb6 100644 --- a/blight-common/src/main/java/de/blight/common/model/Bench.java +++ b/blight-common/src/main/java/de/blight/common/model/Bench.java @@ -41,4 +41,10 @@ public class Bench implements Interactable { public String getDisplayText() { 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"; + } } diff --git a/blight-common/src/main/java/de/blight/common/model/Interactable.java b/blight-common/src/main/java/de/blight/common/model/Interactable.java index b8b01db..b70c6d2 100644 --- a/blight-common/src/main/java/de/blight/common/model/Interactable.java +++ b/blight-common/src/main/java/de/blight/common/model/Interactable.java @@ -2,5 +2,8 @@ package de.blight.common.model; 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 ""; } } diff --git a/blight-common/src/main/java/de/blight/common/model/Item.java b/blight-common/src/main/java/de/blight/common/model/Item.java index dab00f7..298c900 100644 --- a/blight-common/src/main/java/de/blight/common/model/Item.java +++ b/blight-common/src/main/java/de/blight/common/model/Item.java @@ -30,4 +30,10 @@ public class Item implements Interactable { public String getDisplayText() { 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" : ""; + } } diff --git a/blight-common/src/main/java/de/blight/common/model/NPC.java b/blight-common/src/main/java/de/blight/common/model/NPC.java index c1e9f82..8add92e 100644 --- a/blight-common/src/main/java/de/blight/common/model/NPC.java +++ b/blight-common/src/main/java/de/blight/common/model/NPC.java @@ -1,7 +1,9 @@ package de.blight.common.model; import java.util.ArrayList; +import java.util.EnumMap; import java.util.List; +import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -11,7 +13,7 @@ import lombok.Setter; @Getter @Setter -public class NPC extends GameCharacter { +public class NPC extends GameCharacter implements Interactable { private static final Logger LOG = LoggerFactory.getLogger(NPC.class); @@ -23,6 +25,15 @@ public class NPC extends GameCharacter { private List currentOptions; + /** Nur im Editor relevant: Optionen die nicht mit currentOptions verbunden sind, aber erhalten werden sollen. */ + private List 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 defaultMessages = new EnumMap<>(Status.class); + /** Tagesabläufe dieses NPCs. Die erste Routine gilt standardmäßig als aktiv. */ private List routines = new ArrayList<>(); @@ -58,6 +69,35 @@ public class NPC extends GameCharacter { 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 ────────────────────────────────────────────────────── public List getAvailableOptions(MainCharacter character) { diff --git a/blight-editor/src/main/java/de/blight/editor/AudioPreviewPopup.java b/blight-editor/src/main/java/de/blight/editor/AudioPreviewPopup.java new file mode 100644 index 0000000..abc8555 --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/AudioPreviewPopup.java @@ -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 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; + } +} diff --git a/blight-editor/src/main/java/de/blight/editor/EditorApp.java b/blight-editor/src/main/java/de/blight/editor/EditorApp.java index d7997f9..1953981 100644 --- a/blight-editor/src/main/java/de/blight/editor/EditorApp.java +++ b/blight-editor/src/main/java/de/blight/editor/EditorApp.java @@ -88,6 +88,7 @@ public class EditorApp extends Application { private StackPane plantPreviewPanel; // einmaliges Vorschau-Panel für alle Generatoren private Stage primaryStage; private JmeEditorApp jmeApp; + private AudioPreviewPopup audioPreviewPopup; // Baum-Generator-Zustand (wird beim Preset-Wechsel neu gesetzt) private TreeParams treeParams = TreeParams.oak(); @@ -155,6 +156,7 @@ public class EditorApp extends Application { private String animSetPendingPlayClip = null; private ComboBox animSetModelCombo; private boolean animSetDirty = false; + private boolean charDirty = false; private String animSetCurrentName = null; private Path animSetCurrentDir = null; // Anim-Offset-Editor (innerhalb AnimSet-Editor) @@ -163,6 +165,10 @@ public class EditorApp extends Application { javafx.collections.FXCollections.observableArrayList(); private java.util.Map animSetOffsets = new java.util.LinkedHashMap<>(); + // Sub-Clip-Editor (innerhalb AnimSet-Editor) + private ListView animSetPartsListView; + private java.util.Map + editableSubClips = new java.util.LinkedHashMap<>(); // Character-Editor-Zustand private de.blight.editor.ui.DialogEditorView dialogEditorView; @@ -173,6 +179,11 @@ public class EditorApp extends Application { private javafx.scene.control.ComboBox charStatusCombo; private javafx.scene.control.CheckBox charTraderCheck; private javafx.scene.control.ListView 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) private javafx.scene.control.Spinner abMagicSpin, abStaffSpin, abSwordSpin, abArcherySpin, abHeavySpin, abCrossbowSpin, abThieverySpin, @@ -211,6 +222,11 @@ public class EditorApp extends Application { // Spiel-Starten-Werkzeug-Zustand private TextField spawnXField; private TextField spawnZField; + private Label tempSpawnCoordsLabel; + private Label permSpawnCoordsLabel; + private Button gameNewBtn; + private boolean launchNewGameAfterSave = false; + private boolean pendingNewGame = false; // Baum-Ordner-Modus private Label randomTreeStatusLabel; @@ -249,6 +265,7 @@ public class EditorApp extends Application { private Spinner modelEditorInteractableXSpin = null; private Spinner modelEditorInteractableYSpin = null; private Spinner modelEditorInteractableZSpin = null; + private ComboBox modelEditorFootstepSurfaceCB = null; private boolean updatingInteractableSpinnersFromJme = false; // Modell-Import-Zustand @@ -396,6 +413,16 @@ public class EditorApp extends Application { stage.setMinWidth(900); stage.setMinHeight(600); 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(); if (jmeApp != null) jmeApp.stop(); Platform.exit(); @@ -603,6 +630,11 @@ public class EditorApp extends Application { updateSpawnFields(input.pickedSpawnInfo); } + if (input.permSpawnChanged) { + input.permSpawnChanged = false; + if (permSpawnCoordsLabel != null) permSpawnCoordsLabel.setText(permSpawnCoordsText()); + } + // Modell-Editor: gebakte Scale aus j3o erkannt → Spinner aktualisieren if (input.modelEditorBakedScaleDetected) { input.modelEditorBakedScaleDetected = false; @@ -3972,6 +4004,10 @@ public class EditorApp extends Application { switchToAnimPreview(); input.animPreviewLoadPath = relPath; 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")) { openAnimSetEditor(relPath, p); } 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. */ private List readAnimClipNames(Path j3oPath) { 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(); + imp.setAssetManager(assetManager); com.jme3.scene.Spatial s = (com.jme3.scene.Spatial) imp.load(j3oPath.toFile()); com.jme3.anim.AnimComposer ac = de.blight.game.animation.RetargetingSystem.findAnimComposer(s); @@ -6134,6 +6175,26 @@ public class EditorApp extends Application { 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 ─────────────────────────────────────────────────────────── Button saveBtn = new Button("💾 Speichern"); saveBtn.setMaxWidth(Double.MAX_VALUE); @@ -6172,7 +6233,10 @@ public class EditorApp extends Application { input.modelInteractableOffsetX, input.modelInteractableOffsetY, input.modelInteractableOffsetZ, - input.modelInteractableRotY)); + input.modelInteractableRotY, + modelEditorFootstepSurfaceCB != null + ? modelEditorFootstepSurfaceCB.getValue() + : "")); placeBtn.setOnAction(e -> { input.modelEditorCloseRequest = true; @@ -6215,6 +6279,8 @@ public class EditorApp extends Application { new Separator(), interactTitle, modelEditorInteractableCB, restPointBox, new Separator(), + footstepTitle, modelEditorFootstepSurfaceCB, + new Separator(), saveBtn, placeBtn, closeBtn ); return panel; @@ -6667,7 +6733,8 @@ public class EditorApp extends Application { java.util.List emitters, de.blight.common.model.InteractableType interactableType, 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) de.blight.common.ModelMeta meta = new de.blight.common.ModelMeta( name, category, tags, 1f, 1f, 1f, uniform, @@ -6675,7 +6742,8 @@ public class EditorApp extends Application { lod1Path, lod2Path, 30f, 80f, 120f, lights, emitters, 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()) { setStatus("Fehler: Modell-Datei nicht gefunden – Meta nicht gespeichert"); @@ -7008,9 +7076,15 @@ public class EditorApp extends Application { if (bothDown) { stopEditTimer(); } else if (e.getButton() == MouseButton.PRIMARY) { - editPressX = e.getX(); editPressY = e.getY(); editPressAction = +1; - submitEdit(editPressX, editPressY, editPressAction); - startEditTimer(); + if (input.activeLayer == SharedInput.LAYER_PLAY_TOOL) { + // Einzel-Klick ohne Edit-Timer (verhindert Dauer-Spam) + 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) { editPressX = e.getX(); editPressY = e.getY(); editPressAction = -1; submitEdit(editPressX, editPressY, editPressAction); @@ -7030,6 +7104,15 @@ public class EditorApp extends Application { 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()) { editPressX = e.getX(); editPressY = e.getY(); @@ -7041,6 +7124,9 @@ public class EditorApp extends Application { viewport.setOnMouseReleased(e -> { objDragging = false; if (!isObjectMode()) stopEditTimer(); + if (input.activeLayer == SharedInput.LAYER_PLAY_TOOL) { + input.playToolMouseUp = true; + } if (input.vertexSnapEnabled && input.objectSelectionMode == SharedInput.SEL_MODE_VERTEX) { input.vertexSnapTrigger = true; @@ -7141,17 +7227,28 @@ public class EditorApp extends Application { } private void launchGame() { - if (launchGameAfterSave) return; // bereits ausstehend + if (launchGameAfterSave) return; launchGameAfterSave = true; - if (gamePlayBtn != null) { - gamePlayBtn.setDisable(true); - gamePlayBtn.setText("⏳ Startet…"); - } + pendingNewGame = false; + if (gamePlayBtn != null) { gamePlayBtn.setDisable(true); gamePlayBtn.setText("⏳ Startet…"); } + if (gameNewBtn != null) gameNewBtn.setDisable(true); input.saveRequested = true; 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() { + final boolean isNewGame = pendingNewGame; + pendingNewGame = false; new Thread(() -> { try { 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 "-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.z=" + input.tempSpawnZ); + cmd.add("-Dblight.temp.spawn.yaw=" + input.tempSpawnYaw); } cmd.addAll(List.of("-cp", classpath, "de.blight.game.BlightGame")); @@ -7191,8 +7291,9 @@ public class EditorApp extends Application { .start(); Platform.runLater(() -> { - setStatus("Spiel gestartet"); + setStatus(isNewGame ? "Neues Spiel gestartet" : "Spiel gestartet"); if (gamePlayBtn != null) gamePlayBtn.setText("🎮 Läuft…"); + if (gameNewBtn != null) gameNewBtn.setText("🎮 Läuft…"); openGameConsole(); }); @@ -7205,22 +7306,18 @@ public class EditorApp extends Application { consoleBuffer.offer(line); } } - // Spiel beendet → Button freigeben + // Spiel beendet → Buttons freigeben consoleBuffer.offer("--- Spiel beendet ---"); Platform.runLater(() -> { - if (gamePlayBtn != null) { - gamePlayBtn.setText("▶ Spielen"); - gamePlayBtn.setDisable(false); - } + if (gamePlayBtn != null) { gamePlayBtn.setText("▶ Spielen"); gamePlayBtn.setDisable(false); } + if (gameNewBtn != null) { gameNewBtn.setText("🆕 Neues Spiel"); gameNewBtn.setDisable(false); } }); } catch (IOException ex) { Platform.runLater(() -> { setStatus("Spielstart fehlgeschlagen: " + ex.getMessage()); - if (gamePlayBtn != null) { - gamePlayBtn.setText("▶ Spielen"); - gamePlayBtn.setDisable(false); - } + if (gamePlayBtn != null) { gamePlayBtn.setText("▶ Spielen"); gamePlayBtn.setDisable(false); } + if (gameNewBtn != null) { gameNewBtn.setText("🆕 Neues Spiel"); gameNewBtn.setDisable(false); } }); } }, "game-launcher").start(); @@ -7809,47 +7906,63 @@ public class EditorApp extends Application { private VBox buildPlayToolPanel() { VBox inner = new VBox(8); 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:"); - coordHint.setStyle("-fx-text-fill: #444;"); - inner.getChildren().add(coordHint); + // ── Abschnitt: Spawnpunkte setzen ──────────────────────────────────── + inner.getChildren().addAll(sectionTitle("Spawnpunkte"), new Separator()); - spawnXField = new TextField(Float.isNaN(input.tempSpawnX) ? "" : String.valueOf(input.tempSpawnX)); - spawnZField = new TextField(Float.isNaN(input.tempSpawnZ) ? "" : String.valueOf(input.tempSpawnZ)); - spawnXField.setPromptText("X"); - spawnZField.setPromptText("Z"); - - Runnable applyFields = () -> { - try { - input.tempSpawnX = Float.parseFloat(spawnXField.getText().trim()); - 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(""); + Button setTempBtn = new Button("📍 Temp. Spawn hier setzen"); + setTempBtn.setMaxWidth(Double.MAX_VALUE); + setTempBtn.setTooltip(new javafx.scene.control.Tooltip( + "Setzt Yaw = Kamera-Blickrichtung und aktiviert Klick-Modus im Viewport")); + setTempBtn.setOnAction(e -> { + input.tempSpawnYaw = camYawToSpawnYaw(input.camYaw); + input.playToolMode = SharedInput.PlayToolMode.SET_TEMP; + setStatus("L-Klick im Viewport → temporären Spawnpunkt setzen"); }); - 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"); gamePlayBtn = playBtn; @@ -7857,8 +7970,21 @@ public class EditorApp extends Application { playBtn.setStyle( "-fx-background-color: #2d8a3e; -fx-text-fill: white; " + "-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()); - 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); scroll.setFitToWidth(true); @@ -7871,12 +7997,28 @@ public class EditorApp extends Application { 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) { - if (spawnXField == null || info == null) return; + if (info == null) return; String[] p = info.split("\\|", -1); if (p.length < 2) return; - spawnXField.setText(p[0]); - spawnZField.setText(p[1]); + if (spawnXField != null) spawnXField.setText(p[0]); + if (spawnZField != null) spawnZField.setText(p[1]); + if (tempSpawnCoordsLabel != null) tempSpawnCoordsLabel.setText(tempSpawnCoordsText()); } // ── Tripo3D-Generator ──────────────────────────────────────────────────── @@ -8280,6 +8422,7 @@ public class EditorApp extends Application { animSetClipListView = new ListView<>(); animSetClipListView.getItems().addAll(animSet.getClips()); animSetClipListView.setPrefHeight(180); + editableSubClips = new java.util.LinkedHashMap<>(animSet.getSubClips()); Path animRootDir = ASSET_ROOT.resolve("animations"); @@ -8390,6 +8533,8 @@ public class EditorApp extends Application { animSetClipListView.getItems().remove(sel); if (animSetActionListView != null) animSetActionListView.getItems().removeIf(it -> it.endsWith(" → " + sel)); + editableSubClips.entrySet().removeIf(en -> sel.equals(en.getValue().source)); + if (animSetPartsListView != null) animSetPartsListView.getItems().clear(); animSetDirty = true; }); @@ -8398,6 +8543,75 @@ public class EditorApp extends Application { HBox.setHgrow(removeClipBtn, Priority.ALWAYS); 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 ───────────────────────────────────────────────── inner.getChildren().addAll(new Separator(), sectionTitle("Aktions-Zuordnung"), new Separator()); @@ -8701,7 +8915,11 @@ public class EditorApp extends Application { actionCombo.getSelectionModel().selectFirst(); ComboBox clipCombo = new ComboBox<>(); - clipCombo.getItems().addAll(animSetClipListView.getItems()); + java.util.List 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.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 startSpinner = new Spinner<>(0.0, 9999.0, + isEdit ? existing.start : 0.0, 0.033); + startSpinner.setEditable(true); + startSpinner.setMaxWidth(Double.MAX_VALUE); + + Spinner 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 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) { if (animSetClipListView == null) { return; @@ -8767,6 +9057,7 @@ public class EditorApp extends Application { } } animSet.setAnimOffsets(offsetFinal); + animSet.setSubClips(new java.util.LinkedHashMap<>(editableSubClips)); // Vorschau-Modell-Pfad beibehalten if (animSetModelCombo != null && animSetModelCombo.getValue() != null && !animSetModelCombo.getValue().isBlank()) { animSet.setPreviewModelPath(animSetModelCombo.getValue()); @@ -9511,10 +9802,24 @@ public class EditorApp extends Application { Label npcTraderLbl = new Label("Handel:"); 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, npcStatusLbl, charStatusCombo, 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()); charNpcSection.setVisible(npcInitial); charNpcSection.setManaged(npcInitial); @@ -9666,6 +9971,10 @@ public class EditorApp extends Application { if (charStatusCombo != null) charStatusCombo.setValue("NEUTRAL"); if (charTraderCheck != null) charTraderCheck.setSelected(false); 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(); updateCharActionCombosFromSet(); if (charEditContainer != null) charEditContainer.setDisable(false); @@ -9710,6 +10019,12 @@ public class EditorApp extends Application { if (npc.getItems() != null) npc.getItems().forEach(it -> charTraderItemsView.getItems().add(it.getItemId())); } + java.util.Map 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) { loadAbilities(mc.getAbilities()); @@ -9721,6 +10036,7 @@ public class EditorApp extends Application { else dialogEditorView.clear(); } if (charEditorStatusLabel != null) charEditorStatusLabel.setText("Geladen: " + id); + charDirty = true; } catch (Exception e) { if (charEditorStatusLabel != null) charEditorStatusLabel.setText("Fehler: " + e.getMessage()); } @@ -9757,6 +10073,14 @@ public class EditorApp extends Application { catch (IllegalArgumentException ignored) {} } if (charTraderCheck != null) npc.setTrader(charTraderCheck.isSelected()); + // Default-Nachrichten je Status + java.util.Map 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()) { java.util.List items = new java.util.ArrayList<>(); charTraderItemsView.getItems().forEach(id2 -> { @@ -9775,6 +10099,7 @@ public class EditorApp extends Application { try { de.blight.common.model.CharacterIO.save(c, charDir); + charDirty = false; refreshCharacterList(); if (charEditorStatusLabel != null) charEditorStatusLabel.setText("Gespeichert: " + id); } catch (Exception e) { @@ -9899,6 +10224,23 @@ public class EditorApp extends Application { return ab; } + private static String msgKey( + java.util.Map 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 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() { onF5 = null; currentTool = "locationEditor"; diff --git a/blight-editor/src/main/java/de/blight/editor/SharedInput.java b/blight-editor/src/main/java/de/blight/editor/SharedInput.java index 2d99511..34ea7b0 100644 --- a/blight-editor/src/main/java/de/blight/editor/SharedInput.java +++ b/blight-editor/src/main/java/de/blight/editor/SharedInput.java @@ -521,9 +521,17 @@ public class SharedInput { public volatile boolean cancelZoneDrawing = false; // ── 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 PlayToolDrag(float screenX, float screenY) {} public final ConcurrentLinkedQueue playToolClickQueue = new ConcurrentLinkedQueue<>(); + public final ConcurrentLinkedQueue 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. @@ -531,10 +539,21 @@ public class SharedInput { */ public volatile String pickedSpawnInfo = null; 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. */ - public volatile float tempSpawnX = Float.NaN; - public volatile float tempSpawnZ = Float.NaN; + public volatile float tempSpawnX = 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 ────────────────────────────────────────────────── public volatile float animPreviewRotY = 0f; diff --git a/blight-editor/src/main/java/de/blight/editor/state/PlayToolState.java b/blight-editor/src/main/java/de/blight/editor/state/PlayToolState.java index 0c9e8ca..f25287c 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/PlayToolState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/PlayToolState.java @@ -9,12 +9,30 @@ import com.jme3.material.Material; import com.jme3.math.*; import com.jme3.renderer.Camera; import com.jme3.scene.*; -import com.jme3.scene.shape.Cylinder; +import com.jme3.scene.shape.*; import com.jme3.terrain.geomipmap.TerrainQuad; 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 { + 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 SimpleApplication app; private Camera cam; @@ -22,7 +40,21 @@ public class PlayToolState extends BaseAppState { private Node rootNode; 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) { this.input = input; @@ -34,10 +66,25 @@ public class PlayToolState extends BaseAppState { cam = app.getCamera(); assets = app.getAssetManager(); 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 protected void onEnable() {} + @Override + protected void cleanup(Application application) { + detachMarker(tempMarkerNode); + detachMarker(permMarkerNode); + } + + @Override protected void onEnable() {} @Override protected void onDisable() {} public void setTerrain(TerrainQuad terrain) { this.terrain = terrain; } @@ -46,58 +93,281 @@ public class PlayToolState extends BaseAppState { public void update(float tpf) { if (input.activeLayer != SharedInput.LAYER_PLAY_TOOL) return; + SharedInput.PlayToolMode mode = input.playToolMode; + + // --- Klick-Auswertung --- SharedInput.PlayToolClick click; while ((click = input.playToolClickQueue.poll()) != null) { - handleClick(click); + handleClick(click, mode); } - // Update marker position if spawn changed from text fields - if (!Float.isNaN(input.tempSpawnX) && !Float.isNaN(input.tempSpawnZ)) { - placeMarkerAt(input.tempSpawnX, input.tempSpawnZ); + // --- Drag --- + if (mode == SharedInput.PlayToolMode.EDIT) { + 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 jmeY = cam.getHeight() - click.screenY() * (float) input.viewportScaleY; + Vector2f screen = new Vector2f(jmeX, jmeY); - Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f); - Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f); - Ray ray = new Ray(near, far.subtract(near).normalizeLocal()); + Ray ray = new Ray(cam.getWorldCoordinates(screen, 0f), + cam.getWorldCoordinates(screen, 1f).subtractLocal( + 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(); terrain.collideWith(ray, hits); - if (hits.size() == 0) return; - - 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); + if (hits.size() == 0) return null; + return hits.getClosestCollision().getContactPoint(); } - private void placeMarkerAt(float x, float z) { - float y = terrain != null ? terrain.getHeight(new Vector2f(x, z)) : 0f; - if (Float.isNaN(y)) y = 0f; - - 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 float terrainY(float x, float z) { + if (terrain == null) return 0f; + float h = terrain.getHeight(new Vector2f(x, z)); + return Float.isNaN(h) ? 0f : h; } - private void removeMarker() { - if (spawnMarker != null) { - rootNode.detachChild(spawnMarker); - spawnMarker = null; + /** Terrain-Normale an (x, z) durch Kreuzprodukt zweier Tangenten. */ + private Vector3f terrainNormal(float x, float z) { + if (terrain == null) return Vector3f.UNIT_Y.clone(); + 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); } } } diff --git a/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java b/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java index b95b752..2d25065 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java @@ -458,6 +458,10 @@ public class TerrainEditorState extends BaseAppState { input.voxelFlatSlot = loadedMapData.voxelFlatSlot; input.voxelSteepSlot = loadedMapData.voxelSteepSlot; input.voxelCeilSlot = loadedMapData.voxelCeilSlot; + input.permSpawnX = loadedMapData.spawnX; + input.permSpawnZ = loadedMapData.spawnZ; + input.permSpawnYaw = loadedMapData.spawnYaw; + input.permSpawnChanged = true; input.voxelTexturesChanged = true; // 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. @@ -1210,6 +1214,16 @@ public class TerrainEditorState extends BaseAppState { data.voxelFlatSlot = voxelFlatSlot; data.voxelSteepSlot = voxelSteepSlot; 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) { try { GrassTuftIO.save(grassData); } diff --git a/blight-editor/src/main/java/de/blight/editor/ui/CraftingTableEditorView.java b/blight-editor/src/main/java/de/blight/editor/ui/CraftingTableEditorView.java index 3fc43f6..14e6159 100644 --- a/blight-editor/src/main/java/de/blight/editor/ui/CraftingTableEditorView.java +++ b/blight-editor/src/main/java/de/blight/editor/ui/CraftingTableEditorView.java @@ -12,9 +12,9 @@ import java.nio.file.Path; 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 - * immer alle 5 Typen; nicht konfigurierte Einträge erscheinen grau. + * immer alle Typen; nicht konfigurierte Einträge erscheinen grau. */ public class CraftingTableEditorView extends BorderPane { @@ -22,21 +22,188 @@ public class CraftingTableEditorView extends BorderPane { new EnumMap<>(CraftingTable.CraftingTableType.class); private final Path tableDir; - private TablePanel left; - private TablePanel right; + // ── List ────────────────────────────────────────────────────────────────── + + private ListView 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) { this.tableDir = tableDir; setStyle("-fx-background-color: #1e1e2e;"); reloadMap(); - left = new TablePanel("Liste 1", shared, tableDir, this::onSaved); - right = new TablePanel("Liste 2", shared, tableDir, this::onSaved); + SplitPane split = new SplitPane(buildListPanel(), buildFormPanel()); + 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); + // ── List panel ──────────────────────────────────────────────────────────── + + private VBox buildListPanel() { + 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() { @@ -44,251 +211,44 @@ public class CraftingTableEditorView extends BorderPane { shared.putAll(CraftingTableIO.loadAll(tableDir)); } - private void onSaved() { - reloadMap(); - left.refresh(); - right.refresh(); + private void refresh() { + listView.refresh(); + if (currentType != null) { + deleteBtn.setDisable(!shared.containsKey(currentType)); + if (!formContainer.isDisable()) loadFormFromType(currentType); + } } - // ── Single panel ────────────────────────────────────────────────────────── + // ── Helpers ─────────────────────────────────────────────────────────────── - static class TablePanel extends VBox { - - private final Map shared; - private final Path tableDir; - private final Runnable onSaved; - - private final ListView listView; - - private CraftingTable.CraftingTableType currentType = null; - - // Form fields - private TextField nameIdField; - private TextField objectPathField; - - private VBox formContainer; - private Button deleteBtn; - private Label formTypeLabel; - - TablePanel(String title, - Map 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 : ""; } + 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 : ""; } } diff --git a/blight-editor/src/main/java/de/blight/editor/ui/DialogEditorView.java b/blight-editor/src/main/java/de/blight/editor/ui/DialogEditorView.java index 0f43db7..478e3ff 100644 --- a/blight-editor/src/main/java/de/blight/editor/ui/DialogEditorView.java +++ b/blight-editor/src/main/java/de/blight/editor/ui/DialogEditorView.java @@ -2,10 +2,13 @@ package de.blight.editor.ui; import de.blight.common.model.*; import de.blight.common.model.quests.Quest; +import de.blight.common.model.quests.QuestIO; import de.blight.common.model.QuestRef; +import de.blight.editor.ProjectRoot; import javafx.geometry.Insets; import javafx.geometry.Orientation; import javafx.geometry.Pos; +import javafx.scene.Cursor; import javafx.scene.Node; import javafx.scene.control.*; import javafx.scene.layout.*; @@ -16,6 +19,7 @@ import javafx.scene.shape.Rectangle; import javafx.scene.text.Text; import javafx.stage.Modality; +import java.nio.file.Path; import java.util.*; /** @@ -89,8 +93,14 @@ public class DialogEditorView extends BorderPane { collectOptions(opt); 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(); clearDetailForm(); } @@ -112,6 +122,26 @@ public class DialogEditorView extends BorderPane { if (opt != null) roots.add(opt); } npc.setCurrentOptions(roots.isEmpty() ? null : roots); + + // Collect orphaned options (not reachable from any root) and preserve them + Set reachable = new HashSet<>(); + for (DialogOption root : roots) collectReachable(root, reachable); + List orphans = new ArrayList<>(); + for (Map.Entry entry : allOptions.entrySet()) { + if (!reachable.contains(entry.getKey())) orphans.add(entry.getValue()); + } + npc.setEditorOnlyOptions(orphans.isEmpty() ? null : orphans); + } + + private void collectReachable(DialogOption opt, Set 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 ──────────────────────────────────────────────────────────────── @@ -147,7 +177,12 @@ public class DialogEditorView extends BorderPane { private SplitPane buildListPane() { // ── Left: option list ───────────────────────────────────────────────── 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<>() { + { + selectedProperty().addListener((obs, old, sel) -> applyStyle()); + } @Override protected void updateItem(String id, boolean empty) { super.updateItem(id, empty); 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()) ? opt.getLabel() : id.substring(0, Math.min(8, id.length())) + "…"; 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( @@ -236,9 +278,16 @@ public class DialogEditorView extends BorderPane { statusCombo.setMaxWidth(Double.MAX_VALUE); 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.setPromptText("Quest-ID"); + questCompleteField.setPromptText("Quest wählen…"); + questCompleteField.setEditable(false); + questCompleteField.setCursor(Cursor.HAND); + questCompleteField.setOnMouseClicked(e -> pickQuestId(questCompleteField)); form.getChildren().addAll( sectionTitle("Voraussetzungen"), @@ -284,16 +333,23 @@ public class DialogEditorView extends BorderPane { // Quests 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.setPromptText("Quest-ID"); + fulfillsQuestField.setPromptText("Quest wählen…"); + fulfillsQuestField.setEditable(false); + fulfillsQuestField.setCursor(Cursor.HAND); + fulfillsQuestField.setOnMouseClicked(e -> pickQuestId(fulfillsQuestField)); abortsQuestsView = new ListView<>(); abortsQuestsView.setPrefHeight(80); abortsQuestsView.setStyle("-fx-background-color: #1e1e2e; -fx-control-inner-background: #1e1e2e;"); Button addAbortsBtn = smallBtn("+"); Button delAbortsBtn = smallBtn("−"); - addAbortsBtn.setOnAction(e -> promptQuestId(abortsQuestsView)); + addAbortsBtn.setOnAction(e -> pickQuestIdForList(abortsQuestsView)); delAbortsBtn.setOnAction(e -> removeSelected(abortsQuestsView)); form.getChildren().addAll( @@ -518,7 +574,12 @@ public class DialogEditorView extends BorderPane { DialogOption opt = new DialogOption(); opt.setLabel("Neue Option"); 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(); optionListView.getSelectionModel().select(opt.getId()); } @@ -580,14 +641,63 @@ public class DialogEditorView extends BorderPane { }); } - private void promptQuestId(ListView target) { - TextInputDialog dlg = new TextInputDialog(); - dlg.setTitle("Quest-ID"); - dlg.setHeaderText("Quest-ID eingeben:"); + private void pickQuestId(TextField target) { + String chosen = showQuestPickerDialog(); + if (chosen != null) target.setText(chosen); + } + + private void pickQuestIdForList(ListView 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 questList = QuestIO.loadAll(questDir); + + Dialog dlg = new Dialog<>(); + dlg.setTitle("Quest auswählen"); dlg.initModality(Modality.APPLICATION_MODAL); - dlg.showAndWait().ifPresent(id -> { - if (!id.isBlank() && !target.getItems().contains(id)) target.getItems().add(id); + + ListView 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 list) { diff --git a/blight-editor/src/main/java/de/blight/editor/ui/FractionEditorView.java b/blight-editor/src/main/java/de/blight/editor/ui/FractionEditorView.java index 2cf26e0..a838120 100644 --- a/blight-editor/src/main/java/de/blight/editor/ui/FractionEditorView.java +++ b/blight-editor/src/main/java/de/blight/editor/ui/FractionEditorView.java @@ -15,288 +15,262 @@ import java.nio.file.Path; import java.util.UUID; /** - * Fraktions-Verwaltung: zwei identische FractionPanel-Instanzen nebeneinander. + * Fraktions-Verwaltung: Liste links, Formular rechts. * Sortiert nach Name-ID, dann nach UUID. */ public class FractionEditorView extends BorderPane { - private final ObservableList sharedFractions = FXCollections.observableArrayList(); + private final ObservableList fractions = FXCollections.observableArrayList(); private final Path fractionDir; + // ── List ────────────────────────────────────────────────────────────────── + + private final SortedList sortedFractions; + private ListView 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) { - this.fractionDir = fractionDir; + this.fractionDir = fractionDir; + this.sortedFractions = new SortedList<>(fractions, FractionIO.SORT_ORDER); setStyle("-fx-background-color: #1e1e2e;"); reload(); - FractionPanel left = new FractionPanel("Liste 1", sharedFractions, fractionDir, this::reload); - FractionPanel right = new FractionPanel("Liste 2", sharedFractions, fractionDir, this::reload); + SplitPane split = new SplitPane(buildListPanel(), buildFormPanel()); + 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); + // ── List panel ──────────────────────────────────────────────────────────── + + private VBox buildListPanel() { + 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() { - 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 fractions; - private final Path fractionDir; - private final Runnable onSaved; + private static String textId(TextReference r) { return r != null ? r.id() : ""; } - private final SortedList sortedFractions; - private final ListView listView; + private static TextField field(String prompt) { + 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 Label idLabel; - private TextField nameField; - private TextField maleMemberField; - private TextField femaleMemberField; - private TextField rank1Field; - private TextField rank2Field; - private TextField rank3Field; - - private VBox formContainer; - private Button deleteBtn; - - FractionPanel(String title, ObservableList 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; - } + 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; } } diff --git a/blight-editor/src/main/java/de/blight/editor/ui/LocationEditorView.java b/blight-editor/src/main/java/de/blight/editor/ui/LocationEditorView.java index 6866991..2447746 100644 --- a/blight-editor/src/main/java/de/blight/editor/ui/LocationEditorView.java +++ b/blight-editor/src/main/java/de/blight/editor/ui/LocationEditorView.java @@ -15,237 +15,239 @@ import java.io.IOException; 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). */ public class LocationEditorView extends BorderPane { - private final ObservableList sharedLocations = FXCollections.observableArrayList(); + private final ObservableList locations = FXCollections.observableArrayList(); + + // ── List ────────────────────────────────────────────────────────────────── + + private ListView 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() { setStyle("-fx-background-color: #1e1e2e;"); reload(); - LocationPanel left = new LocationPanel("Liste 1", sharedLocations, this::saveAll); - LocationPanel right = new LocationPanel("Liste 2", sharedLocations, this::saveAll); - - HBox panels = new HBox(1, left, right); - HBox.setHgrow(left, Priority.ALWAYS); - HBox.setHgrow(right, Priority.ALWAYS); - setCenter(panels); + SplitPane split = new SplitPane(buildListPanel(), buildFormPanel()); + split.setDividerPositions(0.28); + setCenter(split); } - private void reload() { - try { sharedLocations.setAll(LocationIO.load()); } - catch (IOException e) { sharedLocations.clear(); } + // ── List panel ──────────────────────────────────────────────────────────── + + 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() { - try { LocationIO.save(sharedLocations); } - catch (IOException e) { - new Alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait(); - } + // ── 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;"); + + 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(); } - // ── Single panel ────────────────────────────────────────────────────────── - - static class LocationPanel extends VBox { - - private final ObservableList locations; - private final Runnable onSaved; - - private final ListView 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 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); + 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; } + String savedId = current.getId(); + persist(); + reload(); + locations.stream() + .filter(l -> savedId.equals(l.getId())) + .findFirst() + .ifPresent(listView.getSelectionModel()::select); + } - private VBox buildForm() { - VBox form = new VBox(6); - form.setPadding(new Insets(10)); - 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 — 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; + private void persist() { + try { LocationIO.save(locations); } + catch (IOException e) { + new Alert(Alert.AlertType.ERROR, "Fehler: " + e.getMessage(), ButtonType.OK).showAndWait(); } } + + 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; + } } diff --git a/blight-editor/src/main/java/de/blight/editor/ui/QuestEditorView.java b/blight-editor/src/main/java/de/blight/editor/ui/QuestEditorView.java index 8ebbb7a..5b3bfea 100644 --- a/blight-editor/src/main/java/de/blight/editor/ui/QuestEditorView.java +++ b/blight-editor/src/main/java/de/blight/editor/ui/QuestEditorView.java @@ -10,7 +10,6 @@ import de.blight.common.model.quests.*; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.geometry.Insets; -import javafx.geometry.Orientation; import javafx.geometry.Pos; import javafx.scene.Node; import javafx.scene.control.*; @@ -21,467 +20,454 @@ import java.nio.file.Path; import java.util.List; /** - * Quest-Verwaltung: zwei identische QuestPanel-Instanzen nebeneinander. - * Beide Panels teilen die gleiche ObservableList und speichern in dasselbe Verzeichnis. + * Quest-Verwaltung: Liste links, Formular rechts. */ public class QuestEditorView extends BorderPane { - private final ObservableList sharedQuests = FXCollections.observableArrayList(); + private final ObservableList quests = FXCollections.observableArrayList(); private final Path questDir; + // ── List ────────────────────────────────────────────────────────────────── + + private ListView listView; + private Button deleteBtn; + private Quest current = null; + + // ── Common form fields ──────────────────────────────────────────────────── + + private TextField idField; + private Spinner xpSpinner; + private TextField textField; + private TextField descField; + private TextField successField; + private ComboBox typeCombo; + private VBox dynamicArea; + private VBox formContainer; + private Button saveBtn; + + // ── Type-specific fields ────────────────────────────────────────────────── + + private TextField f1, f2, f3; + private Spinner countSpinner; + public QuestEditorView(Path questDir) { this.questDir = questDir; setStyle("-fx-background-color: #1e1e2e;"); reload(); - QuestPanel left = new QuestPanel("Liste 1", sharedQuests, questDir, this::reload); - QuestPanel right = new QuestPanel("Liste 2", sharedQuests, questDir, this::reload); + SplitPane split = new SplitPane(buildListPanel(), buildFormPanel()); + 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); + // ── List panel ──────────────────────────────────────────────────────────── + + private VBox buildListPanel() { + 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() { List 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 quests; - private final Path questDir; - private final Runnable onSaved; + 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 final ListView listView; - private Quest current = null; + private static TextField tf(String prompt) { + TextField f = new TextField(); + f.setPromptText(prompt); + return f; + } - // Common fields - private TextField idField; - private Spinner xpSpinner; - private TextField textField; - private TextField descField; - private TextField successField; + private static String safe(String s) { return s != null ? s : ""; } - // Type selection - private ComboBox typeCombo; + private static TextReference ref(TextField f) { + String s = f.getText().trim(); + return s.isBlank() ? null : new TextReference(s); + } - // Dynamic area - private VBox dynamicArea; - - // Type-specific fields (lazily filled) - private TextField f1, f2, f3; - private Spinner countSpinner; - - // Form container (disabled when nothing loaded) - private VBox formContainer; - private Button saveBtn; - private Button deleteBtn; - - QuestPanel(String title, ObservableList 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(); - } + private void showError(String msg) { + Alert a = new Alert(Alert.AlertType.ERROR, msg, ButtonType.OK); + a.showAndWait(); } } diff --git a/blight-editor/src/main/java/de/blight/editor/ui/RecipeEditorView.java b/blight-editor/src/main/java/de/blight/editor/ui/RecipeEditorView.java index 9e4af00..8ae93ab 100644 --- a/blight-editor/src/main/java/de/blight/editor/ui/RecipeEditorView.java +++ b/blight-editor/src/main/java/de/blight/editor/ui/RecipeEditorView.java @@ -5,7 +5,6 @@ import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.collections.transformation.SortedList; import javafx.geometry.Insets; -import javafx.geometry.Orientation; import javafx.geometry.Pos; import javafx.scene.Node; import javafx.scene.control.*; @@ -17,492 +16,450 @@ import java.nio.file.Path; import java.util.*; /** - * Rezept-Verwaltung: zwei identische RecipePanel-Instanzen nebeneinander. + * Rezept-Verwaltung: Liste links, Formular rechts. * Sortiert nach CraftingTableType, dann nach erstelltem Item-ID. */ public class RecipeEditorView extends BorderPane { - private final ObservableList sharedRecipes = FXCollections.observableArrayList(); + private static final String NO_TABLE = "— kein Tisch —"; + + private final ObservableList recipes = FXCollections.observableArrayList(); private final Path recipeDir; + // ── List ────────────────────────────────────────────────────────────────── + + private final SortedList sortedRecipes; + private ListView listView; + private Button deleteBtn; + private Recipe current = null; + private String oldFileId = null; + + // ── Form fields ─────────────────────────────────────────────────────────── + + private TextField createsField; + private ListView componentsList; + private ComboBox tableCombo; + private HBox alchemyRow; + private HBox enchantingRow; + private HBox smitheryRow; + private HBox engineeringRow; + private Spinner alchemySpinner; + private Spinner enchantingSpinner; + private Spinner smitherySpinner; + private Spinner engineeringSpinner; + private VBox formContainer; + public RecipeEditorView(Path recipeDir) { - this.recipeDir = recipeDir; + this.recipeDir = recipeDir; + this.sortedRecipes = new SortedList<>(recipes, RecipeIO.SORT_ORDER); setStyle("-fx-background-color: #1e1e2e;"); reload(); - RecipePanel left = new RecipePanel("Liste 1", sharedRecipes, recipeDir, this::reload); - RecipePanel right = new RecipePanel("Liste 2", sharedRecipes, recipeDir, this::reload); + SplitPane split = new SplitPane(buildListPanel(), buildFormPanel()); + 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); + // ── List panel ──────────────────────────────────────────────────────────── + + private VBox buildListPanel() { + 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 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 dlg = new Dialog<>(); + dlg.setTitle("Zutat hinzufügen"); + dlg.initModality(Modality.APPLICATION_MODAL); + + TextField itemIdField = new TextField(); + itemIdField.setPromptText("Item-ID"); + Spinner 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() { - sharedRecipes.setAll(RecipeIO.loadAll(recipeDir)); + recipes.setAll(RecipeIO.loadAll(recipeDir)); } - // ── Single panel ────────────────────────────────────────────────────────── - - static class RecipePanel extends VBox { - - private static final String NO_TABLE = "— kein Tisch —"; - - private final ObservableList recipes; - private final Path recipeDir; - private final Runnable onSaved; - - private final SortedList sortedRecipes; - private final ListView listView; - - private Recipe current = null; - private String oldFileId = null; // for rename-on-save detection - - // Form fields - private TextField createsField; - private ListView componentsList; // "itemId × count" - private ComboBox tableCombo; - - // Level-Anforderungs-Zeilen (jeweils Label + Spinner) - private HBox alchemyRow; - private HBox enchantingRow; - private HBox smitheryRow; - private HBox engineeringRow; - private Spinner alchemySpinner; - private Spinner enchantingSpinner; - private Spinner smitherySpinner; - private Spinner engineeringSpinner; - - private VBox formContainer; - private Button deleteBtn; - - RecipePanel(String title, ObservableList 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 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 dlg = new Dialog<>(); - dlg.setTitle("Zutat hinzufügen"); - dlg.initModality(Modality.APPLICATION_MODAL); - - TextField itemIdField = new TextField(); - itemIdField.setPromptText("Item-ID"); - Spinner 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 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 lvlSpinner() { - Spinner 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 : ""; } + // ── 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 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 lvlSpinner() { + Spinner 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 : ""; } } diff --git a/blight-game/src/main/java/de/blight/game/BlightGame.java b/blight-game/src/main/java/de/blight/game/BlightGame.java index d2be40e..38bc50e 100644 --- a/blight-game/src/main/java/de/blight/game/BlightGame.java +++ b/blight-game/src/main/java/de/blight/game/BlightGame.java @@ -9,6 +9,7 @@ import com.jme3.system.AppSettings; import de.blight.common.BlightHome; import de.blight.common.SaveGameIO; import de.blight.game.config.*; +import de.blight.game.state.AudioSettingsState; import de.blight.game.state.SaveGameState; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -31,11 +32,13 @@ public class BlightGame extends SimpleApplication { private KeyBindings keyBindings; private GraphicsSettings graphicsSettings; + private AudioSettings audioSettings; private ScreenshotAppState screenshotState; private Path screenshotDir; private WorldScene worldScene; private ConfigScreen configScreen; private GraphicsScreen graphicsScreen; + private AudioScreen audioScreen; private PauseMenu pauseMenu; private MainMenuState mainMenuState; @@ -135,6 +138,8 @@ public class BlightGame extends SimpleApplication { keyBindings = KeyBindingStore.load(); graphicsSettings = GraphicsStore.load(); + audioSettings = AudioSettingsStore.load(); + stateManager.attach(new AudioSettingsState(audioSettings)); status("Lade Spielstand..."); stateManager.attach(new SaveGameState()); @@ -164,6 +169,13 @@ public class BlightGame extends SimpleApplication { stateManager.attach(graphicsScreen); graphicsScreen.setEnabled(false); + audioScreen = new AudioScreen(audioSettings, () -> { + audioScreen.setEnabled(false); + screenCloseTarget.run(); + }); + stateManager.attach(audioScreen); + audioScreen.setEnabled(false); + SaveGameState saveState = stateManager.getState(SaveGameState.class); pauseMenu = new PauseMenu( @@ -173,6 +185,11 @@ public class BlightGame extends SimpleApplication { pauseMenu.setEnabled(false); graphicsScreen.setEnabled(true); }, + () -> { + screenCloseTarget = () -> pauseMenu.setEnabled(true); + pauseMenu.setEnabled(false); + audioScreen.setEnabled(true); + }, () -> { screenCloseTarget = () -> pauseMenu.setEnabled(true); pauseMenu.setEnabled(false); @@ -237,6 +254,7 @@ public class BlightGame extends SimpleApplication { if (mainMenuState != null && mainMenuState.isEnabled()) return; if (graphicsScreen.isEnabled()) return; + if (audioScreen.isEnabled()) return; if (configScreen.isEnabled()) { if (configScreen.isWaiting()) { diff --git a/blight-game/src/main/java/de/blight/game/animation/AnimSet.java b/blight-game/src/main/java/de/blight/game/animation/AnimSet.java index 9fd1c0d..0281ac5 100644 --- a/blight-game/src/main/java/de/blight/game/animation/AnimSet.java +++ b/blight-game/src/main/java/de/blight/game/animation/AnimSet.java @@ -28,6 +28,15 @@ public class AnimSet { private String previewModelPath = null; /** Manueller Positions-/Rotations-Versatz pro Clip-Name. */ private Map animOffsets = new LinkedHashMap<>(); + /** Sub-Clip-Definitionen: Name → Zeitfenster aus einem kombinierten Quell-Clip. */ + private Map 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 getClips() { return clips; } public void setClips(List clips) { this.clips = clips; } @@ -41,6 +50,10 @@ public class AnimSet { public void setAnimOffsets(Map animOffsets) { this.animOffsets = animOffsets; } + public Map getSubClips() { + return subClips != null ? subClips : new LinkedHashMap<>(); + } + public void setSubClips(Map subClips) { this.subClips = subClips; } /** Speichert dieses Set als {@code .animset.json} im Verzeichnis {@code setDir}. */ public void save(Path setDir, String setName) throws IOException { diff --git a/blight-game/src/main/java/de/blight/game/animation/AnimationLibrary.java b/blight-game/src/main/java/de/blight/game/animation/AnimationLibrary.java index 475c816..f171f10 100644 --- a/blight-game/src/main/java/de/blight/game/animation/AnimationLibrary.java +++ b/blight-game/src/main/java/de/blight/game/animation/AnimationLibrary.java @@ -9,6 +9,7 @@ import com.jme3.scene.Spatial; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.jme3.math.Quaternion; import com.jme3.math.Vector3f; import java.io.IOException; @@ -220,6 +221,71 @@ public class AnimationLibrary extends BaseAppState { } else { 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 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> 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) { diff --git a/blight-game/src/main/java/de/blight/game/audio/FootstepSystem.java b/blight-game/src/main/java/de/blight/game/audio/FootstepSystem.java new file mode 100644 index 0000000..0a120b2 --- /dev/null +++ b/blight-game/src/main/java/de/blight/game/audio/FootstepSystem.java @@ -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; + +/** + * Dreistu​fige 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> 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>> gaitPools = new HashMap<>(); + /** Stufe 2: surface → AudioNodes (flach, kein Gait-Unterordner) */ + private final Map> flatPools = new HashMap<>(); + /** Stufe 3: Dateien direkt in audio/footsteps/ */ + private final List 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> gaitMap = new HashMap<>(); + for (String gait : GAITS) { + List 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 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 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 resolve(String key, String gait) { + // Stufe 1a: exakt surface/gait + List 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 fromGaitPool(String key, String gait) { + Map> m = gaitPools.get(key); + if (m == null) return null; + List l = m.get(gait); + return (l != null && !l.isEmpty()) ? l : null; + } + + private List fromFlatPool(String key) { + List l = flatPools.get(key); + return (l != null && !l.isEmpty()) ? l : null; + } + + private List 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 scanDir(AssetManager am, Node parentNode, + Path assetRoot, String relDir) { + Path absDir = assetRoot.resolve(relDir); + if (!Files.isDirectory(absDir)) { + return Collections.emptyList(); + } + + List 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 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; + } +} diff --git a/blight-game/src/main/java/de/blight/game/audio/SurfaceType.java b/blight-game/src/main/java/de/blight/game/audio/SurfaceType.java new file mode 100644 index 0000000..87b2377 --- /dev/null +++ b/blight-game/src/main/java/de/blight/game/audio/SurfaceType.java @@ -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; + } + } +} diff --git a/blight-game/src/main/java/de/blight/game/config/ConfigScreen.java b/blight-game/src/main/java/de/blight/game/config/ConfigScreen.java index 2fb61b9..48c4298 100644 --- a/blight-game/src/main/java/de/blight/game/config/ConfigScreen.java +++ b/blight-game/src/main/java/de/blight/game/config/ConfigScreen.java @@ -1,309 +1,275 @@ -package de.blight.game.config; - -import java.util.ArrayList; -import java.util.List; - -import com.jme3.app.Application; -import com.jme3.app.SimpleApplication; -import com.jme3.app.state.BaseAppState; -import com.jme3.font.BitmapFont; -import com.jme3.font.BitmapText; -import com.jme3.input.KeyInput; -import com.jme3.input.MouseInput; -import com.jme3.input.RawInputListener; -import com.jme3.input.controls.ActionListener; -import com.jme3.input.controls.MouseButtonTrigger; -import com.jme3.input.event.JoyAxisEvent; -import com.jme3.input.event.JoyButtonEvent; -import com.jme3.input.event.KeyInputEvent; -import com.jme3.input.event.MouseButtonEvent; -import com.jme3.input.event.MouseMotionEvent; -import com.jme3.input.event.TouchEvent; -import com.jme3.material.Material; -import com.jme3.material.RenderState; -import com.jme3.math.ColorRGBA; -import com.jme3.math.Vector2f; -import com.jme3.renderer.queue.RenderQueue; -import com.jme3.scene.Geometry; -import com.jme3.scene.Node; -import com.jme3.scene.shape.Quad; - -/** - * Overlay-AppState der die Tastenbelegungs-Maske anzeigt. - * - * ESC → Schließen (ohne Speichern) - * Klick auf Row → wartet auf neue Taste - * ESC während Warten → bricht nur die Zuweisung ab - * Speichern → schreibt JSON, ruft onSave-Callback - */ -public class ConfigScreen extends BaseAppState implements RawInputListener { - - // Farben - private static final ColorRGBA COL_BG = new ColorRGBA(0.05f, 0.05f, 0.08f, 0.88f); - private static final ColorRGBA COL_PANEL = new ColorRGBA(0.10f, 0.10f, 0.16f, 1.00f); - private static final ColorRGBA COL_ROW = new ColorRGBA(0.18f, 0.18f, 0.28f, 1.00f); - private static final ColorRGBA COL_ROW_HOVER = new ColorRGBA(0.25f, 0.25f, 0.40f, 1.00f); - private static final ColorRGBA COL_ROW_WAIT = new ColorRGBA(0.50f, 0.30f, 0.10f, 1.00f); - private static final ColorRGBA COL_BTN_SAVE = new ColorRGBA(0.15f, 0.40f, 0.15f, 1.00f); - private static final ColorRGBA COL_BTN_CANCEL = new ColorRGBA(0.40f, 0.15f, 0.15f, 1.00f); - private static final ColorRGBA COL_TEXT = ColorRGBA.White; - private static final ColorRGBA COL_TEXT_KEY = new ColorRGBA(0.85f, 0.85f, 0.50f, 1.00f); - - // ----------------------------------------------------------------------- - - private SimpleApplication app; - private Node guiNode; - private BitmapFont font; - - private KeyBindings liveBindings; // geteilt mit der ganzen App - private KeyBindings editCopy; // wird beim Öffnen geklont - - private Runnable onSave; // Callback → PlayerInputControl.reloadBindings - private Runnable onClose; // Callback → PauseMenu wiederherstellen - - private Node panel; - private List rows = new ArrayList<>(); - private int waitingRow = -1; // -1 = keine Zuweisung aktiv - - // UI-Elemente für Buttons (Bounds in Screen-Koordinaten) - private float saveBtnX, saveBtnY, saveBtnW, saveBtnH; - private float cancelBtnX, cancelBtnY; - - // ----------------------------------------------------------------------- - - private static class Row { - String field; - String label; - BitmapText keyText; - Geometry bg; - float x, y, w, h; // Button-Bounds - } - - // ----------------------------------------------------------------------- - - public ConfigScreen(KeyBindings liveBindings, Runnable onSave) { - this.liveBindings = liveBindings; - this.onSave = onSave; - } - - public boolean isWaiting() { return waitingRow >= 0; } - - public void setOnClose(Runnable onClose) { this.onClose = onClose; } - - public void cancelWaiting() { - if (waitingRow >= 0) { resetRowColor(waitingRow); waitingRow = -1; } - } - - // ----------------------------------------------------------------------- - // Lifecycle - // ----------------------------------------------------------------------- - - @Override - protected void initialize(Application app) { - this.app = (SimpleApplication) app; - this.guiNode = this.app.getGuiNode(); - this.font = app.getAssetManager().loadFont("Interface/Fonts/Default.fnt"); - } - - @Override - protected void onEnable() { - editCopy = liveBindings.copy(); - waitingRow = -1; - buildUI(); - app.getInputManager().setCursorVisible(true); - app.getInputManager().addRawInputListener(this); - app.getInputManager().addMapping("_CfgClick", new MouseButtonTrigger(MouseInput.BUTTON_LEFT)); - app.getInputManager().addListener(clickListener, "_CfgClick"); - } - - @Override - protected void onDisable() { - if (panel != null) { guiNode.detachChild(panel); panel = null; } - rows.clear(); - waitingRow = -1; - app.getInputManager().removeRawInputListener(this); - app.getInputManager().deleteMapping("_CfgClick"); - app.getInputManager().setCursorVisible(false); - } - - @Override protected void cleanup(Application app) {} - - // ----------------------------------------------------------------------- - // UI aufbauen - // ----------------------------------------------------------------------- - - private void buildUI() { - float sw = app.getCamera().getWidth(); - float sh = app.getCamera().getHeight(); - - panel = new Node("cfg-panel"); - - // Halbdurchsichtiger Overlay über dem Spiel - addQuad(panel, 0, 0, sw, sh, COL_BG, -2); - - float pw = 720, ph = 440; - float px = (sw - pw) / 2f, py = (sh - ph) / 2f; - addQuad(panel, px, py, pw, ph, COL_PANEL, -1); - - // Titel - BitmapText title = text("TASTENBELEGUNG", 20, COL_TEXT); - centerText(title, px, py + ph - 40, pw); - panel.attachChild(title); - - BitmapText hint = text("Klicke eine Taste um sie neu zu belegen", 14, new ColorRGBA(0.7f, 0.7f, 0.7f, 1f)); - centerText(hint, px, py + ph - 70, pw); - panel.attachChild(hint); - - // Reihen - float rowX = px + 30; - float keyX = px + pw - 220; - float rowW = 180; - float rowH = 36; - float startY = py + ph - 110; - float stepY = 48; - - for (int i = 0; i < KeyBindings.ENTRIES.length; i++) { - String[] entry = KeyBindings.ENTRIES[i]; - float ry = startY - i * stepY; - - BitmapText lbl = text(entry[1], 16, COL_TEXT); - lbl.setLocalTranslation(rowX, ry + rowH - 8, 0); - panel.attachChild(lbl); - - Geometry bg = addQuad(panel, keyX, ry, rowW, rowH, COL_ROW, 0); - - BitmapText kt = text(KeyNames.of(editCopy.get(entry[0])), 16, COL_TEXT_KEY); - kt.setLocalTranslation(keyX + 10, ry + rowH - 8, 1); - panel.attachChild(kt); - - Row row = new Row(); - row.field = entry[0]; - row.label = entry[1]; - row.keyText = kt; - row.bg = bg; - row.x = keyX; row.y = ry; row.w = rowW; row.h = rowH; - rows.add(row); - } - - // Buttons - float btnW = 160, btnH = 42; - float btnY = py + 25; - saveBtnX = px + pw / 2f - btnW - 15; - saveBtnY = btnY; - saveBtnW = btnW; - saveBtnH = btnH; - cancelBtnX = px + pw / 2f + 15; - cancelBtnY = btnY; - - addQuad(panel, saveBtnX, saveBtnY, btnW, btnH, COL_BTN_SAVE, 0); - BitmapText saveLabel = text("Speichern", 16, COL_TEXT); - centerText(saveLabel, saveBtnX, saveBtnY + btnH - 10, btnW); - panel.attachChild(saveLabel); - - addQuad(panel, cancelBtnX, cancelBtnY, btnW, btnH, COL_BTN_CANCEL, 0); - BitmapText cancelLabel = text("Abbrechen", 16, COL_TEXT); - centerText(cancelLabel, cancelBtnX, cancelBtnY + btnH - 10, btnW); - panel.attachChild(cancelLabel); - - guiNode.attachChild(panel); - } - - // ----------------------------------------------------------------------- - // Mausklick - // ----------------------------------------------------------------------- - - private final ActionListener clickListener = (name, isPressed, tpf) -> { - if (!isPressed) return; - Vector2f cursor = app.getInputManager().getCursorPosition(); - - // Reihen prüfen - for (int i = 0; i < rows.size(); i++) { - Row r = rows.get(i); - if (hits(cursor, r.x, r.y, r.w, r.h)) { - waitingRow = i; - r.bg.getMaterial().setColor("Color", COL_ROW_WAIT); - r.keyText.setText("..."); - return; - } - } - - // Speichern - if (hits(cursor, saveBtnX, saveBtnY, saveBtnW, saveBtnH)) { - liveBindings.copyFrom(editCopy); - KeyBindingStore.save(liveBindings); - if (onSave != null) onSave.run(); - setEnabled(false); - if (onClose != null) onClose.run(); - return; - } - - // Abbrechen - if (hits(cursor, cancelBtnX, cancelBtnY, saveBtnW, saveBtnH)) { - setEnabled(false); - if (onClose != null) onClose.run(); - } - }; - - // ----------------------------------------------------------------------- - // Tastendruck beim Warten auf Zuweisung (RawInputListener) - // ----------------------------------------------------------------------- - - @Override - public void onKeyEvent(KeyInputEvent evt) { - if (!evt.isPressed() || waitingRow < 0) return; - if (evt.getKeyCode() == KeyInput.KEY_ESCAPE) return; // cancelWaiting() wird von BlightApp aufgerufen - - Row r = rows.get(waitingRow); - editCopy.set(r.field, evt.getKeyCode()); - r.keyText.setText(KeyNames.of(evt.getKeyCode())); - resetRowColor(waitingRow); - waitingRow = -1; - } - - private void resetRowColor(int idx) { - rows.get(idx).bg.getMaterial().setColor("Color", COL_ROW); - } - - // RawInputListener-Pflichtmethoden - @Override public void beginInput() {} - @Override public void endInput() {} - @Override public void onMouseMotionEvent(MouseMotionEvent evt) {} - @Override public void onMouseButtonEvent(MouseButtonEvent evt) {} - @Override public void onJoyAxisEvent(JoyAxisEvent evt) {} - @Override public void onJoyButtonEvent(JoyButtonEvent evt) {} - @Override public void onTouchEvent(TouchEvent evt) {} - - // ----------------------------------------------------------------------- - // Hilfsmethoden - // ----------------------------------------------------------------------- - - private Geometry addQuad(Node parent, float x, float y, float w, float h, ColorRGBA color, float z) { - Geometry geo = new Geometry("q", new Quad(w, h)); - Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md"); - mat.setColor("Color", color.clone()); - if (color.a < 1f) { - mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha); - geo.setQueueBucket(RenderQueue.Bucket.Transparent); - } - geo.setMaterial(mat); - geo.setLocalTranslation(x, y, z); - parent.attachChild(geo); - return geo; - } - - private BitmapText text(String content, int size, ColorRGBA color) { - BitmapText t = new BitmapText(font); - 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; - } -} +package de.blight.game.config; + +import java.util.ArrayList; +import java.util.List; + +import com.jme3.app.Application; +import com.jme3.app.SimpleApplication; +import com.jme3.app.state.BaseAppState; +import com.jme3.font.BitmapFont; +import com.jme3.font.BitmapText; +import com.jme3.input.KeyInput; +import com.jme3.input.MouseInput; +import com.jme3.input.RawInputListener; +import com.jme3.input.controls.ActionListener; +import com.jme3.input.controls.MouseButtonTrigger; +import com.jme3.input.event.JoyAxisEvent; +import com.jme3.input.event.JoyButtonEvent; +import com.jme3.input.event.KeyInputEvent; +import com.jme3.input.event.MouseButtonEvent; +import com.jme3.input.event.MouseMotionEvent; +import com.jme3.input.event.TouchEvent; +import com.jme3.material.Material; +import com.jme3.material.RenderState; +import com.jme3.math.ColorRGBA; +import com.jme3.math.Vector2f; +import com.jme3.renderer.queue.RenderQueue; +import com.jme3.scene.Geometry; +import com.jme3.scene.Node; +import com.jme3.scene.shape.Quad; +import de.blight.lang.TextResolver; + +public class ConfigScreen extends BaseAppState implements RawInputListener { + + private static final ColorRGBA COL_ROW = new ColorRGBA(0.18f, 0.18f, 0.28f, 1.00f); + private static final ColorRGBA COL_ROW_WAIT = new ColorRGBA(0.50f, 0.30f, 0.10f, 1.00f); + private static final ColorRGBA COL_TEXT = ColorRGBA.White; + private static final ColorRGBA COL_TEXT_KEY = new ColorRGBA(0.85f, 0.85f, 0.50f, 1.00f); + + private SimpleApplication app; + private Node guiNode; + private BitmapFont font; + + private KeyBindings liveBindings; + private KeyBindings editCopy; + + private Runnable onSave; + private Runnable onClose; + + private Node panel; + private Node bgLayer; + private Node canvasNode; + private List rows = new ArrayList<>(); + private int waitingRow = -1; + + private float saveBtnX, saveBtnY, saveBtnW, saveBtnH; + private float cancelBtnX, cancelBtnY; + + private static class Row { + String field; + BitmapText keyText; + Geometry bg; + float x, y, w, h; + } + + public ConfigScreen(KeyBindings liveBindings, Runnable onSave) { + this.liveBindings = liveBindings; + this.onSave = onSave; + } + + public boolean isWaiting() { return waitingRow >= 0; } + public void setOnClose(Runnable onClose) { this.onClose = onClose; } + + public void cancelWaiting() { + if (waitingRow >= 0) { resetRowColor(waitingRow); waitingRow = -1; } + } + + @Override + protected void initialize(Application app) { + this.app = (SimpleApplication) app; + this.guiNode = this.app.getGuiNode(); + this.font = app.getAssetManager().loadFont("Interface/Fonts/Default.fnt"); + } + + @Override + protected void onEnable() { + editCopy = liveBindings.copy(); + waitingRow = -1; + buildUI(); + app.getInputManager().setCursorVisible(true); + app.getInputManager().addRawInputListener(this); + app.getInputManager().addMapping("_CfgClick", new MouseButtonTrigger(MouseInput.BUTTON_LEFT)); + app.getInputManager().addListener(clickListener, "_CfgClick"); + } + + @Override + protected void onDisable() { + if (bgLayer != null) { guiNode.detachChild(bgLayer); bgLayer = null; } + if (canvasNode != null) { guiNode.detachChild(canvasNode); canvasNode = null; } + panel = null; + rows.clear(); + waitingRow = -1; + app.getInputManager().removeRawInputListener(this); + app.getInputManager().deleteMapping("_CfgClick"); + app.getInputManager().setCursorVisible(false); + } + + @Override protected void cleanup(Application app) {} + + private void buildUI() { + float sw = MenuCanvas.REF_W; + float sh = MenuCanvas.REF_H; + + bgLayer = MenuCanvas.createBgLayer(app.getAssetManager(), app.getCamera()); + canvasNode = MenuCanvas.createCanvas(app.getCamera()); + guiNode.attachChild(bgLayer); + guiNode.attachChild(canvasNode); + + panel = new Node("cfg-panel"); + + float pw = 720, ph = 440; + float px = (sw - pw) / 2f, py = (sh - ph) / 2f; + panel.attachChild(NinePatch.panel(app.getAssetManager()).build(px, py, pw, ph, -1)); + + BitmapText title = text(t("menu.controls.title"), 20, COL_TEXT); + centerText(title, px, py + ph - 40, pw); + panel.attachChild(title); + + BitmapText hint = text(t("menu.controls.hint"), 14, new ColorRGBA(0.7f, 0.7f, 0.7f, 1f)); + centerText(hint, px, py + ph - 70, pw); + panel.attachChild(hint); + + float rowX = px + 30; + float keyX = px + pw - 220; + float rowW = 180; + float rowH = 36; + float startY = py + ph - 110; + float stepY = 48; + + for (int i = 0; i < KeyBindings.ENTRIES.length; i++) { + String[] entry = KeyBindings.ENTRIES[i]; + float ry = startY - i * stepY; + + BitmapText lbl = text(t("key." + entry[0]), 16, COL_TEXT); + lbl.setLocalTranslation(rowX, ry + rowH - 8, 0); + panel.attachChild(lbl); + + Geometry bg = addQuad(panel, keyX, ry, rowW, rowH, COL_ROW, 0); + + BitmapText kt = text(KeyNames.of(editCopy.get(entry[0])), 16, COL_TEXT_KEY); + kt.setLocalTranslation(keyX + 10, ry + rowH - 8, 1); + panel.attachChild(kt); + + Row row = new Row(); + row.field = entry[0]; + row.keyText = kt; + row.bg = bg; + row.x = keyX; row.y = ry; row.w = rowW; row.h = rowH; + rows.add(row); + } + + float btnW = 160, btnH = 42; + float btnY = py + 25; + saveBtnX = px + pw / 2f - btnW - 15; + saveBtnY = btnY; + saveBtnW = btnW; + saveBtnH = btnH; + cancelBtnX = px + pw / 2f + 15; + cancelBtnY = btnY; + + panel.attachChild(NinePatch.buttonSave(app.getAssetManager()).build(saveBtnX, saveBtnY, btnW, btnH, 0)); + BitmapText saveLabel = text(t("menu.controls.btn.save"), 16, COL_TEXT); + centerText(saveLabel, saveBtnX, saveBtnY + btnH - 10, btnW); + panel.attachChild(saveLabel); + + panel.attachChild(NinePatch.buttonQuit(app.getAssetManager()).build(cancelBtnX, cancelBtnY, btnW, btnH, 0)); + BitmapText cancelLabel = text(t("menu.controls.btn.cancel"), 16, COL_TEXT); + centerText(cancelLabel, cancelBtnX, cancelBtnY + btnH - 10, btnW); + panel.attachChild(cancelLabel); + + canvasNode.attachChild(panel); + } + + private final ActionListener clickListener = (name, isPressed, tpf) -> { + if (!isPressed) return; + float[] v = toVirtual(app.getInputManager().getCursorPosition()); + + for (int i = 0; i < rows.size(); i++) { + Row r = rows.get(i); + if (hits(v, r.x, r.y, r.w, r.h)) { + waitingRow = i; + r.bg.getMaterial().setColor("Color", COL_ROW_WAIT); + r.keyText.setText("..."); + return; + } + } + + if (hits(v, saveBtnX, saveBtnY, saveBtnW, saveBtnH)) { + liveBindings.copyFrom(editCopy); + KeyBindingStore.save(liveBindings); + if (onSave != null) onSave.run(); + setEnabled(false); + if (onClose != null) onClose.run(); + return; + } + + if (hits(v, cancelBtnX, cancelBtnY, saveBtnW, saveBtnH)) { + setEnabled(false); + if (onClose != null) onClose.run(); + } + }; + + @Override + public void onKeyEvent(KeyInputEvent evt) { + if (!evt.isPressed() || waitingRow < 0) return; + if (evt.getKeyCode() == KeyInput.KEY_ESCAPE) return; + + Row r = rows.get(waitingRow); + editCopy.set(r.field, evt.getKeyCode()); + r.keyText.setText(KeyNames.of(evt.getKeyCode())); + resetRowColor(waitingRow); + waitingRow = -1; + } + + private void resetRowColor(int idx) { + rows.get(idx).bg.getMaterial().setColor("Color", COL_ROW); + } + + 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 static String t(String id) { return TextResolver.get().resolveId(id); } + + @Override public void beginInput() {} + @Override public void endInput() {} + @Override public void onMouseMotionEvent(MouseMotionEvent evt) {} + @Override public void onMouseButtonEvent(MouseButtonEvent evt) {} + @Override public void onJoyAxisEvent(JoyAxisEvent evt) {} + @Override public void onJoyButtonEvent(JoyButtonEvent evt) {} + @Override public void onTouchEvent(TouchEvent evt) {} + + 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(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; + } +} diff --git a/blight-game/src/main/java/de/blight/game/config/GraphicsScreen.java b/blight-game/src/main/java/de/blight/game/config/GraphicsScreen.java index 99614bd..40ee75e 100644 --- a/blight-game/src/main/java/de/blight/game/config/GraphicsScreen.java +++ b/blight-game/src/main/java/de/blight/game/config/GraphicsScreen.java @@ -1,290 +1,291 @@ -package de.blight.game.config; - -import com.jme3.app.Application; -import com.jme3.app.SimpleApplication; -import com.jme3.app.state.BaseAppState; -import com.jme3.font.BitmapFont; -import com.jme3.font.BitmapText; -import com.jme3.input.MouseInput; -import com.jme3.input.controls.ActionListener; -import com.jme3.input.controls.MouseButtonTrigger; -import com.jme3.material.Material; -import com.jme3.material.RenderState; -import com.jme3.math.ColorRGBA; -import com.jme3.math.Vector2f; -import com.jme3.renderer.queue.RenderQueue; -import com.jme3.scene.Geometry; -import com.jme3.scene.Node; -import com.jme3.scene.shape.Quad; -import com.jme3.system.AppSettings; - -public class GraphicsScreen extends BaseAppState { - - private static final ColorRGBA COL_BG = new ColorRGBA(0.05f, 0.05f, 0.08f, 0.88f); - private static final ColorRGBA COL_PANEL = new ColorRGBA(0.10f, 0.10f, 0.16f, 1.00f); - private static final ColorRGBA COL_ROW = new ColorRGBA(0.18f, 0.18f, 0.28f, 1.00f); - private static final ColorRGBA COL_ARROW = new ColorRGBA(0.28f, 0.28f, 0.44f, 1.00f); - private static final ColorRGBA COL_BTN_OK = new ColorRGBA(0.15f, 0.40f, 0.15f, 1.00f); - private static final ColorRGBA COL_BTN_CANCEL = new ColorRGBA(0.40f, 0.15f, 0.15f, 1.00f); - private static final ColorRGBA COL_TEXT = ColorRGBA.White; - private static final ColorRGBA COL_TEXT_VAL = new ColorRGBA(0.85f, 0.85f, 0.50f, 1.00f); - - private static final int[][] RESOLUTIONS = { - {1280, 720}, {1600, 900}, {1920, 1080}, {2560, 1440}, {3840, 2160} - }; - private static final int[] SAMPLES = {0, 2, 4, 8}; - - private static final int ROW_RES = 0; - private static final int ROW_FULL = 1; - private static final int ROW_VSYNC = 2; - private static final int ROW_AA = 3; - - private SimpleApplication app; - private Node guiNode; - private BitmapFont font; - private Node panel; - - private final GraphicsSettings live; - private GraphicsSettings edit; - private final Runnable onClose; - - private int resIdx; - private int samplesIdx; - - // Per-row layout (indexed by ROW_*) - private final float[] cellX = new float[4]; - private final float[] cellY = new float[4]; - private final float[] cellW = new float[4]; - private final float cellH = 36; - private final float arrW = 30; - private final float[] leftX = new float[4]; - private final float[] rightX = new float[4]; - private final BitmapText[] valTexts = new BitmapText[4]; - - private float okX, okY, okW, okH; - private float cancelX, cancelY; - - public GraphicsScreen(GraphicsSettings live, Runnable onClose) { - this.live = live; - this.onClose = onClose; - } - - @Override - protected void initialize(Application app) { - this.app = (SimpleApplication) app; - this.guiNode = this.app.getGuiNode(); - this.font = app.getAssetManager().loadFont("Interface/Fonts/Default.fnt"); - } - - @Override - protected void onEnable() { - edit = new GraphicsSettings(); - edit.width = live.width; edit.height = live.height; - edit.fullscreen = live.fullscreen; - edit.vsync = live.vsync; - edit.samples = live.samples; - - resIdx = 0; - for (int i = 0; i < RESOLUTIONS.length; i++) { - if (RESOLUTIONS[i][0] == edit.width && RESOLUTIONS[i][1] == edit.height) { - resIdx = i; break; - } - } - samplesIdx = 0; - for (int i = 0; i < SAMPLES.length; i++) { - if (SAMPLES[i] == edit.samples) { samplesIdx = i; break; } - } - - buildUI(); - app.getInputManager().setCursorVisible(true); - app.getInputManager().addMapping("_GfxClick", new MouseButtonTrigger(MouseInput.BUTTON_LEFT)); - app.getInputManager().addListener(clickListener, "_GfxClick"); - } - - @Override - protected void onDisable() { - if (panel != null) { guiNode.detachChild(panel); panel = null; } - app.getInputManager().deleteMapping("_GfxClick"); - app.getInputManager().setCursorVisible(false); - } - - @Override protected void cleanup(Application app) {} - - private void buildUI() { - float sw = app.getCamera().getWidth(); - float sh = app.getCamera().getHeight(); - panel = new Node("gfx-panel"); - addQuad(panel, 0, 0, sw, sh, COL_BG, -2); - - float pw = 640, ph = 400; - float px = (sw - pw) / 2f, py = (sh - ph) / 2f; - addQuad(panel, px, py, pw, ph, COL_PANEL, -1); - - BitmapText title = txt("GRAFIKEINSTELLUNGEN", 20, COL_TEXT); - centerText(title, px, py + ph - 42, pw); - panel.attachChild(title); - - String[] labels = {"Auflösung", "Vollbild", "VSync", "Kantenglättung"}; - float lblX = px + 30; - float vx = px + pw - 270; - float vw = 190; - float startY = py + ph - 100; - float step = 60; - - for (int i = 0; i < 4; i++) { - float ry = startY - i * step; - - BitmapText lbl = txt(labels[i], 16, COL_TEXT); - lbl.setLocalTranslation(lblX, ry + cellH - 8, 0); - panel.attachChild(lbl); - - // Left arrow - addQuad(panel, vx - arrW - 6, ry, arrW, cellH, COL_ARROW, 0); - BitmapText lt = txt("<", 16, COL_TEXT); - lt.setLocalTranslation(vx - arrW - 6 + (arrW - lt.getLineWidth()) / 2f, ry + cellH - 8, 1); - panel.attachChild(lt); - - // Value cell - addQuad(panel, vx, ry, vw, cellH, COL_ROW, 0); - - // Right arrow - addQuad(panel, vx + vw + 6, ry, arrW, cellH, COL_ARROW, 0); - BitmapText rt = txt(">", 16, COL_TEXT); - rt.setLocalTranslation(vx + vw + 6 + (arrW - rt.getLineWidth()) / 2f, ry + cellH - 8, 1); - panel.attachChild(rt); - - BitmapText vt = txt("", 16, COL_TEXT_VAL); - panel.attachChild(vt); - valTexts[i] = vt; - - cellX[i] = vx; cellY[i] = ry; cellW[i] = vw; - leftX[i] = vx - arrW - 6; - rightX[i] = vx + vw + 6; - } - - for (int i = 0; i < 4; i++) refreshText(i); - - float bw = 160, bh = 42; - okW = bw; okH = bh; - okX = px + pw / 2f - bw - 10; - okY = py + 22; - cancelX = px + pw / 2f + 10; - cancelY = py + 22; - - addQuad(panel, okX, okY, bw, bh, COL_BTN_OK, 0); - BitmapText okLbl = txt("Übernehmen", 16, COL_TEXT); - centerText(okLbl, okX, okY + bh - 10, bw); - panel.attachChild(okLbl); - - addQuad(panel, cancelX, cancelY, bw, bh, COL_BTN_CANCEL, 0); - BitmapText cancelLbl = txt("Abbrechen", 16, COL_TEXT); - centerText(cancelLbl, cancelX, cancelY + bh - 10, bw); - panel.attachChild(cancelLbl); - - guiNode.attachChild(panel); - } - - private void refreshText(int row) { - String val = switch (row) { - case ROW_RES -> RESOLUTIONS[resIdx][0] + "x" + RESOLUTIONS[resIdx][1]; - case ROW_FULL -> edit.fullscreen ? "An" : "Aus"; - case ROW_VSYNC -> edit.vsync ? "An" : "Aus"; - case ROW_AA -> SAMPLES[samplesIdx] == 0 ? "Aus" : SAMPLES[samplesIdx] + "x MSAA"; - default -> ""; - }; - BitmapText vt = valTexts[row]; - vt.setText(val); - vt.setLocalTranslation( - cellX[row] + (cellW[row] - vt.getLineWidth()) / 2f, - cellY[row] + cellH - 8, - 1 - ); - } - - private final ActionListener clickListener = (name, isPressed, tpf) -> { - if (!isPressed) return; - Vector2f c = app.getInputManager().getCursorPosition(); - - for (int i = 0; i < 4; i++) { - if (hits(c, leftX[i], cellY[i], arrW, cellH)) { cycleRow(i, -1); return; } - if (hits(c, rightX[i], cellY[i], arrW, cellH)) { cycleRow(i, +1); return; } - } - if (hits(c, okX, okY, okW, okH)) { applyAndSave(); return; } - if (hits(c, cancelX, cancelY, okW, okH)) { close(); } - }; - - private void cycleRow(int row, int dir) { - switch (row) { - case ROW_RES: - resIdx = (resIdx + dir + RESOLUTIONS.length) % RESOLUTIONS.length; - edit.width = RESOLUTIONS[resIdx][0]; - edit.height = RESOLUTIONS[resIdx][1]; - break; - case ROW_FULL: - edit.fullscreen = !edit.fullscreen; - break; - case ROW_VSYNC: - edit.vsync = !edit.vsync; - break; - case ROW_AA: - samplesIdx = (samplesIdx + dir + SAMPLES.length) % SAMPLES.length; - edit.samples = SAMPLES[samplesIdx]; - break; - } - refreshText(row); - } - - private void applyAndSave() { - live.width = edit.width; live.height = edit.height; - live.fullscreen = edit.fullscreen; - live.vsync = edit.vsync; - live.samples = edit.samples; - - GraphicsStore.save(live); - - AppSettings s = app.getContext().getSettings(); - s.setResolution(live.width, live.height); - s.setFullscreen(live.fullscreen); - s.setVSync(live.vsync); - s.setSamples(live.samples); - app.setSettings(s); - - close(); - app.restart(); - } - - private void close() { - setEnabled(false); - if (onClose != null) onClose.run(); - } - - // ----------------------------------------------------------------------- - - private Geometry addQuad(Node parent, float x, float y, float w, float h, ColorRGBA color, float z) { - Geometry geo = new Geometry("q", new Quad(w, h)); - Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md"); - mat.setColor("Color", color.clone()); - if (color.a < 1f) { - mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha); - geo.setQueueBucket(RenderQueue.Bucket.Transparent); - } - geo.setMaterial(mat); - geo.setLocalTranslation(x, y, z); - parent.attachChild(geo); - return geo; - } - - private BitmapText txt(String s, int size, ColorRGBA color) { - BitmapText t = new BitmapText(font); - t.setSize(size); t.setColor(color); t.setText(s); - return t; - } - - private void centerText(BitmapText t, float x, float y, float width) { - t.setLocalTranslation(x + (width - t.getLineWidth()) / 2f, y, 1); - } - - private boolean hits(Vector2f p, float x, float y, float w, float h) { - return p.x >= x && p.x <= x + w && p.y >= y && p.y <= y + h; - } -} +package de.blight.game.config; + +import com.jme3.app.Application; +import com.jme3.app.SimpleApplication; +import com.jme3.app.state.BaseAppState; +import com.jme3.font.BitmapFont; +import com.jme3.font.BitmapText; +import com.jme3.input.MouseInput; +import com.jme3.input.controls.ActionListener; +import com.jme3.input.controls.MouseButtonTrigger; +import com.jme3.material.Material; +import com.jme3.material.RenderState; +import com.jme3.math.ColorRGBA; +import com.jme3.math.Vector2f; +import com.jme3.renderer.queue.RenderQueue; +import com.jme3.scene.Geometry; +import com.jme3.scene.Node; +import com.jme3.scene.shape.Quad; +import com.jme3.system.AppSettings; +import de.blight.lang.TextResolver; + +public class GraphicsScreen extends BaseAppState { + + private static final ColorRGBA COL_TEXT = ColorRGBA.White; + private static final ColorRGBA COL_TEXT_VAL = new ColorRGBA(0.85f, 0.85f, 0.50f, 1.00f); + + private static final int[][] RESOLUTIONS = { + {1280, 720}, {1600, 900}, {1920, 1080}, {2560, 1440}, {3840, 2160} + }; + private static final int[] SAMPLES = {0, 2, 4, 8}; + + private static final int ROW_RES = 0; + private static final int ROW_FULL = 1; + private static final int ROW_VSYNC = 2; + private static final int ROW_AA = 3; + + private SimpleApplication app; + private Node guiNode; + private BitmapFont font; + private Node panel; + private Node bgLayer; + private Node canvasNode; + + private final GraphicsSettings live; + private GraphicsSettings edit; + private final Runnable onClose; + + private int resIdx; + private int samplesIdx; + + private final float[] cellX = new float[4]; + private final float[] cellY = new float[4]; + private final float[] cellW = new float[4]; + private final float cellH = 36; + private final float arrW = 30; + private final float[] leftX = new float[4]; + private final float[] rightX = new float[4]; + private final BitmapText[] valTexts = new BitmapText[4]; + + private float okX, okY, okW, okH; + private float cancelX, cancelY; + + public GraphicsScreen(GraphicsSettings live, Runnable onClose) { + this.live = live; + this.onClose = onClose; + } + + @Override + protected void initialize(Application app) { + this.app = (SimpleApplication) app; + this.guiNode = this.app.getGuiNode(); + this.font = app.getAssetManager().loadFont("Interface/Fonts/Default.fnt"); + } + + @Override + protected void onEnable() { + edit = new GraphicsSettings(); + edit.width = live.width; edit.height = live.height; + edit.fullscreen = live.fullscreen; + edit.vsync = live.vsync; + edit.samples = live.samples; + + resIdx = 0; + for (int i = 0; i < RESOLUTIONS.length; i++) { + if (RESOLUTIONS[i][0] == edit.width && RESOLUTIONS[i][1] == edit.height) { + resIdx = i; break; + } + } + samplesIdx = 0; + for (int i = 0; i < SAMPLES.length; i++) { + if (SAMPLES[i] == edit.samples) { samplesIdx = i; break; } + } + + buildUI(); + app.getInputManager().setCursorVisible(true); + app.getInputManager().addMapping("_GfxClick", new MouseButtonTrigger(MouseInput.BUTTON_LEFT)); + app.getInputManager().addListener(clickListener, "_GfxClick"); + } + + @Override + protected void onDisable() { + if (bgLayer != null) { guiNode.detachChild(bgLayer); bgLayer = null; } + if (canvasNode != null) { guiNode.detachChild(canvasNode); canvasNode = null; } + panel = null; + app.getInputManager().deleteMapping("_GfxClick"); + app.getInputManager().setCursorVisible(false); + } + + @Override protected void cleanup(Application app) {} + + private void buildUI() { + float sw = MenuCanvas.REF_W; + float sh = MenuCanvas.REF_H; + + bgLayer = MenuCanvas.createBgLayer(app.getAssetManager(), app.getCamera()); + canvasNode = MenuCanvas.createCanvas(app.getCamera()); + guiNode.attachChild(bgLayer); + guiNode.attachChild(canvasNode); + + panel = new Node("gfx-panel"); + + float pw = 640, ph = 400; + float px = (sw - pw) / 2f, py = (sh - ph) / 2f; + panel.attachChild(NinePatch.panel(app.getAssetManager()).build(px, py, pw, ph, -1)); + + BitmapText title = txt(t("menu.graphics.title"), 20, COL_TEXT); + centerText(title, px, py + ph - 42, pw); + panel.attachChild(title); + + String[] labelKeys = { + "menu.graphics.row.resolution", + "menu.graphics.row.fullscreen", + "menu.graphics.row.vsync", + "menu.graphics.row.aa" + }; + float lblX = px + 30; + float vx = px + pw - 270; + float vw = 190; + float startY = py + ph - 100; + float step = 60; + + for (int i = 0; i < 4; i++) { + float ry = startY - i * step; + + BitmapText lbl = txt(t(labelKeys[i]), 16, COL_TEXT); + lbl.setLocalTranslation(lblX, ry + cellH - 8, 0); + panel.attachChild(lbl); + + panel.attachChild(NinePatch.buttonArrow(app.getAssetManager()).build(vx - arrW - 6, ry, arrW, cellH, 0)); + BitmapText lt = txt("<", 16, COL_TEXT); + lt.setLocalTranslation(vx - arrW - 6 + (arrW - lt.getLineWidth()) / 2f, ry + cellH - 8, 1); + panel.attachChild(lt); + + panel.attachChild(NinePatch.button(app.getAssetManager()).build(vx, ry, vw, cellH, 0)); + + panel.attachChild(NinePatch.buttonArrow(app.getAssetManager()).build(vx + vw + 6, ry, arrW, cellH, 0)); + BitmapText rt = txt(">", 16, COL_TEXT); + rt.setLocalTranslation(vx + vw + 6 + (arrW - rt.getLineWidth()) / 2f, ry + cellH - 8, 1); + panel.attachChild(rt); + + BitmapText vt = txt("", 16, COL_TEXT_VAL); + panel.attachChild(vt); + valTexts[i] = vt; + + cellX[i] = vx; cellY[i] = ry; cellW[i] = vw; + leftX[i] = vx - arrW - 6; + rightX[i] = vx + vw + 6; + } + + for (int i = 0; i < 4; i++) refreshText(i); + + float bw = 160, bh = 42; + okW = bw; okH = bh; + okX = px + pw / 2f - bw - 10; + okY = py + 22; + cancelX = px + pw / 2f + 10; + cancelY = py + 22; + + panel.attachChild(NinePatch.buttonSave(app.getAssetManager()).build(okX, okY, bw, bh, 0)); + BitmapText okLbl = txt(t("menu.graphics.btn.apply"), 16, COL_TEXT); + centerText(okLbl, okX, okY + bh - 10, bw); + panel.attachChild(okLbl); + + 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); + panel.attachChild(cancelLbl); + + canvasNode.attachChild(panel); + } + + private void refreshText(int row) { + String val = switch (row) { + case ROW_RES -> RESOLUTIONS[resIdx][0] + "x" + RESOLUTIONS[resIdx][1]; + case ROW_FULL -> edit.fullscreen ? t("menu.graphics.val.on") : t("menu.graphics.val.off"); + case ROW_VSYNC -> edit.vsync ? t("menu.graphics.val.on") : t("menu.graphics.val.off"); + case ROW_AA -> SAMPLES[samplesIdx] == 0 + ? t("menu.graphics.val.off") + : SAMPLES[samplesIdx] + "x MSAA"; + default -> ""; + }; + BitmapText vt = valTexts[row]; + vt.setText(val); + vt.setLocalTranslation( + cellX[row] + (cellW[row] - vt.getLineWidth()) / 2f, + cellY[row] + cellH - 8, + 1 + ); + } + + private final ActionListener clickListener = (name, isPressed, tpf) -> { + if (!isPressed) return; + float[] v = toVirtual(app.getInputManager().getCursorPosition()); + + for (int i = 0; i < 4; i++) { + if (hits(v, leftX[i], cellY[i], arrW, cellH)) { cycleRow(i, -1); return; } + if (hits(v, rightX[i], cellY[i], arrW, cellH)) { cycleRow(i, +1); return; } + } + if (hits(v, okX, okY, okW, okH)) { applyAndSave(); return; } + if (hits(v, cancelX, cancelY, okW, okH)) { close(); } + }; + + private void cycleRow(int row, int dir) { + switch (row) { + case ROW_RES: + resIdx = (resIdx + dir + RESOLUTIONS.length) % RESOLUTIONS.length; + edit.width = RESOLUTIONS[resIdx][0]; + edit.height = RESOLUTIONS[resIdx][1]; + break; + case ROW_FULL: + edit.fullscreen = !edit.fullscreen; + break; + case ROW_VSYNC: + edit.vsync = !edit.vsync; + break; + case ROW_AA: + samplesIdx = (samplesIdx + dir + SAMPLES.length) % SAMPLES.length; + edit.samples = SAMPLES[samplesIdx]; + break; + } + refreshText(row); + } + + private void applyAndSave() { + live.width = edit.width; live.height = edit.height; + live.fullscreen = edit.fullscreen; + live.vsync = edit.vsync; + live.samples = edit.samples; + + GraphicsStore.save(live); + + AppSettings s = app.getContext().getSettings(); + s.setResolution(live.width, live.height); + s.setFullscreen(live.fullscreen); + s.setVSync(live.vsync); + s.setSamples(live.samples); + app.setSettings(s); + + close(); + app.restart(); + } + + private void close() { + setEnabled(false); + if (onClose != null) onClose.run(); + } + + private 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 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[] v, float x, float y, float w, float h) { + return v[0] >= x && v[0] <= x + w && v[1] >= y && v[1] <= y + h; + } +} diff --git a/blight-game/src/main/java/de/blight/game/config/PauseMenu.java b/blight-game/src/main/java/de/blight/game/config/PauseMenu.java index e87a34d..906e478 100644 --- a/blight-game/src/main/java/de/blight/game/config/PauseMenu.java +++ b/blight-game/src/main/java/de/blight/game/config/PauseMenu.java @@ -1,174 +1,193 @@ -package de.blight.game.config; - -import com.jme3.app.Application; -import com.jme3.app.SimpleApplication; -import com.jme3.app.state.BaseAppState; -import com.jme3.font.BitmapFont; -import com.jme3.font.BitmapText; -import com.jme3.input.MouseInput; -import com.jme3.input.controls.ActionListener; -import com.jme3.input.controls.MouseButtonTrigger; -import com.jme3.material.Material; -import com.jme3.material.RenderState; -import com.jme3.math.ColorRGBA; -import com.jme3.math.Vector2f; -import com.jme3.renderer.queue.RenderQueue; -import com.jme3.scene.Geometry; -import com.jme3.scene.Node; -import com.jme3.scene.shape.Quad; - -public class PauseMenu extends BaseAppState { - - private static final ColorRGBA COL_BG = new ColorRGBA(0.05f, 0.05f, 0.08f, 0.88f); - private static final ColorRGBA COL_PANEL = new ColorRGBA(0.10f, 0.10f, 0.16f, 1.00f); - private static final ColorRGBA COL_BTN = new ColorRGBA(0.18f, 0.18f, 0.28f, 1.00f); - private static final ColorRGBA COL_BTN_DIS = new ColorRGBA(0.12f, 0.12f, 0.18f, 1.00f); - private static final ColorRGBA COL_BTN_QUIT = new ColorRGBA(0.38f, 0.10f, 0.10f, 1.00f); - private static final ColorRGBA COL_TEXT = ColorRGBA.White; - private static final ColorRGBA COL_TEXT_DIS = new ColorRGBA(0.40f, 0.40f, 0.40f, 1.00f); - private static final ColorRGBA COL_TEXT_SUB = new ColorRGBA(0.35f, 0.35f, 0.35f, 1.00f); - - private static final int BTN_GRAFIK = 0; - private static final int BTN_AUDIO = 1; - private static final int BTN_STEUERUNG = 2; - private static final int BTN_SPEICHERN = 3; - private static final int BTN_BEENDEN = 4; - - private static final ColorRGBA COL_BTN_SAVE = new ColorRGBA(0.12f, 0.28f, 0.14f, 1.00f); - - private SimpleApplication app; - private Node guiNode; - private BitmapFont font; - private Node panel; - - private Runnable onGraphics; - private Runnable onControls; - private Runnable onSave; - - // [x, y, w, h] per button - private final float[][] btnBounds = new float[5][4]; - - public PauseMenu(Runnable onSave, Runnable onGraphics, Runnable onControls) { - this.onSave = onSave; - this.onGraphics = onGraphics; - this.onControls = onControls; - } - - @Override - protected void initialize(Application app) { - this.app = (SimpleApplication) app; - this.guiNode = this.app.getGuiNode(); - this.font = app.getAssetManager().loadFont("Interface/Fonts/Default.fnt"); - } - - @Override - protected void onEnable() { - buildUI(); - app.getInputManager().setCursorVisible(true); - app.getInputManager().addMapping("_PauseClick", new MouseButtonTrigger(MouseInput.BUTTON_LEFT)); - app.getInputManager().addListener(clickListener, "_PauseClick"); - } - - @Override - protected void onDisable() { - if (panel != null) { guiNode.detachChild(panel); panel = null; } - app.getInputManager().deleteMapping("_PauseClick"); - app.getInputManager().setCursorVisible(false); - } - - @Override protected void cleanup(Application app) {} - - private void buildUI() { - float sw = app.getCamera().getWidth(); - float sh = app.getCamera().getHeight(); - panel = new Node("pause-panel"); - addQuad(panel, 0, 0, sw, sh, COL_BG, -2); - - float pw = 320, ph = 430; - float px = (sw - pw) / 2f, py = (sh - ph) / 2f; - addQuad(panel, px, py, pw, ph, COL_PANEL, -1); - - BitmapText title = txt("PAUSE", 26, COL_TEXT); - centerText(title, px, py + ph - 48, pw); - panel.attachChild(title); - - String[] labels = {"Grafik", "Audio", "Steuerung", "Speichern", "Beenden"}; - boolean[] enabled = {true, false, true, true, true}; - ColorRGBA[] bgCols = {COL_BTN, COL_BTN_DIS, COL_BTN, COL_BTN_SAVE, COL_BTN_QUIT}; - - float bw = 260, bh = 52; - float bx = px + (pw - bw) / 2f; - float startY = py + ph - 112; - float step = 62; - - for (int i = 0; i < 5; i++) { - float by = startY - i * step; - ColorRGBA txCol = enabled[i] ? COL_TEXT : COL_TEXT_DIS; - - addQuad(panel, bx, by, bw, bh, bgCols[i], 0); - - BitmapText lbl = txt(labels[i], 18, txCol); - if (!enabled[i]) { - lbl.setLocalTranslation(bx + (bw - lbl.getLineWidth()) / 2f, by + bh - 12, 1); - BitmapText hint = txt("Bald verfügbar", 12, COL_TEXT_SUB); - hint.setLocalTranslation(bx + (bw - hint.getLineWidth()) / 2f, by + 14, 1); - panel.attachChild(hint); - } else { - centerText(lbl, bx, by + bh - 16, bw); - } - panel.attachChild(lbl); - - btnBounds[i][0] = bx; btnBounds[i][1] = by; - btnBounds[i][2] = bw; btnBounds[i][3] = bh; - } - - guiNode.attachChild(panel); - } - - private final ActionListener clickListener = (name, isPressed, tpf) -> { - if (!isPressed) return; - Vector2f c = app.getInputManager().getCursorPosition(); - - for (int i = 0; i < 5; i++) { - if (!hits(c, btnBounds[i][0], btnBounds[i][1], btnBounds[i][2], btnBounds[i][3])) continue; - switch (i) { - case BTN_GRAFIK -> { if (onGraphics != null) onGraphics.run(); } - case BTN_AUDIO -> { /* Bald verfügbar */ } - case BTN_STEUERUNG -> { if (onControls != null) onControls.run(); } - case BTN_SPEICHERN -> { if (onSave != null) onSave.run(); } - case BTN_BEENDEN -> app.stop(); - } - return; - } - }; - - // ----------------------------------------------------------------------- - - private Geometry addQuad(Node parent, float x, float y, float w, float h, ColorRGBA color, float z) { - Geometry geo = new Geometry("q", new Quad(w, h)); - Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md"); - mat.setColor("Color", color.clone()); - if (color.a < 1f) { - mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha); - geo.setQueueBucket(RenderQueue.Bucket.Transparent); - } - geo.setMaterial(mat); - geo.setLocalTranslation(x, y, z); - parent.attachChild(geo); - return geo; - } - - private BitmapText txt(String s, int size, ColorRGBA color) { - BitmapText t = new BitmapText(font); - t.setSize(size); t.setColor(color); t.setText(s); - return t; - } - - private void centerText(BitmapText t, float x, float y, float width) { - t.setLocalTranslation(x + (width - t.getLineWidth()) / 2f, y, 1); - } - - private boolean hits(Vector2f p, float x, float y, float w, float h) { - return p.x >= x && p.x <= x + w && p.y >= y && p.y <= y + h; - } -} +package de.blight.game.config; + +import com.jme3.app.Application; +import com.jme3.app.SimpleApplication; +import com.jme3.app.state.BaseAppState; +import com.jme3.font.BitmapFont; +import com.jme3.font.BitmapText; +import com.jme3.input.MouseInput; +import com.jme3.input.controls.ActionListener; +import com.jme3.input.controls.MouseButtonTrigger; +import com.jme3.material.Material; +import com.jme3.material.RenderState; +import com.jme3.math.ColorRGBA; +import com.jme3.math.Vector2f; +import com.jme3.post.FilterPostProcessor; +import com.jme3.renderer.queue.RenderQueue; +import com.jme3.scene.Geometry; +import com.jme3.scene.Node; +import com.jme3.scene.shape.Quad; +import de.blight.game.post.GaussianBlurFilter; +import de.blight.game.scene.WorldScene; +import de.blight.lang.TextResolver; + +public class PauseMenu extends BaseAppState { + + private static final ColorRGBA COL_TEXT = ColorRGBA.White; + + private static final int BTN_GRAFIK = 0; + private static final int BTN_AUDIO = 1; + private static final int BTN_STEUERUNG = 2; + private static final int BTN_SPEICHERN = 3; + private static final int BTN_BEENDEN = 4; + + private SimpleApplication app; + private Node guiNode; + private BitmapFont font; + private Node panel; + private Node bgLayer; + private Node canvasNode; + + private Runnable onSave; + private Runnable onGraphics; + private Runnable onAudio; + private Runnable onControls; + + private GaussianBlurFilter blurFilter; + + private final float[][] btnBounds = new float[5][4]; + + public PauseMenu(Runnable onSave, Runnable onGraphics, Runnable onAudio, Runnable onControls) { + this.onSave = onSave; + this.onGraphics = onGraphics; + this.onAudio = onAudio; + this.onControls = onControls; + } + + @Override + protected void initialize(Application app) { + this.app = (SimpleApplication) app; + this.guiNode = this.app.getGuiNode(); + this.font = app.getAssetManager().loadFont("Interface/Fonts/Default.fnt"); + } + + @Override + protected void onEnable() { + buildUI(); + app.getInputManager().setCursorVisible(true); + app.getInputManager().addMapping("_PauseClick", new MouseButtonTrigger(MouseInput.BUTTON_LEFT)); + app.getInputManager().addListener(clickListener, "_PauseClick"); + addBlur(); + } + + @Override + protected void onDisable() { + removeBlur(); + if (bgLayer != null) { guiNode.detachChild(bgLayer); bgLayer = null; } + if (canvasNode != null) { guiNode.detachChild(canvasNode); canvasNode = null; } + panel = null; + app.getInputManager().deleteMapping("_PauseClick"); + app.getInputManager().setCursorVisible(false); + } + + private void addBlur() { + WorldScene ws = getApplication().getStateManager().getState(WorldScene.class); + if (ws == null) return; + FilterPostProcessor fpp = ws.getSharedFPP(); + if (fpp == null) return; + blurFilter = new GaussianBlurFilter(6f); + fpp.addFilter(blurFilter); + } + + private void removeBlur() { + if (blurFilter == null) return; + WorldScene ws = getApplication().getStateManager().getState(WorldScene.class); + if (ws != null) { + FilterPostProcessor fpp = ws.getSharedFPP(); + if (fpp != null) fpp.removeFilter(blurFilter); + } + blurFilter = null; + } + + @Override protected void cleanup(Application app) {} + + private void buildUI() { + float sw = MenuCanvas.REF_W; + float sh = MenuCanvas.REF_H; + + bgLayer = MenuCanvas.createPauseBgLayer(app.getAssetManager(), app.getCamera()); + canvasNode = MenuCanvas.createFixedCanvas(app.getCamera()); + guiNode.attachChild(bgLayer); + guiNode.attachChild(canvasNode); + + panel = new Node("pause-panel"); + + float pw = 320, ph = 430; + float px = (sw - pw) / 2f, py = (sh - ph) / 2f; + panel.attachChild(NinePatch.panel(app.getAssetManager()).build(px, py, pw, ph, -1)); + + BitmapText title = txt(t("menu.pause.title"), 26, COL_TEXT); + centerText(title, px, py + ph - 48, pw); + panel.attachChild(title); + + String[] labelKeys = { + "menu.pause.btn.graphics", + "menu.pause.btn.audio", + "menu.pause.btn.controls", + "menu.pause.btn.save", + "menu.pause.btn.quit" + }; + + float bw = 260, bh = 52; + float bx = px + (pw - bw) / 2f; + float startY = py + ph - 112; + float step = 62; + + for (int i = 0; i < 5; i++) { + float by = startY - i * step; + NinePatch btnPatch = switch (i) { + case 3 -> NinePatch.buttonSave(app.getAssetManager()); + case 4 -> NinePatch.buttonQuit(app.getAssetManager()); + 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); + panel.attachChild(lbl); + + btnBounds[i][0] = bx; btnBounds[i][1] = by; + btnBounds[i][2] = bw; btnBounds[i][3] = bh; + } + + canvasNode.attachChild(panel); + } + + private final ActionListener clickListener = (name, isPressed, tpf) -> { + if (!isPressed) return; + Vector2f c = app.getInputManager().getCursorPosition(); + // Kein Scaling – einfach den Zentrumversatz abziehen + float ox = (app.getCamera().getWidth() - MenuCanvas.REF_W) / 2f; + float oy = (app.getCamera().getHeight() - MenuCanvas.REF_H) / 2f; + float vx = c.x - ox; + float vy = c.y - oy; + + for (int i = 0; i < 5; i++) { + if (!hits(vx, vy, btnBounds[i][0], btnBounds[i][1], btnBounds[i][2], btnBounds[i][3])) continue; + switch (i) { + case BTN_GRAFIK -> { if (onGraphics != null) onGraphics.run(); } + case BTN_AUDIO -> { if (onAudio != null) onAudio.run(); } + case BTN_STEUERUNG -> { if (onControls != null) onControls.run(); } + case BTN_SPEICHERN -> { if (onSave != null) onSave.run(); } + 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; + } +} diff --git a/blight-game/src/main/java/de/blight/game/control/PlayerInputControl.java b/blight-game/src/main/java/de/blight/game/control/PlayerInputControl.java index 8628a86..2d9284e 100644 --- a/blight-game/src/main/java/de/blight/game/control/PlayerInputControl.java +++ b/blight-game/src/main/java/de/blight/game/control/PlayerInputControl.java @@ -56,6 +56,19 @@ public class PlayerInputControl { private String runningClip; 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 surfaceQuery; + private float lastLeftFootY = Float.MAX_VALUE; + private float lastRightFootY = Float.MAX_VALUE; // ── Anim-Offsets ───────────────────────────────────────────────────────── private Map animOffsets = new LinkedHashMap<>(); @@ -141,6 +154,10 @@ public class PlayerInputControl { log.info("[AnimCtx] AnimComposer gefunden: {}", animComposer != null); skinningControl = findSkinningControl(visual); log.info("[AnimCtx] SkinningControl gefunden: {}", skinningControl != null); + if (skinningControl != null) { + armature = skinningControl.getArmature(); + logJointNames(armature); + } if (visual != null) { visualBaseTranslation = visual.getLocalTranslation().clone(); } @@ -281,6 +298,76 @@ public class PlayerInputControl { 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. * Während der Navigation werden WASD-Eingaben ignoriert. @@ -363,6 +450,8 @@ public class PlayerInputControl { visual.setLocalTranslation(visualBaseTranslation.clone().addLocal(animOffsetCurrent)); } + if (inputsBlocked) return; + if (paused) { if (autopilotDir != null) { autopilotDir = null; @@ -502,6 +591,8 @@ public class PlayerInputControl { playAction(target); currentAnim = target; } + + pollFootsteps(); } 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 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. */ private com.jme3.anim.SkinningControl findSkinningControl(Spatial s) { if (s == null) { diff --git a/blight-game/src/main/java/de/blight/game/post/GaussianBlurFilter.java b/blight-game/src/main/java/de/blight/game/post/GaussianBlurFilter.java new file mode 100644 index 0000000..b40ddc2 --- /dev/null +++ b/blight-game/src/main/java/de/blight/game/post/GaussianBlurFilter.java @@ -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; + } +} diff --git a/blight-game/src/main/java/de/blight/game/scene/WorldScene.java b/blight-game/src/main/java/de/blight/game/scene/WorldScene.java index f884136..277c238 100644 --- a/blight-game/src/main/java/de/blight/game/scene/WorldScene.java +++ b/blight-game/src/main/java/de/blight/game/scene/WorldScene.java @@ -44,10 +44,12 @@ import de.blight.game.state.SculptedMeshState; import de.blight.game.state.WaterBodyState; import de.blight.game.state.DayNightState; import de.blight.game.state.WeatherState; +import de.blight.game.state.DialogHudState; import de.blight.game.state.InteractionHudState; import de.blight.game.state.InventoryState; import de.blight.game.state.WorldInteractableState; import de.blight.game.state.WorldItemsState; +import de.blight.game.state.WorldNpcsState; import de.blight.game.state.StoneWorldState; import de.blight.game.state.WorldObjectsState; import de.blight.game.state.WorldLightState; @@ -83,15 +85,89 @@ public class WorldScene extends BaseAppState { private CharacterControl physicsChar; private boolean animContextReady = false; private boolean physicsCharPending = false; - private float spawnX = 0f; - private float spawnY = 5f; - private float spawnZ = 0f; + private float spawnX = 0f; + private float spawnY = 5f; + 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) { this.keyBindings = keyBindings; } 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. */ public void reloadBindings(KeyBindings kb) { @@ -173,6 +249,12 @@ public class WorldScene extends BaseAppState { playerInput.setPhysicsCharacter(physicsChar); 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) try { 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)); app.getStateManager().attach( new WorldInteractableState(keyBindings, physicsChar, playerInput)); + app.getStateManager().attach(new DialogHudState()); + app.getStateManager().attach( + new WorldNpcsState(keyBindings, physicsChar, playerInput, mc)); app.getStateManager().attach(new InteractionHudState()); inventoryState = new InventoryState(mc, keyBindings); inventoryState.setEnabled(false); @@ -225,12 +310,26 @@ public class WorldScene extends BaseAppState { } if (!animContextReady && animLib != null && animLib.isInitialized()) { - setupAnimationContext(); - animContextReady = true; + animContextReady = true; // zuerst setzen – verhindert endlose Wiederholung bei Exception + 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); 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) if (terrainMaterial != null && dayNight != null && dayNight.getSunLight() != null) { @@ -319,6 +418,16 @@ public class WorldScene extends BaseAppState { if (characterVisual != null) { 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. @@ -500,22 +609,28 @@ public class WorldScene extends BaseAppState { } } - // Spawn-Priorität: 1) Editor-Property 2) Spielstand 3) Karten-Default - String propX = System.getProperty("blight.temp.spawn.x"); - String propZ = System.getProperty("blight.temp.spawn.z"); + // 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 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) { - spawnX = Float.parseFloat(propX); - spawnZ = propZ != null ? Float.parseFloat(propZ) : (loadedMapData != null ? loadedMapData.spawnZ : 0f); + spawnX = Float.parseFloat(propX); + spawnZ = propZ != null ? Float.parseFloat(propZ) : (loadedMapData != null ? loadedMapData.spawnZ : 0f); + spawnYaw = propYaw != null ? Float.parseFloat(propYaw) : 0f; } else { - de.blight.game.state.SaveGameState saveState = + de.blight.game.state.SaveGameState saveState = isNewGame ? null : app.getStateManager().getState(de.blight.game.state.SaveGameState.class); if (saveState != null && saveState.getSave().character.positionSaved) { - spawnX = saveState.getSave().character.x; - spawnY = saveState.getSave().character.y; - spawnZ = saveState.getSave().character.z; + spawnX = saveState.getSave().character.x; + spawnY = saveState.getSave().character.y; + spawnZ = saveState.getSave().character.z; + spawnYaw = loadedMapData != null ? loadedMapData.spawnYaw : 0f; } else if (loadedMapData != null) { - spawnX = loadedMapData.spawnX; - spawnZ = loadedMapData.spawnZ; + spawnX = loadedMapData.spawnX; + spawnZ = loadedMapData.spawnZ; + spawnYaw = loadedMapData.spawnYaw; } } log.info("[WorldScene] SpawnXZ: X={} Z={}", spawnX, spawnZ); @@ -545,6 +660,12 @@ public class WorldScene extends BaseAppState { terrainChunkState.setSpawnHint(spawnX, spawnZ); 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[8+i] = new ColorRGBA(0.45f, 0.32f, 0.25f, 1f); 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, buildTextureArray(diffPaths, diffFb, assetManager)); diff --git a/blight-game/src/main/java/de/blight/game/state/AmbientSoundSystem.java b/blight-game/src/main/java/de/blight/game/state/AmbientSoundSystem.java index 29103af..1a8d26c 100644 --- a/blight-game/src/main/java/de/blight/game/state/AmbientSoundSystem.java +++ b/blight-game/src/main/java/de/blight/game/state/AmbientSoundSystem.java @@ -18,23 +18,29 @@ import java.util.ArrayList; import java.util.List; /** - * Distanzbasierter Ambient-Sound pro Polygon-Bereich. - * Innerhalb des Polygons: volle Lautstärke. - * Außerhalb: lineares Fade bis zur Reichweite (volume * CROSSFADE_SCALE Einheiten). + * Distanzbasierter Ambient-Sound pro Polygon-Bereich, jetzt mit Stereo-Panning: + * - Innerhalb des Polygons: nicht-positional (Rundum-Klang). + * - 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 { 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 FADE_DURATION = 2f; // Sekunden für vollen Lautstärke-Hub + private static final float CROSSFADE_SCALE = 30f; + 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 AssetManager assets; private Node rootNode; - private final List data = new ArrayList<>(); - private final List sounds = new ArrayList<>(); - private final List attached = new ArrayList<>(); + private final List data = new ArrayList<>(); + private final List sounds = new ArrayList<>(); + private final List attached = new ArrayList<>(); + private final List wasInside = new ArrayList<>(); 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); node.setLooping(true); 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); sounds.add(node); attached.add(false); + wasInside.add(false); } catch (Exception e) { log.warn("[AmbientSoundSystem] Sound nicht ladbar '{}': {}", area.soundPath(), e.getMessage()); } @@ -77,6 +87,7 @@ public class AmbientSoundSystem extends BaseAppState { data.clear(); sounds.clear(); attached.clear(); + wasInside.clear(); } @Override protected void onEnable() {} @@ -95,9 +106,8 @@ public class AmbientSoundSystem extends BaseAppState { AudioNode node = sounds.get(i); float target = computeTarget(area); float cur = node.getVolume(); - boolean wasOn = attached.get(i); - if (target > 0f && !wasOn) { + if (target > 0f && !attached.get(i)) { node.setVolume(0f); rootNode.attachChild(node); node.play(); @@ -106,6 +116,9 @@ public class AmbientSoundSystem extends BaseAppState { } if (attached.get(i)) { + // Stereo-Panning: AudioNode zur richtigen Richtung verschieben + updateNodePosition(node, area, i); + float step = area.volume() * tpf / FADE_DURATION; float nv = target > cur ? Math.min(cur + step, target) @@ -122,11 +135,33 @@ public class AmbientSoundSystem extends BaseAppState { } } - /** - * Zielvolumen basierend auf signiertem Abstand zur Polygongrenze. - * Innen (≥0): volle Lautstärke. - * Außen: linear von voll (Grenze) auf null (hearRange = volume * CROSSFADE_SCALE). - */ + // ── AudioNode-Position für Stereo-Panning ──────────────────────────────── + + private void updateNodePosition(AudioNode node, PlacedSoundArea area, int i) { + 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) { float signedDist = signedDistToPolygon(playerPos.x, playerPos.z, area.pointsX(), area.pointsZ()); if (signedDist >= 0f) return area.volume(); @@ -135,18 +170,38 @@ public class AmbientSoundSystem extends BaseAppState { return area.volume() * (1f + signedDist / hearRange); } - /** - * Positiv = Spieler ist innen (Distanz zur nächsten Kante). - * Negativ = Spieler ist außen (negierte Distanz zur nächsten Kante). - */ + /** Positiv = Spieler ist innen, Negativ = Spieler ist außen. */ private static float signedDistToPolygon(float px, float pz, float[] xs, float[] zs) { float edgeDist = minDistToPolygonEdge(px, pz, xs, zs); 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; 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++) { float d2 = pointToSegmentDist2(px, pz, xs[j], zs[j], xs[i], zs[i]); if (d2 < minD2) minD2 = d2; diff --git a/blight-game/src/main/java/de/blight/game/state/AudioSettingsState.java b/blight-game/src/main/java/de/blight/game/state/AudioSettingsState.java new file mode 100644 index 0000000..edcde19 --- /dev/null +++ b/blight-game/src/main/java/de/blight/game/state/AudioSettingsState.java @@ -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)); } +} diff --git a/blight-game/src/main/java/de/blight/game/state/DialogHudState.java b/blight-game/src/main/java/de/blight/game/state/DialogHudState.java new file mode 100644 index 0000000..bbb0b50 --- /dev/null +++ b/blight-game/src/main/java/de/blight/game/state/DialogHudState.java @@ -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. + * + *

Phasen

+ *
+ *   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"
+ * 
+ * + * 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> 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 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 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 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 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 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 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 wrapText(String text, int maxChars) { + List 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> paginate(List lines, int linesPerPage) { + List> 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) {} +} diff --git a/blight-game/src/main/java/de/blight/game/state/InteractionHudState.java b/blight-game/src/main/java/de/blight/game/state/InteractionHudState.java index 4e49e8a..5ddeb92 100644 --- a/blight-game/src/main/java/de/blight/game/state/InteractionHudState.java +++ b/blight-game/src/main/java/de/blight/game/state/InteractionHudState.java @@ -10,6 +10,7 @@ import com.jme3.math.Vector3f; import com.jme3.renderer.Camera; import com.jme3.scene.Node; import com.jme3.scene.Spatial; +import de.blight.lang.TextResolver; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -23,9 +24,12 @@ public class InteractionHudState extends BaseAppState { private static final float Y_OFFSET = 0.6f; - private Camera cam; - private Node guiNode; - private WorldItemsState worldItems; + private Camera cam; + private Node guiNode; + private WorldItemsState worldItems; + private WorldInteractableState worldInteractables; + private WorldNpcsState worldNpcs; + private DialogHudState dialogHud; private BitmapText labelText; @@ -47,9 +51,14 @@ public class InteractionHudState extends BaseAppState { @Override 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) log.warn("[InteractionHud] WorldItemsState nicht gefunden."); + if (worldInteractables == null) + log.warn("[InteractionHud] WorldInteractableState nicht gefunden."); } @Override @@ -66,40 +75,54 @@ public class InteractionHudState extends BaseAppState { @Override public void update(float tpf) { - if (worldItems == null) { + // Label ausblenden wenn Dialog aktiv + if (dialogHud != null && dialogHud.isActive()) { labelText.setCullHint(Spatial.CullHint.Always); return; } - int idx = worldItems.getHoveredIdx(); - if (idx < 0) { + String labelKey = null; + 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); return; } - String name = worldItems.getHoveredItemName(); - 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 + Vector3f screenV = cam.getScreenCoordinates(worldPos.add(0f, Y_OFFSET, 0f)); if (screenV.z >= 1f) { labelText.setCullHint(Spatial.CullHint.Always); return; } - labelText.setText(name); + String text = TextResolver.get().resolveId(labelKey); + labelText.setText(text); float textW = labelText.getLineWidth(); labelText.setLocalTranslation(screenV.x - textW * 0.5f, screenV.y, 1f); labelText.setCullHint(Spatial.CullHint.Inherit); diff --git a/blight-game/src/main/java/de/blight/game/state/InventoryState.java b/blight-game/src/main/java/de/blight/game/state/InventoryState.java index b701add..39c0ba2 100644 --- a/blight-game/src/main/java/de/blight/game/state/InventoryState.java +++ b/blight-game/src/main/java/de/blight/game/state/InventoryState.java @@ -20,9 +20,13 @@ import com.jme3.renderer.queue.RenderQueue; import com.jme3.scene.Geometry; import com.jme3.scene.Node; import com.jme3.scene.shape.Quad; +import com.jme3.post.FilterPostProcessor; import com.jme3.texture.Texture; import de.blight.common.model.*; 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 java.util.*; @@ -82,6 +86,9 @@ public class InventoryState extends BaseAppState { private Node guiNode; private Node panel; private Node gridNode; + private Node bgLayer; + private Node canvasNode; + private GaussianBlurFilter blurFilter; // ── Daten ───────────────────────────────────────────────────────────────── @@ -137,7 +144,14 @@ public class InventoryState extends BaseAppState { app.getInputManager().addListener(scrollListener, MAP_SCROLL_UP, MAP_SCROLL_DN); app.getInputManager().addListener(clickListener, MAP_CLICK); WorldScene ws = app.getStateManager().getState(WorldScene.class); - if (ws != null) ws.setPaused(true); + if (ws != null) { + ws.setPaused(true); + FilterPostProcessor fpp = ws.getSharedFPP(); + if (fpp != null) { + blurFilter = new GaussianBlurFilter(6f); + fpp.addFilter(blurFilter); + } + } } @Override @@ -147,7 +161,14 @@ public class InventoryState extends BaseAppState { app.getInputManager().removeListener(clickListener); app.getInputManager().setCursorVisible(false); 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 @@ -173,10 +194,10 @@ public class InventoryState extends BaseAppState { private final ActionListener clickListener = (name, pressed, tpf) -> { 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++) { 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]); return; } @@ -186,8 +207,13 @@ public class InventoryState extends BaseAppState { // ── Haupt-Panel aufbauen ────────────────────────────────────────────────── private void buildPanel() { - float sw = app.getCamera().getWidth(); - float sh = app.getCamera().getHeight(); + float sw = MenuCanvas.REF_W; + 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 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; panel = new Node("inv-panel"); - quad(panel, 0, 0, sw, sh, COL_OVERLAY, -20); // Verdunkelung - quad(panel, px, py, pw, ph, COL_PANEL, -19); // Hauptpanel + panel.attachChild(NinePatch.panel(assetManager).build(px, py, pw, ph, -19)); quad(panel, px, py + ph - HDR_H, pw, HDR_H, COL_HDR, -18); // Header-Balken // Titel @@ -213,7 +238,7 @@ public class InventoryState extends BaseAppState { // Tabs aufbauen buildTabs(px, py, pw, ph); - guiNode.attachChild(panel); + canvasNode.attachChild(panel); } 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++) { boolean active = tabOrder[i] == activeTab; float tx = tabX0 + i * tabW; - quad(panel, tx, tabY, tabW - 4, TAB_H, active ? COL_TAB_ON : COL_TAB_OFF, -18); + 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); lbl.setLocalTranslation(tx + (tabW - 4 - lbl.getLineWidth()) / 2f, tabY + TAB_H - 8, -17); panel.attachChild(lbl); @@ -341,9 +367,7 @@ public class InventoryState extends BaseAppState { // ── Einzel-Zelle ────────────────────────────────────────────────────────── private void buildCell(Node parent, Item item, int count, float x, float y) { - // Zell-Hintergrund + Rand - quad(parent, x, y, CELL_W, CELL_H, COL_CELL_FRAME, -18); - quad(parent, x + 1, y + 1, CELL_W - 2, CELL_H - 2, COL_CELL, -17); + parent.attachChild(NinePatch.itemCell(assetManager).build(x, y, CELL_W, CELL_H, -18)); // Thumbnail float thumbX = x + (CELL_W - THUMB_SZ) / 2f; @@ -415,8 +439,18 @@ public class InventoryState extends BaseAppState { .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() { - 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) { diff --git a/blight-game/src/main/java/de/blight/game/state/NewGameIntroState.java b/blight-game/src/main/java/de/blight/game/state/NewGameIntroState.java new file mode 100644 index 0000000..7f7e9c0 --- /dev/null +++ b/blight-game/src/main/java/de/blight/game/state/NewGameIntroState.java @@ -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() {} +} diff --git a/blight-game/src/main/java/de/blight/game/state/OceanSoundState.java b/blight-game/src/main/java/de/blight/game/state/OceanSoundState.java new file mode 100644 index 0000000..79bc8a4 --- /dev/null +++ b/blight-game/src/main/java/de/blight/game/state/OceanSoundState.java @@ -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; + } +} diff --git a/blight-game/src/main/java/de/blight/game/state/WorldInteractableState.java b/blight-game/src/main/java/de/blight/game/state/WorldInteractableState.java index 64f0a8f..b49016e 100644 --- a/blight-game/src/main/java/de/blight/game/state/WorldInteractableState.java +++ b/blight-game/src/main/java/de/blight/game/state/WorldInteractableState.java @@ -9,6 +9,7 @@ import com.jme3.input.controls.ActionListener; import com.jme3.input.controls.KeyTrigger; import com.jme3.input.controls.MouseButtonTrigger; import com.jme3.math.Vector3f; +import com.jme3.renderer.Camera; import com.jme3.scene.Spatial; import de.blight.common.PlacedModel; import de.blight.common.PlacedModelIO; @@ -82,7 +83,8 @@ public class WorldInteractableState extends BaseAppState { private record InteractableEntry( float worldX, float worldY, float worldZ, InteractableType type, - String interactableId + String interactableId, + String labelKey ) {} private final List entries = new ArrayList<>(); @@ -98,6 +100,9 @@ public class WorldInteractableState extends BaseAppState { WALKING_BACK } + private Camera cam; + private int hoveredIdx = -1; + private Phase phase = Phase.IDLE; private int targetIdx = -1; private float walkTimer = 0f; @@ -122,13 +127,20 @@ public class WorldInteractableState extends BaseAppState { @Override protected void initialize(Application app) { this.inputManager = app.getInputManager(); + this.cam = app.getCamera(); try { List models = PlacedModelIO.load(); for (PlacedModel m : models) { if (m.interactableType() == null || m.interactableType().isBlank()) continue; InteractableType t = InteractableType.fromString(m.interactableType()); - if (t == InteractableType.BED || t == InteractableType.BENCH) { - entries.add(new InteractableEntry(m.x(), m.y(), m.z(), t, m.interactableId())); + if (t == InteractableType.BENCH) { + 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()); @@ -158,6 +170,7 @@ public class WorldInteractableState extends BaseAppState { @Override public void update(float tpf) { + updateHover(); if (phase == Phase.WALKING_BACK) walkTimer += tpf; if (benchPendingId != null) { benchPendingTimer += tpf; @@ -183,6 +196,58 @@ public class WorldInteractableState extends BaseAppState { 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 ─────────────────────────────────────────── private int findNearestInRange() { diff --git a/blight-game/src/main/java/de/blight/game/state/WorldItemsState.java b/blight-game/src/main/java/de/blight/game/state/WorldItemsState.java index 0e4f30c..54e6ef5 100644 --- a/blight-game/src/main/java/de/blight/game/state/WorldItemsState.java +++ b/blight-game/src/main/java/de/blight/game/state/WorldItemsState.java @@ -342,6 +342,19 @@ public class WorldItemsState extends BaseAppState { 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 ──────────────────────────────────────────────────────── private void updateWalking(float tpf) { diff --git a/blight-game/src/main/java/de/blight/game/state/WorldNpcsState.java b/blight-game/src/main/java/de/blight/game/state/WorldNpcsState.java new file mode 100644 index 0000000..37a0ecd --- /dev/null +++ b/blight-game/src/main/java/de/blight/game/state/WorldNpcsState.java @@ -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 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 interactablePositions = new HashMap<>(); + + /** Aktuell in der Welt gespawnte NPCs. */ + private final List 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 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 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 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 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; + } +} diff --git a/blight-lang/src/main/resources/lang/messages_de.properties b/blight-lang/src/main/resources/lang/messages_de.properties index f849048..1f426b3 100644 --- a/blight-lang/src/main/resources/lang/messages_de.properties +++ b/blight-lang/src/main/resources/lang/messages_de.properties @@ -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 ──────────────────────────────────────────────────────────────────── item.driftwood.name=Treibholz 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.fisherman.greet.hero=Hallo? Ist da jemand? 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 diff --git a/blight-lang/src/main/resources/lang/messages_en.properties b/blight-lang/src/main/resources/lang/messages_en.properties index d44e260..f11e843 100644 --- a/blight-lang/src/main/resources/lang/messages_en.properties +++ b/blight-lang/src/main/resources/lang/messages_en.properties @@ -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 ──────────────────────────────────────────────────────────────────── item.driftwood.name=Driftwood 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.fisherman.greet.hero=Hello? Is someone there? 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 diff --git a/blight-map/src/main/map/blight_lights.bll b/blight-map/src/main/map/blight_lights.bll index 9c57f3b..36a87bd 100644 --- a/blight-map/src/main/map/blight_lights.bll +++ b/blight-map/src/main/map/blight_lights.bll @@ -1,4 +1 @@ # 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 diff --git a/blight-map/src/main/map/blight_map.blm b/blight-map/src/main/map/blight_map.blm index d701846..f23b060 100644 Binary files a/blight-map/src/main/map/blight_map.blm and b/blight-map/src/main/map/blight_map.blm differ diff --git a/blight-map/src/main/map/blight_objects.blo b/blight-map/src/main/map/blight_objects.blo index b6d7db8..4d08e7f 100644 --- a/blight-map/src/main/map/blight_objects.blo +++ b/blight-map/src/main/map/blight_objects.blo @@ -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/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/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 diff --git a/blight-map/src/main/map/chunks/chunk_00_00.blc b/blight-map/src/main/map/chunks/chunk_00_00.blc index b0143cb..f7a2fc4 100644 Binary files a/blight-map/src/main/map/chunks/chunk_00_00.blc and b/blight-map/src/main/map/chunks/chunk_00_00.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_00_01.blc b/blight-map/src/main/map/chunks/chunk_00_01.blc index 6645c39..59e239d 100644 Binary files a/blight-map/src/main/map/chunks/chunk_00_01.blc and b/blight-map/src/main/map/chunks/chunk_00_01.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_00_02.blc b/blight-map/src/main/map/chunks/chunk_00_02.blc index cfab93f..9c49461 100644 Binary files a/blight-map/src/main/map/chunks/chunk_00_02.blc and b/blight-map/src/main/map/chunks/chunk_00_02.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_00_03.blc b/blight-map/src/main/map/chunks/chunk_00_03.blc index ff25f3b..ef6bd0a 100644 Binary files a/blight-map/src/main/map/chunks/chunk_00_03.blc and b/blight-map/src/main/map/chunks/chunk_00_03.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_00_04.blc b/blight-map/src/main/map/chunks/chunk_00_04.blc index f53775f..d3d1485 100644 Binary files a/blight-map/src/main/map/chunks/chunk_00_04.blc and b/blight-map/src/main/map/chunks/chunk_00_04.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_01_00.blc b/blight-map/src/main/map/chunks/chunk_01_00.blc index 1cf65e9..db1cd02 100644 Binary files a/blight-map/src/main/map/chunks/chunk_01_00.blc and b/blight-map/src/main/map/chunks/chunk_01_00.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_01_01.blc b/blight-map/src/main/map/chunks/chunk_01_01.blc index 5980709..8d5c4f3 100644 Binary files a/blight-map/src/main/map/chunks/chunk_01_01.blc and b/blight-map/src/main/map/chunks/chunk_01_01.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_01_02.blc b/blight-map/src/main/map/chunks/chunk_01_02.blc index 544e075..b97e5b9 100644 Binary files a/blight-map/src/main/map/chunks/chunk_01_02.blc and b/blight-map/src/main/map/chunks/chunk_01_02.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_01_03.blc b/blight-map/src/main/map/chunks/chunk_01_03.blc index 46388e7..9ba902a 100644 Binary files a/blight-map/src/main/map/chunks/chunk_01_03.blc and b/blight-map/src/main/map/chunks/chunk_01_03.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_01_04.blc b/blight-map/src/main/map/chunks/chunk_01_04.blc index 7ce1e00..3cf6fb5 100644 Binary files a/blight-map/src/main/map/chunks/chunk_01_04.blc and b/blight-map/src/main/map/chunks/chunk_01_04.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_02_00.blc b/blight-map/src/main/map/chunks/chunk_02_00.blc index da55c0d..b126b67 100644 Binary files a/blight-map/src/main/map/chunks/chunk_02_00.blc and b/blight-map/src/main/map/chunks/chunk_02_00.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_02_01.blc b/blight-map/src/main/map/chunks/chunk_02_01.blc index 7b12695..5b1ba7d 100644 Binary files a/blight-map/src/main/map/chunks/chunk_02_01.blc and b/blight-map/src/main/map/chunks/chunk_02_01.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_02_02.blc b/blight-map/src/main/map/chunks/chunk_02_02.blc index 6fcedf2..f843d3a 100644 Binary files a/blight-map/src/main/map/chunks/chunk_02_02.blc and b/blight-map/src/main/map/chunks/chunk_02_02.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_02_03.blc b/blight-map/src/main/map/chunks/chunk_02_03.blc index be9effe..c003193 100644 Binary files a/blight-map/src/main/map/chunks/chunk_02_03.blc and b/blight-map/src/main/map/chunks/chunk_02_03.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_02_04.blc b/blight-map/src/main/map/chunks/chunk_02_04.blc index 78ee4ad..29aa045 100644 Binary files a/blight-map/src/main/map/chunks/chunk_02_04.blc and b/blight-map/src/main/map/chunks/chunk_02_04.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_03_00.blc b/blight-map/src/main/map/chunks/chunk_03_00.blc index 4837292..7caa536 100644 Binary files a/blight-map/src/main/map/chunks/chunk_03_00.blc and b/blight-map/src/main/map/chunks/chunk_03_00.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_03_01.blc b/blight-map/src/main/map/chunks/chunk_03_01.blc index 2bf5cd6..3966b7f 100644 Binary files a/blight-map/src/main/map/chunks/chunk_03_01.blc and b/blight-map/src/main/map/chunks/chunk_03_01.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_03_02.blc b/blight-map/src/main/map/chunks/chunk_03_02.blc index cc2d2bf..21bf713 100644 Binary files a/blight-map/src/main/map/chunks/chunk_03_02.blc and b/blight-map/src/main/map/chunks/chunk_03_02.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_03_03.blc b/blight-map/src/main/map/chunks/chunk_03_03.blc index 3503a17..a14a007 100644 Binary files a/blight-map/src/main/map/chunks/chunk_03_03.blc and b/blight-map/src/main/map/chunks/chunk_03_03.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_03_04.blc b/blight-map/src/main/map/chunks/chunk_03_04.blc index 7acf41a..a761c21 100644 Binary files a/blight-map/src/main/map/chunks/chunk_03_04.blc and b/blight-map/src/main/map/chunks/chunk_03_04.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_04_00.blc b/blight-map/src/main/map/chunks/chunk_04_00.blc index 22f6102..9f2bc08 100644 Binary files a/blight-map/src/main/map/chunks/chunk_04_00.blc and b/blight-map/src/main/map/chunks/chunk_04_00.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_04_01.blc b/blight-map/src/main/map/chunks/chunk_04_01.blc index 2392227..adb0470 100644 Binary files a/blight-map/src/main/map/chunks/chunk_04_01.blc and b/blight-map/src/main/map/chunks/chunk_04_01.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_04_02.blc b/blight-map/src/main/map/chunks/chunk_04_02.blc index b3b9be3..fc1308c 100644 Binary files a/blight-map/src/main/map/chunks/chunk_04_02.blc and b/blight-map/src/main/map/chunks/chunk_04_02.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_04_03.blc b/blight-map/src/main/map/chunks/chunk_04_03.blc index 81af5c1..59ccfec 100644 Binary files a/blight-map/src/main/map/chunks/chunk_04_03.blc and b/blight-map/src/main/map/chunks/chunk_04_03.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_04_04.blc b/blight-map/src/main/map/chunks/chunk_04_04.blc index 1423d23..7a0ce00 100644 Binary files a/blight-map/src/main/map/chunks/chunk_04_04.blc and b/blight-map/src/main/map/chunks/chunk_04_04.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_04_05.blc b/blight-map/src/main/map/chunks/chunk_04_05.blc index 9cce2d3..61f1590 100644 Binary files a/blight-map/src/main/map/chunks/chunk_04_05.blc and b/blight-map/src/main/map/chunks/chunk_04_05.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_05_00.blc b/blight-map/src/main/map/chunks/chunk_05_00.blc index 40f9873..f65b163 100644 Binary files a/blight-map/src/main/map/chunks/chunk_05_00.blc and b/blight-map/src/main/map/chunks/chunk_05_00.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_05_01.blc b/blight-map/src/main/map/chunks/chunk_05_01.blc index 9cd9a6c..a05880b 100644 Binary files a/blight-map/src/main/map/chunks/chunk_05_01.blc and b/blight-map/src/main/map/chunks/chunk_05_01.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_05_02.blc b/blight-map/src/main/map/chunks/chunk_05_02.blc index b527d12..4c821a1 100644 Binary files a/blight-map/src/main/map/chunks/chunk_05_02.blc and b/blight-map/src/main/map/chunks/chunk_05_02.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_05_03.blc b/blight-map/src/main/map/chunks/chunk_05_03.blc index ac342ac..41a755c 100644 Binary files a/blight-map/src/main/map/chunks/chunk_05_03.blc and b/blight-map/src/main/map/chunks/chunk_05_03.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_05_04.blc b/blight-map/src/main/map/chunks/chunk_05_04.blc index 3bb5ad0..9bff792 100644 Binary files a/blight-map/src/main/map/chunks/chunk_05_04.blc and b/blight-map/src/main/map/chunks/chunk_05_04.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_05_05.blc b/blight-map/src/main/map/chunks/chunk_05_05.blc index 7b73213..2821313 100644 Binary files a/blight-map/src/main/map/chunks/chunk_05_05.blc and b/blight-map/src/main/map/chunks/chunk_05_05.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_06_00.blc b/blight-map/src/main/map/chunks/chunk_06_00.blc index 6c62c14..8ae8e72 100644 Binary files a/blight-map/src/main/map/chunks/chunk_06_00.blc and b/blight-map/src/main/map/chunks/chunk_06_00.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_06_01.blc b/blight-map/src/main/map/chunks/chunk_06_01.blc index 62e8a1e..0924675 100644 Binary files a/blight-map/src/main/map/chunks/chunk_06_01.blc and b/blight-map/src/main/map/chunks/chunk_06_01.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_06_02.blc b/blight-map/src/main/map/chunks/chunk_06_02.blc index 5d0e12e..859bc78 100644 Binary files a/blight-map/src/main/map/chunks/chunk_06_02.blc and b/blight-map/src/main/map/chunks/chunk_06_02.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_06_03.blc b/blight-map/src/main/map/chunks/chunk_06_03.blc index 2ccf962..526f265 100644 Binary files a/blight-map/src/main/map/chunks/chunk_06_03.blc and b/blight-map/src/main/map/chunks/chunk_06_03.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_06_04.blc b/blight-map/src/main/map/chunks/chunk_06_04.blc index 64f83e7..5d33cae 100644 Binary files a/blight-map/src/main/map/chunks/chunk_06_04.blc and b/blight-map/src/main/map/chunks/chunk_06_04.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_06_05.blc b/blight-map/src/main/map/chunks/chunk_06_05.blc index 35cc1d6..42bb984 100644 Binary files a/blight-map/src/main/map/chunks/chunk_06_05.blc and b/blight-map/src/main/map/chunks/chunk_06_05.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_07_00.blc b/blight-map/src/main/map/chunks/chunk_07_00.blc index 260c0e4..a9ba6ed 100644 Binary files a/blight-map/src/main/map/chunks/chunk_07_00.blc and b/blight-map/src/main/map/chunks/chunk_07_00.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_07_01.blc b/blight-map/src/main/map/chunks/chunk_07_01.blc index 598b5cc..41a7ec6 100644 Binary files a/blight-map/src/main/map/chunks/chunk_07_01.blc and b/blight-map/src/main/map/chunks/chunk_07_01.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_07_02.blc b/blight-map/src/main/map/chunks/chunk_07_02.blc index 84f832a..ea4d350 100644 Binary files a/blight-map/src/main/map/chunks/chunk_07_02.blc and b/blight-map/src/main/map/chunks/chunk_07_02.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_07_03.blc b/blight-map/src/main/map/chunks/chunk_07_03.blc index 6c79077..fb161bc 100644 Binary files a/blight-map/src/main/map/chunks/chunk_07_03.blc and b/blight-map/src/main/map/chunks/chunk_07_03.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_07_04.blc b/blight-map/src/main/map/chunks/chunk_07_04.blc index 05e665f..b27b5a0 100644 Binary files a/blight-map/src/main/map/chunks/chunk_07_04.blc and b/blight-map/src/main/map/chunks/chunk_07_04.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_07_05.blc b/blight-map/src/main/map/chunks/chunk_07_05.blc index 05f74aa..1608b14 100644 Binary files a/blight-map/src/main/map/chunks/chunk_07_05.blc and b/blight-map/src/main/map/chunks/chunk_07_05.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_08_00.blc b/blight-map/src/main/map/chunks/chunk_08_00.blc index 7faa8a1..d9f0ca4 100644 Binary files a/blight-map/src/main/map/chunks/chunk_08_00.blc and b/blight-map/src/main/map/chunks/chunk_08_00.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_08_01.blc b/blight-map/src/main/map/chunks/chunk_08_01.blc index 17ed433..a78a5ca 100644 Binary files a/blight-map/src/main/map/chunks/chunk_08_01.blc and b/blight-map/src/main/map/chunks/chunk_08_01.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_08_02.blc b/blight-map/src/main/map/chunks/chunk_08_02.blc index 1b95b65..cf3f3e2 100644 Binary files a/blight-map/src/main/map/chunks/chunk_08_02.blc and b/blight-map/src/main/map/chunks/chunk_08_02.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_08_03.blc b/blight-map/src/main/map/chunks/chunk_08_03.blc index f531153..4e67ff1 100644 Binary files a/blight-map/src/main/map/chunks/chunk_08_03.blc and b/blight-map/src/main/map/chunks/chunk_08_03.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_08_04.blc b/blight-map/src/main/map/chunks/chunk_08_04.blc index 7733db3..da3e144 100644 Binary files a/blight-map/src/main/map/chunks/chunk_08_04.blc and b/blight-map/src/main/map/chunks/chunk_08_04.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_08_05.blc b/blight-map/src/main/map/chunks/chunk_08_05.blc index 59849b8..548a85c 100644 Binary files a/blight-map/src/main/map/chunks/chunk_08_05.blc and b/blight-map/src/main/map/chunks/chunk_08_05.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_09_00.blc b/blight-map/src/main/map/chunks/chunk_09_00.blc index cd3e1b3..0d00d78 100644 Binary files a/blight-map/src/main/map/chunks/chunk_09_00.blc and b/blight-map/src/main/map/chunks/chunk_09_00.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_09_01.blc b/blight-map/src/main/map/chunks/chunk_09_01.blc index cb5ca14..dac907e 100644 Binary files a/blight-map/src/main/map/chunks/chunk_09_01.blc and b/blight-map/src/main/map/chunks/chunk_09_01.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_09_02.blc b/blight-map/src/main/map/chunks/chunk_09_02.blc index 4ac42c5..0236bb6 100644 Binary files a/blight-map/src/main/map/chunks/chunk_09_02.blc and b/blight-map/src/main/map/chunks/chunk_09_02.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_09_03.blc b/blight-map/src/main/map/chunks/chunk_09_03.blc index 68442cf..09e9028 100644 Binary files a/blight-map/src/main/map/chunks/chunk_09_03.blc and b/blight-map/src/main/map/chunks/chunk_09_03.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_09_04.blc b/blight-map/src/main/map/chunks/chunk_09_04.blc index 5831769..40cecd2 100644 Binary files a/blight-map/src/main/map/chunks/chunk_09_04.blc and b/blight-map/src/main/map/chunks/chunk_09_04.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_09_05.blc b/blight-map/src/main/map/chunks/chunk_09_05.blc index cc77143..9d45917 100644 Binary files a/blight-map/src/main/map/chunks/chunk_09_05.blc and b/blight-map/src/main/map/chunks/chunk_09_05.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_10_00.blc b/blight-map/src/main/map/chunks/chunk_10_00.blc index 6e54699..e7598c5 100644 Binary files a/blight-map/src/main/map/chunks/chunk_10_00.blc and b/blight-map/src/main/map/chunks/chunk_10_00.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_10_01.blc b/blight-map/src/main/map/chunks/chunk_10_01.blc index 1ccb39a..55cde23 100644 Binary files a/blight-map/src/main/map/chunks/chunk_10_01.blc and b/blight-map/src/main/map/chunks/chunk_10_01.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_10_02.blc b/blight-map/src/main/map/chunks/chunk_10_02.blc index f0e7d96..93dde8f 100644 Binary files a/blight-map/src/main/map/chunks/chunk_10_02.blc and b/blight-map/src/main/map/chunks/chunk_10_02.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_10_03.blc b/blight-map/src/main/map/chunks/chunk_10_03.blc index deed67d..6e9fdc2 100644 Binary files a/blight-map/src/main/map/chunks/chunk_10_03.blc and b/blight-map/src/main/map/chunks/chunk_10_03.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_10_04.blc b/blight-map/src/main/map/chunks/chunk_10_04.blc index 8f358fe..c0ed461 100644 Binary files a/blight-map/src/main/map/chunks/chunk_10_04.blc and b/blight-map/src/main/map/chunks/chunk_10_04.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_10_05.blc b/blight-map/src/main/map/chunks/chunk_10_05.blc index 2167841..7c4f5be 100644 Binary files a/blight-map/src/main/map/chunks/chunk_10_05.blc and b/blight-map/src/main/map/chunks/chunk_10_05.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_11_00.blc b/blight-map/src/main/map/chunks/chunk_11_00.blc index 5755936..0de1cc4 100644 Binary files a/blight-map/src/main/map/chunks/chunk_11_00.blc and b/blight-map/src/main/map/chunks/chunk_11_00.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_11_01.blc b/blight-map/src/main/map/chunks/chunk_11_01.blc index 4966600..8986c9d 100644 Binary files a/blight-map/src/main/map/chunks/chunk_11_01.blc and b/blight-map/src/main/map/chunks/chunk_11_01.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_11_02.blc b/blight-map/src/main/map/chunks/chunk_11_02.blc index 5f3020e..5e47acd 100644 Binary files a/blight-map/src/main/map/chunks/chunk_11_02.blc and b/blight-map/src/main/map/chunks/chunk_11_02.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_11_03.blc b/blight-map/src/main/map/chunks/chunk_11_03.blc index 601ef90..2ab78ce 100644 Binary files a/blight-map/src/main/map/chunks/chunk_11_03.blc and b/blight-map/src/main/map/chunks/chunk_11_03.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_11_04.blc b/blight-map/src/main/map/chunks/chunk_11_04.blc index 3929fe3..1bb8d7b 100644 Binary files a/blight-map/src/main/map/chunks/chunk_11_04.blc and b/blight-map/src/main/map/chunks/chunk_11_04.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_11_05.blc b/blight-map/src/main/map/chunks/chunk_11_05.blc index 0e51b20..c8d9718 100644 Binary files a/blight-map/src/main/map/chunks/chunk_11_05.blc and b/blight-map/src/main/map/chunks/chunk_11_05.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_12_00.blc b/blight-map/src/main/map/chunks/chunk_12_00.blc index cf12349..0676b75 100644 Binary files a/blight-map/src/main/map/chunks/chunk_12_00.blc and b/blight-map/src/main/map/chunks/chunk_12_00.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_12_01.blc b/blight-map/src/main/map/chunks/chunk_12_01.blc index c32298b..46e4203 100644 Binary files a/blight-map/src/main/map/chunks/chunk_12_01.blc and b/blight-map/src/main/map/chunks/chunk_12_01.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_12_02.blc b/blight-map/src/main/map/chunks/chunk_12_02.blc index 6e36c5a..134daa8 100644 Binary files a/blight-map/src/main/map/chunks/chunk_12_02.blc and b/blight-map/src/main/map/chunks/chunk_12_02.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_12_03.blc b/blight-map/src/main/map/chunks/chunk_12_03.blc index 131b32a..96c0ea4 100644 Binary files a/blight-map/src/main/map/chunks/chunk_12_03.blc and b/blight-map/src/main/map/chunks/chunk_12_03.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_12_04.blc b/blight-map/src/main/map/chunks/chunk_12_04.blc index 8906d2e..353fd7c 100644 Binary files a/blight-map/src/main/map/chunks/chunk_12_04.blc and b/blight-map/src/main/map/chunks/chunk_12_04.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_12_05.blc b/blight-map/src/main/map/chunks/chunk_12_05.blc index a6342b1..02ae8fb 100644 Binary files a/blight-map/src/main/map/chunks/chunk_12_05.blc and b/blight-map/src/main/map/chunks/chunk_12_05.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_13_00.blc b/blight-map/src/main/map/chunks/chunk_13_00.blc index 97440e7..8266ae6 100644 Binary files a/blight-map/src/main/map/chunks/chunk_13_00.blc and b/blight-map/src/main/map/chunks/chunk_13_00.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_13_01.blc b/blight-map/src/main/map/chunks/chunk_13_01.blc index dd84063..d8fb307 100644 Binary files a/blight-map/src/main/map/chunks/chunk_13_01.blc and b/blight-map/src/main/map/chunks/chunk_13_01.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_13_02.blc b/blight-map/src/main/map/chunks/chunk_13_02.blc index b25c7ca..395830c 100644 Binary files a/blight-map/src/main/map/chunks/chunk_13_02.blc and b/blight-map/src/main/map/chunks/chunk_13_02.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_13_03.blc b/blight-map/src/main/map/chunks/chunk_13_03.blc index 1398566..51deab2 100644 Binary files a/blight-map/src/main/map/chunks/chunk_13_03.blc and b/blight-map/src/main/map/chunks/chunk_13_03.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_13_04.blc b/blight-map/src/main/map/chunks/chunk_13_04.blc index 103f2e9..b488613 100644 Binary files a/blight-map/src/main/map/chunks/chunk_13_04.blc and b/blight-map/src/main/map/chunks/chunk_13_04.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_13_05.blc b/blight-map/src/main/map/chunks/chunk_13_05.blc index 1632dbf..665cd1a 100644 Binary files a/blight-map/src/main/map/chunks/chunk_13_05.blc and b/blight-map/src/main/map/chunks/chunk_13_05.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_14_00.blc b/blight-map/src/main/map/chunks/chunk_14_00.blc index 3e28466..a3c4c20 100644 Binary files a/blight-map/src/main/map/chunks/chunk_14_00.blc and b/blight-map/src/main/map/chunks/chunk_14_00.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_14_01.blc b/blight-map/src/main/map/chunks/chunk_14_01.blc index 5d25c7c..44d3cfe 100644 Binary files a/blight-map/src/main/map/chunks/chunk_14_01.blc and b/blight-map/src/main/map/chunks/chunk_14_01.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_14_02.blc b/blight-map/src/main/map/chunks/chunk_14_02.blc index cfad5cb..d639195 100644 Binary files a/blight-map/src/main/map/chunks/chunk_14_02.blc and b/blight-map/src/main/map/chunks/chunk_14_02.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_14_03.blc b/blight-map/src/main/map/chunks/chunk_14_03.blc index abc72db..fae1657 100644 Binary files a/blight-map/src/main/map/chunks/chunk_14_03.blc and b/blight-map/src/main/map/chunks/chunk_14_03.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_14_04.blc b/blight-map/src/main/map/chunks/chunk_14_04.blc index 136e467..85b2ca0 100644 Binary files a/blight-map/src/main/map/chunks/chunk_14_04.blc and b/blight-map/src/main/map/chunks/chunk_14_04.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_14_05.blc b/blight-map/src/main/map/chunks/chunk_14_05.blc index 485affe..3ba157e 100644 Binary files a/blight-map/src/main/map/chunks/chunk_14_05.blc and b/blight-map/src/main/map/chunks/chunk_14_05.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_14_06.blc b/blight-map/src/main/map/chunks/chunk_14_06.blc index 9ea6905..7e63074 100644 Binary files a/blight-map/src/main/map/chunks/chunk_14_06.blc and b/blight-map/src/main/map/chunks/chunk_14_06.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_15_00.blc b/blight-map/src/main/map/chunks/chunk_15_00.blc index 976568a..da909de 100644 Binary files a/blight-map/src/main/map/chunks/chunk_15_00.blc and b/blight-map/src/main/map/chunks/chunk_15_00.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_15_01.blc b/blight-map/src/main/map/chunks/chunk_15_01.blc index e9affbf..b9e876c 100644 Binary files a/blight-map/src/main/map/chunks/chunk_15_01.blc and b/blight-map/src/main/map/chunks/chunk_15_01.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_15_02.blc b/blight-map/src/main/map/chunks/chunk_15_02.blc index fcb1011..9d32755 100644 Binary files a/blight-map/src/main/map/chunks/chunk_15_02.blc and b/blight-map/src/main/map/chunks/chunk_15_02.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_15_03.blc b/blight-map/src/main/map/chunks/chunk_15_03.blc index 4f775c5..f1d9161 100644 Binary files a/blight-map/src/main/map/chunks/chunk_15_03.blc and b/blight-map/src/main/map/chunks/chunk_15_03.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_15_04.blc b/blight-map/src/main/map/chunks/chunk_15_04.blc index 7794a61..1e29ce1 100644 Binary files a/blight-map/src/main/map/chunks/chunk_15_04.blc and b/blight-map/src/main/map/chunks/chunk_15_04.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_15_05.blc b/blight-map/src/main/map/chunks/chunk_15_05.blc index 17552a7..df59117 100644 Binary files a/blight-map/src/main/map/chunks/chunk_15_05.blc and b/blight-map/src/main/map/chunks/chunk_15_05.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_15_06.blc b/blight-map/src/main/map/chunks/chunk_15_06.blc index 36b4a0f..c39dbfd 100644 Binary files a/blight-map/src/main/map/chunks/chunk_15_06.blc and b/blight-map/src/main/map/chunks/chunk_15_06.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_16_00.blc b/blight-map/src/main/map/chunks/chunk_16_00.blc index 49dd8b3..b5def23 100644 Binary files a/blight-map/src/main/map/chunks/chunk_16_00.blc and b/blight-map/src/main/map/chunks/chunk_16_00.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_16_01.blc b/blight-map/src/main/map/chunks/chunk_16_01.blc index 4e52462..85efde7 100644 Binary files a/blight-map/src/main/map/chunks/chunk_16_01.blc and b/blight-map/src/main/map/chunks/chunk_16_01.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_16_02.blc b/blight-map/src/main/map/chunks/chunk_16_02.blc index 82d7b46..69f5f85 100644 Binary files a/blight-map/src/main/map/chunks/chunk_16_02.blc and b/blight-map/src/main/map/chunks/chunk_16_02.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_16_03.blc b/blight-map/src/main/map/chunks/chunk_16_03.blc index 72e773c..c949002 100644 Binary files a/blight-map/src/main/map/chunks/chunk_16_03.blc and b/blight-map/src/main/map/chunks/chunk_16_03.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_16_04.blc b/blight-map/src/main/map/chunks/chunk_16_04.blc index 2384248..96b3443 100644 Binary files a/blight-map/src/main/map/chunks/chunk_16_04.blc and b/blight-map/src/main/map/chunks/chunk_16_04.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_16_05.blc b/blight-map/src/main/map/chunks/chunk_16_05.blc index 3e75bfa..068a0aa 100644 Binary files a/blight-map/src/main/map/chunks/chunk_16_05.blc and b/blight-map/src/main/map/chunks/chunk_16_05.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_16_06.blc b/blight-map/src/main/map/chunks/chunk_16_06.blc index 50ce17e..edc0d11 100644 Binary files a/blight-map/src/main/map/chunks/chunk_16_06.blc and b/blight-map/src/main/map/chunks/chunk_16_06.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_17_00.blc b/blight-map/src/main/map/chunks/chunk_17_00.blc index d218475..d422380 100644 Binary files a/blight-map/src/main/map/chunks/chunk_17_00.blc and b/blight-map/src/main/map/chunks/chunk_17_00.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_17_01.blc b/blight-map/src/main/map/chunks/chunk_17_01.blc index e62ec3b..f1ab0f2 100644 Binary files a/blight-map/src/main/map/chunks/chunk_17_01.blc and b/blight-map/src/main/map/chunks/chunk_17_01.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_17_02.blc b/blight-map/src/main/map/chunks/chunk_17_02.blc index 184e1d1..4b4bbfc 100644 Binary files a/blight-map/src/main/map/chunks/chunk_17_02.blc and b/blight-map/src/main/map/chunks/chunk_17_02.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_17_03.blc b/blight-map/src/main/map/chunks/chunk_17_03.blc index 619caf3..f539ec4 100644 Binary files a/blight-map/src/main/map/chunks/chunk_17_03.blc and b/blight-map/src/main/map/chunks/chunk_17_03.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_17_04.blc b/blight-map/src/main/map/chunks/chunk_17_04.blc index 12416da..4fb338c 100644 Binary files a/blight-map/src/main/map/chunks/chunk_17_04.blc and b/blight-map/src/main/map/chunks/chunk_17_04.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_17_05.blc b/blight-map/src/main/map/chunks/chunk_17_05.blc index 3755b60..84e5b55 100644 Binary files a/blight-map/src/main/map/chunks/chunk_17_05.blc and b/blight-map/src/main/map/chunks/chunk_17_05.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_17_06.blc b/blight-map/src/main/map/chunks/chunk_17_06.blc index 2af5642..668b881 100644 Binary files a/blight-map/src/main/map/chunks/chunk_17_06.blc and b/blight-map/src/main/map/chunks/chunk_17_06.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_18_00.blc b/blight-map/src/main/map/chunks/chunk_18_00.blc index d56ea49..e924545 100644 Binary files a/blight-map/src/main/map/chunks/chunk_18_00.blc and b/blight-map/src/main/map/chunks/chunk_18_00.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_18_01.blc b/blight-map/src/main/map/chunks/chunk_18_01.blc index 8af9875..5062658 100644 Binary files a/blight-map/src/main/map/chunks/chunk_18_01.blc and b/blight-map/src/main/map/chunks/chunk_18_01.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_18_02.blc b/blight-map/src/main/map/chunks/chunk_18_02.blc index c46dc8f..6e0a869 100644 Binary files a/blight-map/src/main/map/chunks/chunk_18_02.blc and b/blight-map/src/main/map/chunks/chunk_18_02.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_18_03.blc b/blight-map/src/main/map/chunks/chunk_18_03.blc index 6e66d0d..9fd931d 100644 Binary files a/blight-map/src/main/map/chunks/chunk_18_03.blc and b/blight-map/src/main/map/chunks/chunk_18_03.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_18_04.blc b/blight-map/src/main/map/chunks/chunk_18_04.blc index 645a14d..5b94992 100644 Binary files a/blight-map/src/main/map/chunks/chunk_18_04.blc and b/blight-map/src/main/map/chunks/chunk_18_04.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_18_05.blc b/blight-map/src/main/map/chunks/chunk_18_05.blc index c4fe8ce..2bace97 100644 Binary files a/blight-map/src/main/map/chunks/chunk_18_05.blc and b/blight-map/src/main/map/chunks/chunk_18_05.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_19_00.blc b/blight-map/src/main/map/chunks/chunk_19_00.blc index f9b6139..fdf6b3e 100644 Binary files a/blight-map/src/main/map/chunks/chunk_19_00.blc and b/blight-map/src/main/map/chunks/chunk_19_00.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_19_01.blc b/blight-map/src/main/map/chunks/chunk_19_01.blc index 549504d..ffb9878 100644 Binary files a/blight-map/src/main/map/chunks/chunk_19_01.blc and b/blight-map/src/main/map/chunks/chunk_19_01.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_19_02.blc b/blight-map/src/main/map/chunks/chunk_19_02.blc index 43936e9..767d254 100644 Binary files a/blight-map/src/main/map/chunks/chunk_19_02.blc and b/blight-map/src/main/map/chunks/chunk_19_02.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_19_03.blc b/blight-map/src/main/map/chunks/chunk_19_03.blc index b4da24f..7b04611 100644 Binary files a/blight-map/src/main/map/chunks/chunk_19_03.blc and b/blight-map/src/main/map/chunks/chunk_19_03.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_19_04.blc b/blight-map/src/main/map/chunks/chunk_19_04.blc index 6233b1b..d064752 100644 Binary files a/blight-map/src/main/map/chunks/chunk_19_04.blc and b/blight-map/src/main/map/chunks/chunk_19_04.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_19_05.blc b/blight-map/src/main/map/chunks/chunk_19_05.blc index deae1a3..c1ea405 100644 Binary files a/blight-map/src/main/map/chunks/chunk_19_05.blc and b/blight-map/src/main/map/chunks/chunk_19_05.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_20_00.blc b/blight-map/src/main/map/chunks/chunk_20_00.blc index b1abe1e..2fef66d 100644 Binary files a/blight-map/src/main/map/chunks/chunk_20_00.blc and b/blight-map/src/main/map/chunks/chunk_20_00.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_20_01.blc b/blight-map/src/main/map/chunks/chunk_20_01.blc index 67f54aa..94dc09a 100644 Binary files a/blight-map/src/main/map/chunks/chunk_20_01.blc and b/blight-map/src/main/map/chunks/chunk_20_01.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_20_02.blc b/blight-map/src/main/map/chunks/chunk_20_02.blc index e9f38d8..ab42e60 100644 Binary files a/blight-map/src/main/map/chunks/chunk_20_02.blc and b/blight-map/src/main/map/chunks/chunk_20_02.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_20_03.blc b/blight-map/src/main/map/chunks/chunk_20_03.blc index be25614..97dbc8f 100644 Binary files a/blight-map/src/main/map/chunks/chunk_20_03.blc and b/blight-map/src/main/map/chunks/chunk_20_03.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_20_04.blc b/blight-map/src/main/map/chunks/chunk_20_04.blc index cf61c69..56eaef9 100644 Binary files a/blight-map/src/main/map/chunks/chunk_20_04.blc and b/blight-map/src/main/map/chunks/chunk_20_04.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_21_00.blc b/blight-map/src/main/map/chunks/chunk_21_00.blc index 94db14d..76e02f4 100644 Binary files a/blight-map/src/main/map/chunks/chunk_21_00.blc and b/blight-map/src/main/map/chunks/chunk_21_00.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_21_01.blc b/blight-map/src/main/map/chunks/chunk_21_01.blc index 8b6c109..74c926a 100644 Binary files a/blight-map/src/main/map/chunks/chunk_21_01.blc and b/blight-map/src/main/map/chunks/chunk_21_01.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_21_02.blc b/blight-map/src/main/map/chunks/chunk_21_02.blc index 4f4ebfb..343a606 100644 Binary files a/blight-map/src/main/map/chunks/chunk_21_02.blc and b/blight-map/src/main/map/chunks/chunk_21_02.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_21_03.blc b/blight-map/src/main/map/chunks/chunk_21_03.blc index 4fc9088..4ec2db0 100644 Binary files a/blight-map/src/main/map/chunks/chunk_21_03.blc and b/blight-map/src/main/map/chunks/chunk_21_03.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_21_04.blc b/blight-map/src/main/map/chunks/chunk_21_04.blc index c2cc971..feccb5d 100644 Binary files a/blight-map/src/main/map/chunks/chunk_21_04.blc and b/blight-map/src/main/map/chunks/chunk_21_04.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_22_00.blc b/blight-map/src/main/map/chunks/chunk_22_00.blc index fd0b65c..d654080 100644 Binary files a/blight-map/src/main/map/chunks/chunk_22_00.blc and b/blight-map/src/main/map/chunks/chunk_22_00.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_22_01.blc b/blight-map/src/main/map/chunks/chunk_22_01.blc index f392751..16c2e77 100644 Binary files a/blight-map/src/main/map/chunks/chunk_22_01.blc and b/blight-map/src/main/map/chunks/chunk_22_01.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_22_02.blc b/blight-map/src/main/map/chunks/chunk_22_02.blc index bf82c3b..9c36f99 100644 Binary files a/blight-map/src/main/map/chunks/chunk_22_02.blc and b/blight-map/src/main/map/chunks/chunk_22_02.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_22_03.blc b/blight-map/src/main/map/chunks/chunk_22_03.blc index 1a2f2e1..e3bc134 100644 Binary files a/blight-map/src/main/map/chunks/chunk_22_03.blc and b/blight-map/src/main/map/chunks/chunk_22_03.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_22_04.blc b/blight-map/src/main/map/chunks/chunk_22_04.blc index ea5a486..2d72663 100644 Binary files a/blight-map/src/main/map/chunks/chunk_22_04.blc and b/blight-map/src/main/map/chunks/chunk_22_04.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_23_00.blc b/blight-map/src/main/map/chunks/chunk_23_00.blc index a73fa50..6ec2785 100644 Binary files a/blight-map/src/main/map/chunks/chunk_23_00.blc and b/blight-map/src/main/map/chunks/chunk_23_00.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_23_01.blc b/blight-map/src/main/map/chunks/chunk_23_01.blc index 8d8e414..83bab9a 100644 Binary files a/blight-map/src/main/map/chunks/chunk_23_01.blc and b/blight-map/src/main/map/chunks/chunk_23_01.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_23_02.blc b/blight-map/src/main/map/chunks/chunk_23_02.blc index b30ce60..32036f7 100644 Binary files a/blight-map/src/main/map/chunks/chunk_23_02.blc and b/blight-map/src/main/map/chunks/chunk_23_02.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_23_03.blc b/blight-map/src/main/map/chunks/chunk_23_03.blc index 26f7423..baf88f1 100644 Binary files a/blight-map/src/main/map/chunks/chunk_23_03.blc and b/blight-map/src/main/map/chunks/chunk_23_03.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_23_04.blc b/blight-map/src/main/map/chunks/chunk_23_04.blc index 6a3374a..1bbb092 100644 Binary files a/blight-map/src/main/map/chunks/chunk_23_04.blc and b/blight-map/src/main/map/chunks/chunk_23_04.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_24_00.blc b/blight-map/src/main/map/chunks/chunk_24_00.blc index 0e8b7de..46d7818 100644 Binary files a/blight-map/src/main/map/chunks/chunk_24_00.blc and b/blight-map/src/main/map/chunks/chunk_24_00.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_24_01.blc b/blight-map/src/main/map/chunks/chunk_24_01.blc index 015825b..d3b72f5 100644 Binary files a/blight-map/src/main/map/chunks/chunk_24_01.blc and b/blight-map/src/main/map/chunks/chunk_24_01.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_24_02.blc b/blight-map/src/main/map/chunks/chunk_24_02.blc index 74ba10e..b684868 100644 Binary files a/blight-map/src/main/map/chunks/chunk_24_02.blc and b/blight-map/src/main/map/chunks/chunk_24_02.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_24_03.blc b/blight-map/src/main/map/chunks/chunk_24_03.blc index e1cd3e9..7acf6a0 100644 Binary files a/blight-map/src/main/map/chunks/chunk_24_03.blc and b/blight-map/src/main/map/chunks/chunk_24_03.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_24_04.blc b/blight-map/src/main/map/chunks/chunk_24_04.blc index 733e3a1..bb9a787 100644 Binary files a/blight-map/src/main/map/chunks/chunk_24_04.blc and b/blight-map/src/main/map/chunks/chunk_24_04.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_25_00.blc b/blight-map/src/main/map/chunks/chunk_25_00.blc index 3422f05..6aed81e 100644 Binary files a/blight-map/src/main/map/chunks/chunk_25_00.blc and b/blight-map/src/main/map/chunks/chunk_25_00.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_25_01.blc b/blight-map/src/main/map/chunks/chunk_25_01.blc index da5f1c9..34a141d 100644 Binary files a/blight-map/src/main/map/chunks/chunk_25_01.blc and b/blight-map/src/main/map/chunks/chunk_25_01.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_25_02.blc b/blight-map/src/main/map/chunks/chunk_25_02.blc index e5ccbcc..c5b0503 100644 Binary files a/blight-map/src/main/map/chunks/chunk_25_02.blc and b/blight-map/src/main/map/chunks/chunk_25_02.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_25_03.blc b/blight-map/src/main/map/chunks/chunk_25_03.blc index 08e445b..369382b 100644 Binary files a/blight-map/src/main/map/chunks/chunk_25_03.blc and b/blight-map/src/main/map/chunks/chunk_25_03.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_25_04.blc b/blight-map/src/main/map/chunks/chunk_25_04.blc index bca21c8..d2a2120 100644 Binary files a/blight-map/src/main/map/chunks/chunk_25_04.blc and b/blight-map/src/main/map/chunks/chunk_25_04.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_26_00.blc b/blight-map/src/main/map/chunks/chunk_26_00.blc index 715225c..cfcdb5d 100644 Binary files a/blight-map/src/main/map/chunks/chunk_26_00.blc and b/blight-map/src/main/map/chunks/chunk_26_00.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_26_01.blc b/blight-map/src/main/map/chunks/chunk_26_01.blc index 56f43dc..38f5441 100644 Binary files a/blight-map/src/main/map/chunks/chunk_26_01.blc and b/blight-map/src/main/map/chunks/chunk_26_01.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_26_02.blc b/blight-map/src/main/map/chunks/chunk_26_02.blc index f220d02..863a1a7 100644 Binary files a/blight-map/src/main/map/chunks/chunk_26_02.blc and b/blight-map/src/main/map/chunks/chunk_26_02.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_26_03.blc b/blight-map/src/main/map/chunks/chunk_26_03.blc index a2c3394..251f08a 100644 Binary files a/blight-map/src/main/map/chunks/chunk_26_03.blc and b/blight-map/src/main/map/chunks/chunk_26_03.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_26_04.blc b/blight-map/src/main/map/chunks/chunk_26_04.blc index 2b07db0..aab1420 100644 Binary files a/blight-map/src/main/map/chunks/chunk_26_04.blc and b/blight-map/src/main/map/chunks/chunk_26_04.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_27_00.blc b/blight-map/src/main/map/chunks/chunk_27_00.blc index 25fdd4f..7cbe917 100644 Binary files a/blight-map/src/main/map/chunks/chunk_27_00.blc and b/blight-map/src/main/map/chunks/chunk_27_00.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_27_01.blc b/blight-map/src/main/map/chunks/chunk_27_01.blc index 7147356..3acf36d 100644 Binary files a/blight-map/src/main/map/chunks/chunk_27_01.blc and b/blight-map/src/main/map/chunks/chunk_27_01.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_27_02.blc b/blight-map/src/main/map/chunks/chunk_27_02.blc index 743d81f..6fb7416 100644 Binary files a/blight-map/src/main/map/chunks/chunk_27_02.blc and b/blight-map/src/main/map/chunks/chunk_27_02.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_27_03.blc b/blight-map/src/main/map/chunks/chunk_27_03.blc index 56eb1e8..34f4ae9 100644 Binary files a/blight-map/src/main/map/chunks/chunk_27_03.blc and b/blight-map/src/main/map/chunks/chunk_27_03.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_27_04.blc b/blight-map/src/main/map/chunks/chunk_27_04.blc index fa6514c..8b72488 100644 Binary files a/blight-map/src/main/map/chunks/chunk_27_04.blc and b/blight-map/src/main/map/chunks/chunk_27_04.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_28_00.blc b/blight-map/src/main/map/chunks/chunk_28_00.blc index 1748dc5..7fd8c8a 100644 Binary files a/blight-map/src/main/map/chunks/chunk_28_00.blc and b/blight-map/src/main/map/chunks/chunk_28_00.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_28_01.blc b/blight-map/src/main/map/chunks/chunk_28_01.blc index ce9672e..ee3f71e 100644 Binary files a/blight-map/src/main/map/chunks/chunk_28_01.blc and b/blight-map/src/main/map/chunks/chunk_28_01.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_28_02.blc b/blight-map/src/main/map/chunks/chunk_28_02.blc index d32dfff..4b7af77 100644 Binary files a/blight-map/src/main/map/chunks/chunk_28_02.blc and b/blight-map/src/main/map/chunks/chunk_28_02.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_28_03.blc b/blight-map/src/main/map/chunks/chunk_28_03.blc index 770247a..b16db68 100644 Binary files a/blight-map/src/main/map/chunks/chunk_28_03.blc and b/blight-map/src/main/map/chunks/chunk_28_03.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_28_04.blc b/blight-map/src/main/map/chunks/chunk_28_04.blc index 6ee7a85..e886a56 100644 Binary files a/blight-map/src/main/map/chunks/chunk_28_04.blc and b/blight-map/src/main/map/chunks/chunk_28_04.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_29_00.blc b/blight-map/src/main/map/chunks/chunk_29_00.blc index f3e6049..eec8158 100644 Binary files a/blight-map/src/main/map/chunks/chunk_29_00.blc and b/blight-map/src/main/map/chunks/chunk_29_00.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_29_01.blc b/blight-map/src/main/map/chunks/chunk_29_01.blc index bb88b1b..16fcc82 100644 Binary files a/blight-map/src/main/map/chunks/chunk_29_01.blc and b/blight-map/src/main/map/chunks/chunk_29_01.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_29_02.blc b/blight-map/src/main/map/chunks/chunk_29_02.blc index 9f4dc9f..c9dadcd 100644 Binary files a/blight-map/src/main/map/chunks/chunk_29_02.blc and b/blight-map/src/main/map/chunks/chunk_29_02.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_29_03.blc b/blight-map/src/main/map/chunks/chunk_29_03.blc index 718c89f..3970b5a 100644 Binary files a/blight-map/src/main/map/chunks/chunk_29_03.blc and b/blight-map/src/main/map/chunks/chunk_29_03.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_29_04.blc b/blight-map/src/main/map/chunks/chunk_29_04.blc index 11600b3..6345b79 100644 Binary files a/blight-map/src/main/map/chunks/chunk_29_04.blc and b/blight-map/src/main/map/chunks/chunk_29_04.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_30_00.blc b/blight-map/src/main/map/chunks/chunk_30_00.blc index 3cb39f4..85395fa 100644 Binary files a/blight-map/src/main/map/chunks/chunk_30_00.blc and b/blight-map/src/main/map/chunks/chunk_30_00.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_30_01.blc b/blight-map/src/main/map/chunks/chunk_30_01.blc index 9a22f5a..597ea88 100644 Binary files a/blight-map/src/main/map/chunks/chunk_30_01.blc and b/blight-map/src/main/map/chunks/chunk_30_01.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_30_02.blc b/blight-map/src/main/map/chunks/chunk_30_02.blc index f73ee0f..236cf74 100644 Binary files a/blight-map/src/main/map/chunks/chunk_30_02.blc and b/blight-map/src/main/map/chunks/chunk_30_02.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_30_03.blc b/blight-map/src/main/map/chunks/chunk_30_03.blc index a25c329..3de6435 100644 Binary files a/blight-map/src/main/map/chunks/chunk_30_03.blc and b/blight-map/src/main/map/chunks/chunk_30_03.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_31_00.blc b/blight-map/src/main/map/chunks/chunk_31_00.blc index ff13c8c..628fa54 100644 Binary files a/blight-map/src/main/map/chunks/chunk_31_00.blc and b/blight-map/src/main/map/chunks/chunk_31_00.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_31_01.blc b/blight-map/src/main/map/chunks/chunk_31_01.blc index 55dc845..d771382 100644 Binary files a/blight-map/src/main/map/chunks/chunk_31_01.blc and b/blight-map/src/main/map/chunks/chunk_31_01.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_31_02.blc b/blight-map/src/main/map/chunks/chunk_31_02.blc index 7f196cb..df9d5ed 100644 Binary files a/blight-map/src/main/map/chunks/chunk_31_02.blc and b/blight-map/src/main/map/chunks/chunk_31_02.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_31_03.blc b/blight-map/src/main/map/chunks/chunk_31_03.blc index dabb40c..579fd35 100644 Binary files a/blight-map/src/main/map/chunks/chunk_31_03.blc and b/blight-map/src/main/map/chunks/chunk_31_03.blc differ