diff --git a/assets/imported/models/bandit1.fbx b/assets/imported/models/bandit1.fbx new file mode 100644 index 0000000..5166766 Binary files /dev/null and b/assets/imported/models/bandit1.fbx differ diff --git a/blight-assets/src/main/resources/Characters/Models/bandit1.j3o b/blight-assets/src/main/resources/Characters/Models/bandit1.j3o new file mode 100644 index 0000000..aa028b9 Binary files /dev/null and b/blight-assets/src/main/resources/Characters/Models/bandit1.j3o differ diff --git a/blight-assets/src/main/resources/Characters/bandit1.character b/blight-assets/src/main/resources/Characters/bandit1.character new file mode 100644 index 0000000..d1ae6cc --- /dev/null +++ b/blight-assets/src/main/resources/Characters/bandit1.character @@ -0,0 +1,35 @@ +{ + "status": "ENEMY", + "trader": false, + "equippedWeaponId": "rostiges_kurzschwert", + "equippedShieldId": "einfacher_schild", + "dialogOptions": {}, + "dialogRoots": [], + "dialogNext": {}, + "dialogDisables": {}, + "defaultMessages": {}, + "routines": [ + { + "name": "patrol_pass", + "blocks": [ + { + "startHour": 0, + "endHour": 23, + "activity": { + "type": "STAND", + "position": { + "x": -39.0, + "y": 2.0, + "z": -26.4, + "yaw": 0.0 + } + } + } + ] + } + ], + "characterId": "bandit1", + "modelPath": "Characters/Models/bjoernson.j3o", + "animSetPath": "human", + "type": "NPC" +} \ No newline at end of file diff --git a/blight-assets/src/main/resources/Characters/bjoernson.character b/blight-assets/src/main/resources/Characters/bjoernson.character index 5b5fda4..07b2ef9 100644 --- a/blight-assets/src/main/resources/Characters/bjoernson.character +++ b/blight-assets/src/main/resources/Characters/bjoernson.character @@ -1,364 +1,25 @@ { "status": "NEUTRAL", "trader": false, - "dialogOptions": { - "begruessung": { - "id": "begruessung", - "requiresChapter": 0, - "textHero": { - "id": "dialog.bjoernson.begruessung.textmainchar" - }, - "textNpc": { - "id": "dialog.bjoernson.begruessung.textnpc" - }, - "heroSteps": [], - "npcSteps": [], - "abortsQuests": [], - "enablesTrade": false, - "onChosenTriggers": [] - }, - "wer_bist_du": { - "id": "wer_bist_du", - "requiresChapter": 0, - "textHero": { - "id": "dialog.bjoernson.wer_bist_du.textmainchar" - }, - "textNpc": { - "id": "dialog.bjoernson.wer_bist_du.textnpc" - }, - "heroSteps": [], - "npcSteps": [], - "abortsQuests": [], - "enablesTrade": false, - "onChosenTriggers": [] - }, - "aus_dem_norden": { - "id": "aus_dem_norden", - "requiresChapter": 0, - "textHero": { - "id": "dialog.bjoernson.aus_dem_norden.textmainchar" - }, - "heroSteps": [], - "npcSteps": [ - { - "text": { "id": "dialog.bjoernson.aus_dem_norden.textnpc" }, - "audio": { "key": "dialog.bjoernson.aus_dem_norden.textnpc" } - }, - { - "text": { "id": "dialog.bjoernson.aus_dem_norden.textnpc.1" }, - "audio": { "key": "dialog.bjoernson.aus_dem_norden.textnpc.1" } - } - ], - "abortsQuests": [], - "enablesTrade": false, - "onChosenTriggers": [] - }, - "bier_angebot": { - "id": "bier_angebot", - "requiresChapter": 0, - "textHero": { - "id": "dialog.bjoernson.bier_angebot.textmainchar" - }, - "textNpc": { - "id": "dialog.bjoernson.bier_angebot.textnpc" - }, - "heroSteps": [], - "npcSteps": [], - "abortsQuests": [], - "enablesTrade": false, - "onChosenTriggers": [] - }, - "abgemacht_bier": { - "id": "abgemacht_bier", - "requiresChapter": 0, - "textHero": { - "id": "dialog.bjoernson.abgemacht_bier.textmainchar" - }, - "recievesQuest": { - "questId": "kuehles_bier" - }, - "heroSteps": [], - "npcSteps": [], - "abortsQuests": [], - "enablesTrade": false, - "onChosenTriggers": [] - }, - "bier_geliefert": { - "id": "bier_geliefert", - "requiresChapter": 0, - "requiresQuestOpen": { - "questId": "kuehles_bier" - }, - "textHero": { - "id": "dialog.bjoernson.bier_geliefert.textmainchar" - }, - "textNpc": { - "id": "dialog.bjoernson.bier_geliefert.textnpc" - }, - "fulfillsQuest": { - "questId": "kuehles_bier" - }, - "heroSteps": [], - "npcSteps": [], - "abortsQuests": [], - "enablesTrade": false, - "onChosenTriggers": [] - }, - "schwert_annehmen": { - "id": "schwert_annehmen", - "requiresChapter": 1, - "textHero": { - "id": "dialog.bjoernson.schwert_annehmen.textmainchar" - }, - "heroSteps": [], - "npcSteps": [ - { - "text": { "id": "dialog.bjoernson.schwert_annehmen.textnpc" }, - "audio": { "key": "dialog.bjoernson.schwert_annehmen.textnpc" } - }, - { - "text": { "id": "dialog.bjoernson.schwert_annehmen.textnpc.1" }, - "audio": { "key": "dialog.bjoernson.schwert_annehmen.textnpc.1" } - }, - { - "text": { "id": "dialog.bjoernson.schwert_annehmen.textnpc.2" }, - "audio": { "key": "dialog.bjoernson.schwert_annehmen.textnpc.2" } - }, - { - "text": { "id": "dialog.bjoernson.schwert_annehmen.textnpc.3" }, - "audio": { "key": "dialog.bjoernson.schwert_annehmen.textnpc.3" } - } - ], - "abortsQuests": [], - "enablesTrade": false, - "onChosenTriggers": [] - }, - "ich_kuemmere_mich": { - "id": "ich_kuemmere_mich", - "requiresChapter": 0, - "textHero": { - "id": "dialog.bjoernson.ich_kuemmere_mich.textmainchar" - }, - "textNpc": { - "id": "dialog.bjoernson.ich_kuemmere_mich.textnpc" - }, - "recievesQuest": { - "questId": "blight_wuehler" - }, - "heroSteps": [], - "npcSteps": [], - "abortsQuests": [], - "enablesTrade": false, - "onChosenTriggers": [] - }, - "erz_quest": { - "id": "erz_quest", - "requiresChapter": 0, - "textHero": { - "id": "dialog.bjoernson.erz_quest.textmainchar" - }, - "recievesQuest": { - "questId": "magnetiterz" - }, - "heroSteps": [], - "npcSteps": [], - "abortsQuests": [], - "enablesTrade": false, - "onChosenTriggers": [] - }, - "wie_schuerfern": { - "id": "wie_schuerfern", - "requiresChapter": 0, - "textHero": { - "id": "dialog.bjoernson.wie_schuerfern.textmainchar" - }, - "heroSteps": [], - "npcSteps": [ - { - "text": { "id": "dialog.bjoernson.wie_schuerfern.textnpc" }, - "audio": { "key": "dialog.bjoernson.wie_schuerfern.textnpc" } - }, - { - "text": { "id": "dialog.bjoernson.wie_schuerfern.textnpc.1" }, - "audio": { "key": "dialog.bjoernson.wie_schuerfern.textnpc.1" } - } - ], - "abortsQuests": [], - "enablesTrade": false, - "onChosenTriggers": [] - }, - "danke_bjoernson": { - "id": "danke_bjoernson", - "requiresChapter": 0, - "textHero": { - "id": "dialog.bjoernson.danke_bjoernson.textmainchar" - }, - "textNpc": { - "id": "dialog.bjoernson.danke_bjoernson.textnpc" - }, - "heroSteps": [], - "npcSteps": [], - "abortsQuests": [], - "enablesTrade": false, - "onChosenTriggers": [] - }, - "quests_erledigt": { - "id": "quests_erledigt", - "requiresChapter": 0, - "requiresQuestComplete": { - "questId": "blight_wuehler" - }, - "textHero": { - "id": "dialog.bjoernson.quests_erledigt.textmainchar" - }, - "textNpc": { - "id": "dialog.bjoernson.quests_erledigt.textnpc" - }, - "heroSteps": [], - "npcSteps": [], - "abortsQuests": [], - "enablesTrade": false, - "onChosenTriggers": [] - }, - "ich_bin_zaeh": { - "id": "ich_bin_zaeh", - "requiresChapter": 0, - "textHero": { - "id": "dialog.bjoernson.ich_bin_zaeh.textmainchar" - }, - "textNpc": { - "id": "dialog.bjoernson.ich_bin_zaeh.textnpc" - }, - "heroSteps": [], - "npcSteps": [], - "abortsQuests": [], - "enablesTrade": false, - "onChosenTriggers": [] - }, - "wie_stehts_abmachung": { - "id": "wie_stehts_abmachung", - "requiresChapter": 0, - "textHero": { - "id": "dialog.bjoernson.wie_stehts_abmachung.textmainchar" - }, - "textNpc": { - "id": "dialog.bjoernson.wie_stehts_abmachung.textnpc" - }, - "heroSteps": [], - "npcSteps": [], - "abortsQuests": [], - "enablesTrade": false, - "onChosenTriggers": [] - }, - "nur_die_waffe": { - "id": "nur_die_waffe", - "requiresChapter": 0, - "textHero": { - "id": "dialog.bjoernson.nur_die_waffe.textmainchar" - }, - "textNpc": { - "id": "dialog.bjoernson.nur_die_waffe.textnpc" - }, - "heroSteps": [], - "npcSteps": [], - "abortsQuests": [], - "enablesTrade": false, - "onChosenTriggers": [] - }, - "selber_lernen": { - "id": "selber_lernen", - "requiresChapter": 0, - "textHero": { - "id": "dialog.bjoernson.selber_lernen.textmainchar" - }, - "textNpc": { - "id": "dialog.bjoernson.selber_lernen.textnpc" - }, - "heroSteps": [], - "npcSteps": [], - "abortsQuests": [], - "enablesTrade": true, - "onChosenTriggers": [] - } - }, - "dialogRoots": [ - "begruessung", - "bier_geliefert", - "schwert_annehmen", - "quests_erledigt" - ], - "dialogNext": { - "begruessung": [ - "wer_bist_du", - "bier_angebot" - ], - "wer_bist_du": [ - "aus_dem_norden", - "bier_angebot" - ], - "aus_dem_norden": [ - "bier_angebot" - ], - "bier_angebot": [ - "abgemacht_bier" - ], - "schwert_annehmen": [ - "ich_kuemmere_mich" - ], - "ich_kuemmere_mich": [ - "erz_quest", - "wie_schuerfern", - "danke_bjoernson" - ], - "erz_quest": [ - "wie_schuerfern", - "danke_bjoernson" - ], - "quests_erledigt": [ - "ich_bin_zaeh" - ], - "ich_bin_zaeh": [ - "wie_stehts_abmachung" - ], - "wie_stehts_abmachung": [ - "nur_die_waffe", - "selber_lernen" - ] - }, - "dialogDisables": { - "abgemacht_bier": [ - "begruessung", - "wer_bist_du", - "aus_dem_norden", - "bier_angebot" - ], - "nur_die_waffe": [ - "selber_lernen", - "quests_erledigt", - "ich_bin_zaeh", - "wie_stehts_abmachung" - ], - "selber_lernen": [ - "nur_die_waffe", - "quests_erledigt", - "ich_bin_zaeh", - "wie_stehts_abmachung" - ] - }, + "dialogOptions": {}, + "dialogRoots": [], + "dialogNext": {}, + "dialogDisables": {}, "defaultMessages": {}, "routines": [ { - "name": "chapter0", + "name": "patrol_pass", "blocks": [ { "startHour": 0, - "endHour": 24, + "endHour": 23, "activity": { "type": "STAND", "position": { - "x": 0.0, - "y": 1.0, - "z": 0.0 + "x": -7.9, + "y": 2.0, + "z": -1.0, + "yaw": 0.0 } } } @@ -366,10 +27,7 @@ } ], "characterId": "bjoernson", - "name": { - "id": "bjoernson.name" - }, - "modelPath": "Models/Chars/bjoernson.j3o", + "modelPath": "Characters/Models/bjoernson.j3o", "animSetPath": "human", "type": "NPC" -} +} \ No newline at end of file diff --git a/blight-assets/src/main/resources/Characters/hero.character b/blight-assets/src/main/resources/Characters/hero.character index cf321b6..efa85aa 100644 --- a/blight-assets/src/main/resources/Characters/hero.character +++ b/blight-assets/src/main/resources/Characters/hero.character @@ -12,6 +12,19 @@ "openManaRegeneration": 0, "openStaminaRegeneration": 0, "restorationValue": 10, + "abilities": { + "lvlMagic": 1, + "lvlStaffCombat": 1, + "lvlSwordsmanship": 1, + "lvlArchery": 1, + "lvlHeavyWeapons": 1, + "lvlCrossbow": 1, + "lvlThievery": 0, + "lvlAlchemy": 0, + "lvlEngineering": 0, + "lvlSmithery": 0, + "lvlEnchanting": 0 + }, "playedMonologueIds": [], "firedTriggerIds": [], "visitedZoneIds": [], diff --git a/blight-assets/src/main/resources/Characters/silas.character b/blight-assets/src/main/resources/Characters/silas.character index 7d36696..3017001 100644 --- a/blight-assets/src/main/resources/Characters/silas.character +++ b/blight-assets/src/main/resources/Characters/silas.character @@ -1,442 +1,12 @@ { "status": "NEUTRAL", "trader": false, - "dialogOptions": { - "begruessung": { - "id": "begruessung", - "requiresChapter": 0, - "textHero": { - "id": "dialog.silas.begruessung.textmainchar" - }, - "textNpc": { - "id": "dialog.silas.begruessung.textnpc" - }, - "abortsQuests": [], - "enablesTrade": false, - "onChosenTriggers": [] - }, - "antwort_sklave": { - "id": "antwort_sklave", - "requiresChapter": 0, - "textHero": { - "id": "dialog.silas.antwort_sklave.textmainchar" - }, - "textNpc": { - "id": "dialog.silas.antwort_sklave.textnpc" - }, - "abortsQuests": [], - "enablesTrade": false, - "onChosenTriggers": [] - }, - "antwort_gedaechtnislos": { - "id": "antwort_gedaechtnislos", - "requiresChapter": 0, - "textHero": { - "id": "dialog.silas.antwort_gedaechtnislos.textmainchar" - }, - "textNpc": { - "id": "dialog.silas.antwort_gedaechtnislos.textnpc" - }, - "abortsQuests": [], - "enablesTrade": false, - "onChosenTriggers": [] - }, - "antwort_gesandter": { - "id": "antwort_gesandter", - "requiresChapter": 0, - "textHero": { - "id": "dialog.silas.antwort_gesandter.textmainchar" - }, - "textNpc": { - "id": "dialog.silas.antwort_gesandter.textnpc" - }, - "abortsQuests": [], - "enablesTrade": false, - "onChosenTriggers": [] - }, - "wer_bist_du": { - "id": "wer_bist_du", - "requiresChapter": 0, - "textHero": { - "id": "dialog.silas.wer_bist_du.textmainchar" - }, - "textNpc": { - "id": "dialog.silas.wer_bist_du.textnpc" - }, - "abortsQuests": [], - "enablesTrade": false, - "onChosenTriggers": [] - }, - "wer_seid_ihr": { - "id": "wer_seid_ihr", - "requiresChapter": 0, - "textHero": { - "id": "dialog.silas.wer_seid_ihr.textmainchar" - }, - "textNpc": { - "id": "dialog.silas.wer_seid_ihr.textnpc" - }, - "abortsQuests": [], - "enablesTrade": false, - "onChosenTriggers": [] - }, - "was_leuchten": { - "id": "was_leuchten", - "requiresChapter": 0, - "textHero": { - "id": "dialog.silas.was_leuchten.textmainchar" - }, - "textNpc": { - "id": "dialog.silas.was_leuchten.textnpc" - }, - "abortsQuests": [], - "enablesTrade": false, - "onChosenTriggers": [] - }, - "ausruestung": { - "id": "ausruestung", - "requiresChapter": 0, - "textHero": { - "id": "dialog.silas.ausruestung.textmainchar" - }, - "textNpc": { - "id": "dialog.silas.ausruestung.textnpc" - }, - "abortsQuests": [], - "enablesTrade": false, - "onChosenTriggers": [] - }, - "gibts_was_rum": { - "id": "gibts_was_rum", - "requiresChapter": 0, - "textHero": { - "id": "dialog.silas.gibts_was_rum.textmainchar" - }, - "textNpc": { - "id": "dialog.silas.gibts_was_rum.textnpc" - }, - "abortsQuests": [], - "enablesTrade": false, - "onChosenTriggers": [] - }, - "tipp_rum": { - "id": "tipp_rum", - "requiresChapter": 0, - "textHero": { - "id": "dialog.silas.tipp_rum.textmainchar" - }, - "textNpc": { - "id": "dialog.silas.tipp_rum.textnpc" - }, - "abortsQuests": [], - "enablesTrade": false, - "onChosenTriggers": [] - }, - "welchen_weg": { - "id": "welchen_weg", - "requiresChapter": 0, - "textHero": { - "id": "dialog.silas.welchen_weg.textmainchar" - }, - "textNpc": { - "id": "dialog.silas.welchen_weg.textnpc" - }, - "abortsQuests": [], - "enablesTrade": false, - "onChosenTriggers": [] - }, - "abschluss_phase1": { - "id": "abschluss_phase1", - "requiresChapter": 0, - "textHero": { - "id": "dialog.silas.abschluss_phase1.textmainchar" - }, - "textNpc": { - "id": "dialog.silas.abschluss_phase1.textnpc" - }, - "abortsQuests": [], - "enablesTrade": false, - "onChosenTriggers": [] - }, - "rueckkehr_beobachtet": { - "id": "rueckkehr_beobachtet", - "requiresChapter": 0, - "textHero": { - "id": "dialog.silas.rueckkehr_beobachtet.textmainchar" - }, - "textNpc": { - "id": "dialog.silas.rueckkehr_beobachtet.textnpc" - }, - "abortsQuests": [], - "enablesTrade": false, - "onChosenTriggers": [] - }, - "ich_bin_zaeh": { - "id": "ich_bin_zaeh", - "requiresChapter": 0, - "textHero": { - "id": "dialog.silas.ich_bin_zaeh.textmainchar" - }, - "textNpc": { - "id": "dialog.silas.ich_bin_zaeh.textnpc" - }, - "abortsQuests": [], - "enablesTrade": false, - "onChosenTriggers": [] - }, - "weg_ins_innere": { - "id": "weg_ins_innere", - "requiresChapter": 0, - "textHero": { - "id": "dialog.silas.weg_ins_innere.textmainchar" - }, - "textNpc": { - "id": "dialog.silas.weg_ins_innere.textnpc" - }, - "abortsQuests": [], - "enablesTrade": false, - "onChosenTriggers": [] - }, - "was_ueber_insel": { - "id": "was_ueber_insel", - "requiresChapter": 0, - "textHero": { - "id": "dialog.silas.was_ueber_insel.textmainchar" - }, - "textNpc": { - "id": "dialog.silas.was_ueber_insel.textnpc" - }, - "abortsQuests": [], - "enablesTrade": false, - "onChosenTriggers": [] - }, - "info_osten": { - "id": "info_osten", - "requiresChapter": 0, - "textHero": { - "id": "dialog.silas.info_osten.textmainchar" - }, - "textNpc": { - "id": "dialog.silas.info_osten.textnpc" - }, - "abortsQuests": [], - "enablesTrade": false, - "onChosenTriggers": [] - }, - "info_westen": { - "id": "info_westen", - "requiresChapter": 0, - "textHero": { - "id": "dialog.silas.info_westen.textmainchar" - }, - "textNpc": { - "id": "dialog.silas.info_westen.textnpc" - }, - "abortsQuests": [], - "enablesTrade": false, - "onChosenTriggers": [] - }, - "info_dorf": { - "id": "info_dorf", - "requiresChapter": 0, - "textHero": { - "id": "dialog.silas.info_dorf.textmainchar" - }, - "textNpc": { - "id": "dialog.silas.info_dorf.textnpc" - }, - "abortsQuests": [], - "enablesTrade": false, - "onChosenTriggers": [] - }, - "info_pass": { - "id": "info_pass", - "requiresChapter": 0, - "textHero": { - "id": "dialog.silas.info_pass.textmainchar" - }, - "textNpc": { - "id": "dialog.silas.info_pass.textnpc" - }, - "abortsQuests": [], - "enablesTrade": false, - "onChosenTriggers": [] - }, - "ich_weiss_genug": { - "id": "ich_weiss_genug", - "requiresChapter": 0, - "textHero": { - "id": "dialog.silas.ich_weiss_genug.textmainchar" - }, - "textNpc": { - "id": "dialog.silas.ich_weiss_genug.textnpc" - }, - "abortsQuests": [], - "enablesTrade": false, - "onChosenTriggers": [] - }, - "karte_zeichnen": { - "id": "karte_zeichnen", - "requiresChapter": 0, - "textHero": { - "id": "dialog.silas.karte_zeichnen.textmainchar" - }, - "textNpc": { - "id": "dialog.silas.karte_zeichnen.textnpc" - }, - "abortsQuests": [], - "enablesTrade": false, - "onChosenTriggers": [] - }, - "dankeschoen": { - "id": "dankeschoen", - "requiresChapter": 0, - "textHero": { - "id": "dialog.silas.dankeschoen.textmainchar" - }, - "textNpc": { - "id": "dialog.silas.dankeschoen.textnpc" - }, - "abortsQuests": [], - "enablesTrade": false, - "onChosenTriggers": [] - }, - "erstaunt_ruhe": { - "id": "erstaunt_ruhe", - "requiresChapter": 0, - "textHero": { - "id": "dialog.silas.erstaunt_ruhe.textmainchar" - }, - "textNpc": { - "id": "dialog.silas.erstaunt_ruhe.textnpc" - }, - "abortsQuests": [], - "enablesTrade": false, - "onChosenTriggers": [] - }, - "abschluss_letzte_worte": { - "id": "abschluss_letzte_worte", - "requiresChapter": 0, - "textHero": { - "id": "dialog.silas.abschluss_letzte_worte.textmainchar" - }, - "textNpc": { - "id": "dialog.silas.abschluss_letzte_worte.textnpc" - }, - "abortsQuests": [], - "enablesTrade": false, - "onChosenTriggers": [] - } - }, - "dialogRoots": [ - "begruessung" - ], - "dialogNext": { - "begruessung": [ - "antwort_sklave", - "antwort_gedaechtnislos", - "antwort_gesandter" - ], - "antwort_sklave": [ - "wer_bist_du", - "was_leuchten", - "ausruestung", - "gibts_was_rum", - "welchen_weg" - ], - "antwort_gedaechtnislos": [ - "wer_bist_du", - "was_leuchten", - "ausruestung", - "tipp_rum", - "welchen_weg" - ], - "antwort_gesandter": [ - "wer_bist_du", - "was_leuchten", - "ausruestung", - "tipp_rum", - "welchen_weg" - ], - "wer_bist_du": [ - "wer_seid_ihr" - ], - "welchen_weg": [ - "abschluss_phase1" - ], - "abschluss_phase1": [ - "rueckkehr_beobachtet" - ], - "rueckkehr_beobachtet": [ - "ich_bin_zaeh" - ], - "ich_bin_zaeh": [ - "weg_ins_innere", - "was_ueber_insel" - ], - "was_ueber_insel": [ - "info_osten", - "info_westen", - "info_dorf", - "info_pass", - "ich_weiss_genug" - ], - "info_pass": [ - "karte_zeichnen" - ], - "ich_weiss_genug": [ - "karte_zeichnen" - ], - "karte_zeichnen": [ - "dankeschoen" - ], - "dankeschoen": [ - "erstaunt_ruhe" - ], - "erstaunt_ruhe": [ - "abschluss_letzte_worte" - ] - }, - "dialogDisables": { - "antwort_sklave": [ - "antwort_gedaechtnislos", - "antwort_gesandter" - ], - "antwort_gedaechtnislos": [ - "antwort_sklave", - "antwort_gesandter" - ], - "antwort_gesandter": [ - "antwort_sklave", - "antwort_gedaechtnislos" - ], - "ich_weiss_genug": [ - "info_osten", - "info_westen", - "info_dorf", - "info_pass" - ] - }, + "dialogOptions": {}, + "dialogRoots": [], + "dialogNext": {}, + "dialogDisables": {}, "defaultMessages": {}, - "routines": [ - { - "name": "chapter0", - "blocks": [ - { - "startHour": 0, - "endHour": 24, - "activity": { - "type": "STAND", - "position": { - "x": -17.0, - "y": 1.2, - "z": -1315.6 - } - } - } - ] - } - ], + "routines": [], "characterId": "silas", "name": { "id": "silas.name" diff --git a/blight-common/src/main/java/de/blight/common/model/GameCharacter.java b/blight-common/src/main/java/de/blight/common/model/GameCharacter.java index a940d2c..a2ceb52 100644 --- a/blight-common/src/main/java/de/blight/common/model/GameCharacter.java +++ b/blight-common/src/main/java/de/blight/common/model/GameCharacter.java @@ -8,8 +8,6 @@ import lombok.Setter; public abstract class GameCharacter { /** Eindeutige ID — entspricht dem Dateinamen ohne Extension. */ private String characterId; - /** Anzeigename des Charakters — wird zur Laufzeit per TextReference aufgelöst. */ - private TextReference name; /** Relativer Pfad zum .j3o-Modell (relativ zu blight-assets/src/main/resources). */ private String modelPath; /** Relativer Pfad zum .j3o-Animations-Set (in animations/sets/). */ 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 b589a0d..b068b55 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 @@ -26,6 +26,19 @@ public class NPC extends GameCharacter implements Interactable { private List items; + /** Basis-HP; 0 = Spielwelt-Standardwert (100). */ + private int maxHp; + /** Basis-Mana; 0 = Spielwelt-Standardwert (100). */ + private int maxMana; + /** Basis-Ausdauer; 0 = Spielwelt-Standardwert (100). */ + private int maxStamina; + + /** Item-ID der primären Nahkampfwaffe (am Rücken getragen). Null = keine Waffe. */ + private String equippedWeaponId; + + /** Item-ID des Schilds (am Rücken getragen; nur bei Schwertkämpfern sinnvoll). Null = kein Schild. */ + private String equippedShieldId; + // ── Flache Dialog-Struktur (serialisiert) ───────────────────────────────── /** Alle Dialog-Optionen dieses NPCs, flach nach ID indiziert. */ @@ -90,13 +103,12 @@ public class NPC extends GameCharacter implements Interactable { @Override public String getDisplayText() { - return TextRegistry.resolve(getName(), getCharacterId() != null ? getCharacterId() : "NPC"); + String key = getLabelKey(); + return TextRegistry.resolve(new TextReference(key), 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" : ""; } diff --git a/blight-common/src/main/java/de/blight/common/model/RoutineBlock.java b/blight-common/src/main/java/de/blight/common/model/RoutineBlock.java index 2aca461..e27987c 100644 --- a/blight-common/src/main/java/de/blight/common/model/RoutineBlock.java +++ b/blight-common/src/main/java/de/blight/common/model/RoutineBlock.java @@ -4,17 +4,14 @@ package de.blight.common.model; * Zeitblock innerhalb eines Tagesablaufs. * * {@code startHour} und {@code endHour} sind ganzzahlige Stunden (0–23). + * {@code endHour} ist inklusiv: ein Block mit endHour=22 läuft bis 22:59:59. * Ein Block, der über Mitternacht geht (z.B. 22–06), ist erlaubt: * in diesem Fall ist {@code endHour < startHour}. - * - * Die Dauer berechnet sich als: - * endHour >= startHour → endHour - startHour - * endHour < startHour → 24 - startHour + endHour */ public class RoutineBlock { private int startHour; // 0-23 - private int endHour; // 0-23 (exklusiv: Block endet um endHour:00) + private int endHour; // 0-23 (inklusiv: Block endet um endHour:59:59) private RoutineActivity activity; public RoutineBlock() {} @@ -34,23 +31,22 @@ public class RoutineBlock { public RoutineActivity getActivity() { return activity; } public void setActivity(RoutineActivity a) { this.activity = a; } - /** Anzahl der durch diesen Block abgedeckten Stunden. */ + /** Anzahl der durch diesen Block abgedeckten Stunden (inkl. endHour). */ public int durationHours() { - if (endHour > startHour) return endHour - startHour; - if (endHour < startHour) return 24 - startHour + endHour; - return 0; + if (endHour >= startHour) return endHour - startHour + 1; + return 24 - startHour + endHour + 1; } - /** Gibt true wenn die gegebene Stunde (0-23) in diesem Block liegt. */ + /** Gibt true wenn die gegebene Stunde (0-23) in diesem Block liegt (endHour inklusiv). */ public boolean covers(int hour) { - if (endHour > startHour) return hour >= startHour && hour < endHour; + if (endHour >= startHour) return hour >= startHour && hour <= endHour; // Über-Mitternacht-Block - return hour >= startHour || hour < endHour; + return hour >= startHour || hour <= endHour; } - /** Formatiert den Block als "HH:00 – HH:00 | Aktivität". */ + /** Formatiert den Block als "HH:00 – HH:59 | Aktivität". */ public String displayLabel() { String act = activity != null ? activity.summary() : "—"; - return String.format("%02d:00 – %02d:00 %s", startHour, endHour, act); + return String.format("%02d:00 – %02d:59 %s", startHour, endHour, act); } } diff --git a/blight-editor/src/main/java/de/blight/editor/state/AnimPreviewState.java b/blight-editor/src/main/java/de/blight/editor/state/AnimPreviewState.java index 80461f3..0b6aeb1 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/AnimPreviewState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/AnimPreviewState.java @@ -1276,6 +1276,17 @@ public class AnimPreviewState extends BaseAppState { assets.registerLocator(fbmDir.getAbsolutePath(), com.jme3.asset.plugins.FileLocator.class); } + // Eingebettete Texturen VOR dem Wrench-Load extrahieren: Wrench ersetzt + // Texturen mit nicht auffindbaren Pfaden sofort durch Platzhalter; danach + // ist der Original-Dateiname nicht mehr aus dem Modell ablesbar. + java.io.File preExtractDir = extractEmbeddedTexturesPreLoad(file); + if (preExtractDir != null) { + assets.registerLocator(preExtractDir.getAbsolutePath(), com.jme3.asset.plugins.FileLocator.class); + LOG.info("[CharImport] {} Textur(en) vorab extrahiert nach '{}'.", + preExtractDir.list() != null ? preExtractDir.list().length : 0, + preExtractDir.getAbsolutePath()); + } + com.github.stephengold.wrench.LwjglAssetKey key = new com.github.stephengold.wrench.LwjglAssetKey(file.getName()); key.setVerboseLogging(true); @@ -1289,6 +1300,8 @@ public class AnimPreviewState extends BaseAppState { return; } + // Post-Load-Fallback: sollte durch Pre-Load nicht mehr nötig sein, + // aber schadet nicht als Sicherheitsnetz. java.io.File embeddedDir = extractEmbeddedTextures(file, model); if (embeddedDir != null) { assets.registerLocator(embeddedDir.getAbsolutePath(), com.jme3.asset.plugins.FileLocator.class); @@ -1578,8 +1591,11 @@ public class AnimPreviewState extends BaseAppState { } /** - * Sucht eingebettete PNG-Texturen im FBX-Binär (Dateiname → PNG-Magic → IEND) + * Sucht eingebettete PNG- und JPEG-Texturen im FBX-Binär (Dateiname → Bild-Magic → Daten) * und schreibt sie in ein temporäres Verzeichnis. + * + * PNG: Dateiname → PNG-Magic → IEND-Marker + * JPEG: Dateiname → (optional: FBX R-Property-Header mit Länge) → JPEG-Magic → Daten */ private java.io.File extractEmbeddedTextures(java.io.File fbxFile, Spatial model) { java.util.Set filenames = collectTextureFilenames(model); @@ -1589,20 +1605,51 @@ public class AnimPreviewState extends BaseAppState { java.io.File tempDir = new java.io.File(System.getProperty("java.io.tmpdir"), "fbxtex_" + Math.abs(fbxFile.getAbsolutePath().hashCode())); tempDir.mkdirs(); - byte[] pngMagic = {(byte)0x89,0x50,0x4E,0x47,0x0D,0x0A,0x1A,0x0A}; - byte[] iend = {0x00,0x00,0x00,0x00,0x49,0x45,0x4E,0x44,(byte)0xAE,0x42,0x60,(byte)0x82}; + byte[] pngMagic = {(byte)0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}; + byte[] iend = {0x00,0x00,0x00,0x00, 0x49,0x45,0x4E,0x44, (byte)0xAE,0x42,0x60,(byte)0x82}; + byte[] jpegMagic = {(byte)0xFF, (byte)0xD8, (byte)0xFF}; + byte[] jpegEoi = {(byte)0xFF, (byte)0xD9}; boolean any = false; for (String filename : filenames) { byte[] nameBytes = filename.getBytes(java.nio.charset.StandardCharsets.US_ASCII); int namePos = binaryIndexOf(data, nameBytes, 0); if (namePos < 0) continue; + + // ── PNG ─────────────────────────────────────────────────────── int pngPos = binaryIndexOf(data, pngMagic, namePos); - if (pngPos < 0 || pngPos - namePos > 65536) continue; - int endPos = binaryIndexOf(data, iend, pngPos + 8); - if (endPos < 0) continue; - byte[] png = java.util.Arrays.copyOfRange(data, pngPos, endPos + iend.length); - java.nio.file.Files.write(new java.io.File(tempDir, filename).toPath(), png); - LOG.info("[FbxImport] Embedded-Textur extrahiert: {} ({} B)", filename, png.length); + if (pngPos >= 0 && pngPos - namePos <= 65536) { + int endPos = binaryIndexOf(data, iend, pngPos + 8); + if (endPos >= 0) { + byte[] png = java.util.Arrays.copyOfRange(data, pngPos, endPos + iend.length); + java.nio.file.Files.write(new java.io.File(tempDir, filename).toPath(), png); + LOG.info("[FbxImport] Embedded PNG extrahiert: {} ({} B)", filename, png.length); + any = true; + continue; + } + } + + // ── JPEG ────────────────────────────────────────────────────── + int jpegPos = binaryIndexOf(data, jpegMagic, namePos); + if (jpegPos < 0 || jpegPos - namePos > 65536) continue; + + // FBX R-Property: 0x52 + 4-Byte LE-Größe direkt vor den JPEG-Daten + int jpegSize = 0; + if (jpegPos >= 5 && data[jpegPos - 5] == (byte) 0x52) { + jpegSize = java.nio.ByteBuffer.wrap(data, jpegPos - 4, 4) + .order(java.nio.ByteOrder.LITTLE_ENDIAN).getInt(); + } + byte[] jpeg; + if (jpegSize > 1024 && jpegPos + jpegSize <= data.length) { + // Exakte Größe aus FBX-Header bekannt + jpeg = java.util.Arrays.copyOfRange(data, jpegPos, jpegPos + jpegSize); + } else { + // Fallback: bis EOI-Marker scannen + int eoiPos = binaryIndexOf(data, jpegEoi, jpegPos + 2); + if (eoiPos < 0) continue; + jpeg = java.util.Arrays.copyOfRange(data, jpegPos, eoiPos + 2); + } + java.nio.file.Files.write(new java.io.File(tempDir, filename).toPath(), jpeg); + LOG.info("[FbxImport] Embedded JPEG extrahiert: {} ({} B)", filename, jpeg.length); any = true; } return any ? tempDir : null; @@ -1612,6 +1659,122 @@ public class AnimPreviewState extends BaseAppState { } } + /** + * Liest das FBX-Binary, findet alle eingebetteten Bild-Texturen anhand von + * FBX-R-Property-Headern und Dateinamen-Strings, und schreibt sie (URL-dekodiert) + * in ein temporäres Verzeichnis — BEVOR das Modell mit dem Wrench-Loader geladen wird. + */ + private java.io.File extractEmbeddedTexturesPreLoad(java.io.File fbxFile) { + try { + byte[] data = java.nio.file.Files.readAllBytes(fbxFile.toPath()); + java.io.File tempDir = new java.io.File(System.getProperty("java.io.tmpdir"), + "fbxtex_pre_" + Math.abs(fbxFile.getAbsolutePath().hashCode())); + tempDir.mkdirs(); + boolean any = false; + + byte[] pngMagic = {(byte)0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}; + byte[] iend = {0x00,0x00,0x00,0x00, 0x49,0x45,0x4E,0x44, (byte)0xAE,0x42,0x60,(byte)0x82}; + byte[] jpegMagic = {(byte)0xFF, (byte)0xD8, (byte)0xFF}; + + // ── JPEG: R-Property Header (0x52 + 4-Byte LE Größe + JPEG Magic) ── + int searchPos = 0; + while (true) { + int jpegPos = binaryIndexOf(data, jpegMagic, searchPos); + if (jpegPos < 0) break; + searchPos = jpegPos + 1; + if (jpegPos < 5 || data[jpegPos - 5] != (byte) 0x52) continue; + int size = java.nio.ByteBuffer.wrap(data, jpegPos - 4, 4) + .order(java.nio.ByteOrder.LITTLE_ENDIAN).getInt(); + if (size < 1024 || jpegPos + size > data.length) continue; + + // Dateiname: letztes .jpg/.jpeg-Vorkommen vor jpegPos (max 65536 Bytes rückwärts) + String decodedName = findImageFilenameBeforePos(data, jpegPos, new String[]{".jpg", ".jpeg"}); + if (decodedName == null) decodedName = "texture_" + jpegPos + ".jpg"; + + java.io.File dest = new java.io.File(tempDir, decodedName); + if (!dest.exists()) { + java.nio.file.Files.write(dest.toPath(), + java.util.Arrays.copyOfRange(data, jpegPos, jpegPos + size)); + LOG.info("[CharImport] JPEG vorab extrahiert: '{}' ({} B)", decodedName, size); + any = true; + } + } + + // ── PNG: PNG-Magic → IEND ────────────────────────────────────────── + searchPos = 0; + while (true) { + int pngPos = binaryIndexOf(data, pngMagic, searchPos); + if (pngPos < 0) break; + searchPos = pngPos + 1; + int endPos = binaryIndexOf(data, iend, pngPos + 8); + if (endPos < 0) continue; + int size = endPos + iend.length - pngPos; + if (size < 256) continue; + + String decodedName = findImageFilenameBeforePos(data, pngPos, new String[]{".png"}); + if (decodedName == null) decodedName = "texture_" + pngPos + ".png"; + + java.io.File dest = new java.io.File(tempDir, decodedName); + if (!dest.exists()) { + java.nio.file.Files.write(dest.toPath(), + java.util.Arrays.copyOfRange(data, pngPos, pngPos + size)); + LOG.info("[CharImport] PNG vorab extrahiert: '{}' ({} B)", decodedName, size); + any = true; + } + } + + return any ? tempDir : null; + } catch (Exception e) { + LOG.warn("[CharImport] Pre-Load-Texturextraktion fehlgeschlagen: {}", e.getMessage()); + return null; + } + } + + /** + * Sucht rückwärts ab {@code beforePos} (max 65536 Bytes) nach einem + * Dateinamen-String mit einer der angegebenen Endungen und gibt den + * URL-dekodierten Basis-Dateinamen zurück. + */ + private static String findImageFilenameBeforePos(byte[] data, int beforePos, String[] extensions) { + int scanFrom = Math.max(0, beforePos - 65536); + int bestPos = -1; + String bestExt = null; + for (String ext : extensions) { + byte[] extBytes = ext.getBytes(java.nio.charset.StandardCharsets.US_ASCII); + int pos = scanFrom; + while (pos < beforePos) { + int found = -1; + outer: + for (int i = pos, max = beforePos - extBytes.length; i <= max; i++) { + for (int j = 0; j < extBytes.length; j++) { + if (data[i + j] != extBytes[j]) continue outer; + } + found = i; + break; + } + if (found < 0) break; + if (found > bestPos) { bestPos = found; bestExt = ext; } + pos = found + 1; + } + } + if (bestPos < 0 || bestExt == null) return null; + + // Rückwärts zum Anfang des Dateinamens (Stopp bei /, \ oder Non-Printable) + int nameStart = bestPos - 1; + while (nameStart > 0 && data[nameStart] >= 0x20 + && data[nameStart] != '/' && data[nameStart] != '\\') { + nameStart--; + } + nameStart++; + String raw = new String(data, nameStart, bestPos + bestExt.length() - nameStart, + java.nio.charset.StandardCharsets.US_ASCII); + try { + return new java.io.File(java.net.URLDecoder.decode(raw, "UTF-8")).getName(); + } catch (Exception e) { + return new java.io.File(raw.replace("+", " ")).getName(); + } + } + private static int binaryIndexOf(byte[] data, byte[] pattern, int from) { outer: for (int i = from, max = data.length - pattern.length; i <= max; i++) { diff --git a/blight-editor/src/main/java/de/blight/editor/toolset/content/ContentController.java b/blight-editor/src/main/java/de/blight/editor/toolset/content/ContentController.java index af82d97..ea48ab5 100644 --- a/blight-editor/src/main/java/de/blight/editor/toolset/content/ContentController.java +++ b/blight-editor/src/main/java/de/blight/editor/toolset/content/ContentController.java @@ -59,6 +59,7 @@ public class ContentController { private de.blight.editor.ui.RoutineEditorView routineEditorView; private de.blight.editor.ui.MonologueEditorView monologueEditorView; private StackPane routineJmeSlot; + private boolean loadingCharacter = false; // ── Character-Formular-Felder ────────────────────────────────────────────── private VBox charEditContainer; @@ -69,7 +70,9 @@ public class ContentController { private CheckBox charTraderCheck; private ListView charTraderItemsView; private TextField charMsgFriendly, charMsgNeutral, charMsgEnraged, charMsgEnemy; - private TextField charNameField, charIdField; + private TextField charMaxHpField, charMaxManaField, charMaxStaminaField; + private ComboBox charWeaponCombo, charShieldCombo; + private TextField charIdField; private ComboBox charTypeCombo, charModelCombo, charAnimSetCombo; private ComboBox charFractionCombo; private Label charEditorStatusLabel; @@ -263,11 +266,11 @@ public class ContentController { public void loadCharacterById(String id) { if (id == null || id.isBlank()) return; Path charDir = ctx.assetRoot().resolve("Characters"); + loadingCharacter = true; try { de.blight.common.model.GameCharacter c = de.blight.common.model.CharacterIO.load(charDir.resolve(id + ".character")); - if (charIdField != null) charIdField.setText(c.getCharacterId() != null ? c.getCharacterId() : ""); - if (charNameField != null) charNameField.setText(c.getName() != null ? c.getName().id() : ""); + if (charIdField != null) charIdField.setText(c.getCharacterId() != null ? c.getCharacterId() : ""); if (charTypeCombo != null) charTypeCombo.setValue(c instanceof de.blight.common.model.MainCharacter ? "MainCharacter" : "NPC"); if (charModelCombo != null) charModelCombo.setValue(c.getModelPath()); @@ -303,6 +306,17 @@ public class ContentController { 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 (charWeaponCombo != null) { + String wid = npc.getEquippedWeaponId(); + charWeaponCombo.setValue(wid != null ? wid : ""); + } + if (charShieldCombo != null) { + String sid = npc.getEquippedShieldId(); + charShieldCombo.setValue(sid != null ? sid : ""); + } + if (charMaxHpField != null) charMaxHpField.setText(String.valueOf(npc.getMaxHp() > 0 ? npc.getMaxHp() : 100)); + if (charMaxManaField != null) charMaxManaField.setText(String.valueOf(npc.getMaxMana() > 0 ? npc.getMaxMana() : 100)); + if (charMaxStaminaField != null) charMaxStaminaField.setText(String.valueOf(npc.getMaxStamina() > 0 ? npc.getMaxStamina() : 100)); if (dialogEditorView != null) { dialogEditorView.setDisable(false); dialogEditorView.loadNpc(npc); @@ -325,6 +339,8 @@ public class ContentController { if (charEditorStatusLabel != null) charEditorStatusLabel.setText("Geladen: " + id); } catch (Exception e) { if (charEditorStatusLabel != null) charEditorStatusLabel.setText("Fehler: " + e.getMessage()); + } finally { + loadingCharacter = false; } } @@ -923,13 +939,25 @@ public class ContentController { dialogEditorView = new de.blight.editor.ui.DialogEditorView(); dialogEditorView.setDisable(true); monologueEditorView = new de.blight.editor.ui.MonologueEditorView(); + routineEditorView = new de.blight.editor.ui.RoutineEditorView(ctx.input(), ctx.assetRoot().resolve("Characters")); + routineEditorView.setDisable(true); + + routineJmeSlot = new StackPane(); + routineJmeSlot.setMinWidth(0); + routineJmeSlot.getChildren().add(ctx.viewportSupplier().get()); + SplitPane routineSplit = new SplitPane(routineEditorView, routineJmeSlot); + routineSplit.setDividerPositions(0.25); + routineSplit.widthProperty().addListener((obs, ov, nv) -> { + if (nv.doubleValue() > 0) routineSplit.setDividerPositions(0.25); + }); TabPane tabs = new TabPane(); tabs.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE); - Tab charTab = new Tab("Charakter", buildCharacterSettingsPane()); - Tab dialogTab = new Tab("Dialog", dialogEditorView); - Tab monologTab = new Tab("Monologe", monologueEditorView); - tabs.getTabs().addAll(charTab, dialogTab, monologTab); + Tab charTab = new Tab("Charakter", buildCharacterSettingsPane()); + Tab dialogTab = new Tab("Dialog", dialogEditorView); + Tab routineTab = new Tab("Tagesabläufe", routineSplit); + Tab monologTab = new Tab("Monologe", monologueEditorView); + tabs.getTabs().addAll(charTab, dialogTab, routineTab, monologTab); tabs.getSelectionModel().select(charTab); setContentCenter(tabs); @@ -1085,9 +1113,6 @@ public class ContentController { charIdField = new TextField(); charIdField.setPromptText("eindeutige ID (Dateiname)"); - charNameField = new TextField(); - charNameField.setPromptText("Anzeigename"); - charNameField.setEditable(false); charTypeCombo = new ComboBox<>(); charTypeCombo.getItems().addAll("MainCharacter", "NPC"); @@ -1113,9 +1138,8 @@ public class ContentController { charAnimSetCombo.setOnAction(e -> updateCharActionCombosFromSet()); charEditContainer.getChildren().addAll( - dl.apply("ID:"), charIdField, - dl.apply("Name:"), charNameField, - dl.apply("Typ:"), charTypeCombo + dl.apply("ID:"), charIdField, + dl.apply("Typ:"), charTypeCombo ); charFractionCombo = new ComboBox<>(); @@ -1180,6 +1204,40 @@ public class ContentController { charMsgEnemy = new TextField(); charMsgEnemy.setPromptText("TextReference-Schlüssel"); + // Weapon / shield combos – populated from items directory + charWeaponCombo = new ComboBox<>(); + charWeaponCombo.setMaxWidth(Double.MAX_VALUE); + charWeaponCombo.setPromptText("— keine Waffe —"); + charWeaponCombo.getItems().add(""); + + charShieldCombo = new ComboBox<>(); + charShieldCombo.setMaxWidth(Double.MAX_VALUE); + charShieldCombo.setPromptText("— kein Schild —"); + charShieldCombo.getItems().add(""); + + try { + java.util.List allItems = + de.blight.common.model.ItemIO.loadAll(ctx.assetRoot().resolve("items")); + allItems.stream() + .filter(i -> i.getCategory() == de.blight.common.model.ItemCategory.WEAPON + && i.getSubCategory() != de.blight.common.model.ItemSubCategory.SHIELD) + .map(de.blight.common.model.Item::getItemId) + .filter(id -> id != null && !id.isBlank()) + .forEach(charWeaponCombo.getItems()::add); + allItems.stream() + .filter(i -> i.getSubCategory() == de.blight.common.model.ItemSubCategory.SHIELD) + .map(de.blight.common.model.Item::getItemId) + .filter(id -> id != null && !id.isBlank()) + .forEach(charShieldCombo.getItems()::add); + } catch (Exception ignored) {} + + charMaxHpField = new TextField("100"); + charMaxHpField.setPromptText("100"); + charMaxManaField = new TextField("100"); + charMaxManaField.setPromptText("100"); + charMaxStaminaField = new TextField("100"); + charMaxStaminaField.setPromptText("100"); + charNpcSection = new VBox(4, dl.apply("Status:"), charStatusCombo, dl.apply("Handel:"), charTraderCheck, @@ -1188,7 +1246,14 @@ public class ContentController { dl.apply("Friendly:"), charMsgFriendly, dl.apply("Neutral:"), charMsgNeutral, dl.apply("Enraged:"), charMsgEnraged, - dl.apply("Enemy:"), charMsgEnemy); + dl.apply("Enemy:"), charMsgEnemy, + dsl.apply("Basiswerte"), + dl.apply("Max HP:"), charMaxHpField, + dl.apply("Max Mana:"), charMaxManaField, + dl.apply("Max Ausdauer:"), charMaxStaminaField, + dsl.apply("Ausrüstung"), + dl.apply("Primärwaffe:"), charWeaponCombo, + dl.apply("Schild (nur bei Schwert):"), charShieldCombo); boolean npcInitial = "NPC".equals(charTypeCombo.getValue()); charNpcSection.setVisible(npcInitial); charNpcSection.setManaged(npcInitial); @@ -1238,17 +1303,11 @@ public class ContentController { ); // Buttons - Button saveBtn = new Button("Speichern"); Button clearBtn = new Button("Leeren"); - saveBtn.setMaxWidth(Double.MAX_VALUE); clearBtn.setMaxWidth(Double.MAX_VALUE); - saveBtn.setStyle("-fx-background-color:#2a5a2a;-fx-text-fill:white;"); - saveBtn.setOnAction(e -> saveCharacter(charDir)); clearBtn.setOnAction(e -> clearCharacterForm()); - HBox.setHgrow(saveBtn, Priority.ALWAYS); HBox.setHgrow(clearBtn, Priority.ALWAYS); - HBox btnBar = new HBox(6, saveBtn, clearBtn); - charEditContainer.getChildren().addAll(new Separator(), btnBar); + charEditContainer.getChildren().addAll(new Separator(), clearBtn); // Auto-Save-Listener charIdField.focusedProperty().addListener((obs, ov, nv) -> { if (!nv) autoSaveCharacter(charDir); }); @@ -1264,6 +1323,12 @@ public class ContentController { charMsgNeutral.focusedProperty().addListener((obs, ov, nv) -> { if (!nv) autoSaveCharacter(charDir); }); charMsgEnraged.focusedProperty().addListener((obs, ov, nv) -> { if (!nv) autoSaveCharacter(charDir); }); charMsgEnemy.focusedProperty().addListener((obs, ov, nv) -> { if (!nv) autoSaveCharacter(charDir); }); + charWeaponCombo.valueProperty().addListener((obs, ov, nv) -> autoSaveCharacter(charDir)); + charShieldCombo.valueProperty().addListener((obs, ov, nv) -> autoSaveCharacter(charDir)); + charMaxHpField.focusedProperty().addListener((obs, ov, nv) -> { if (!nv) autoSaveCharacter(charDir); }); + charMaxManaField.focusedProperty().addListener((obs, ov, nv) -> { if (!nv) autoSaveCharacter(charDir); }); + charMaxStaminaField.focusedProperty().addListener((obs, ov, nv) -> { if (!nv) autoSaveCharacter(charDir); }); + if (routineEditorView != null) routineEditorView.setOnDataChanged(() -> autoSaveCharacter(charDir)); inner.getChildren().add(charEditContainer); @@ -1275,6 +1340,7 @@ public class ContentController { } private void autoSaveCharacter(Path charDir) { + if (loadingCharacter) return; if (charIdField == null || charIdField.getText().trim().isBlank()) return; saveCharacter(charDir); } @@ -1333,8 +1399,7 @@ public class ContentController { } private void clearCharacterForm() { - if (charIdField != null) charIdField.clear(); - if (charNameField != null) charNameField.clear(); + if (charIdField != null) charIdField.clear(); if (charTypeCombo != null) charTypeCombo.setValue("NPC"); if (charModelCombo != null) charModelCombo.setValue(null); if (charAnimSetCombo != null) charAnimSetCombo.setValue(null); @@ -1373,8 +1438,6 @@ public class ContentController { ? new de.blight.common.model.MainCharacter() : new de.blight.common.model.NPC(); c.setCharacterId(id); - String nameId = charNameField != null ? charNameField.getText().trim() : ""; - c.setName(nameId.isBlank() ? null : new de.blight.common.model.TextReference(nameId)); c.setModelPath(charModelCombo != null ? charModelCombo.getValue() : null); c.setAnimSetPath(charAnimSetCombo != null ? charAnimSetCombo.getValue() : null); if (c instanceof de.blight.common.model.NPC npc) { @@ -1404,12 +1467,24 @@ public class ContentController { } else { npc.setItems(null); } + String wid = charWeaponCombo != null ? charWeaponCombo.getValue() : null; + npc.setEquippedWeaponId(wid != null && !wid.isBlank() ? wid : null); + String sid = charShieldCombo != null ? charShieldCombo.getValue() : null; + npc.setEquippedShieldId(sid != null && !sid.isBlank() ? sid : null); + npc.setMaxHp(parseStatField(charMaxHpField, 100)); + npc.setMaxMana(parseStatField(charMaxManaField, 100)); + npc.setMaxStamina(parseStatField(charMaxStaminaField, 100)); } if (c instanceof de.blight.common.model.MainCharacter mc) { mc.setAbilities(buildAbilities()); } try { de.blight.common.model.CharacterIO.save(c, charDir); + if (c instanceof de.blight.common.model.NPC) { + String langKey = "npc." + id + ".name"; + String langName = deriveDisplayName(id); + de.blight.editor.ui.LangDefaults.writeIfAbsent(langKey, langName); + } refreshContentCharacterTree(); if (charEditorStatusLabel != null) charEditorStatusLabel.setText("Gespeichert: " + id); } catch (Exception e) { @@ -1417,6 +1492,27 @@ public class ContentController { } } + private static int parseStatField(TextField field, int defaultVal) { + if (field == null) { return defaultVal; } + try { + int v = Integer.parseInt(field.getText().trim()); + return v > 0 ? v : defaultVal; + } catch (NumberFormatException e) { + return defaultVal; + } + } + + private static String deriveDisplayName(String id) { + String s = id.replaceAll("[0-9]", "").replace('_', ' ').replace('-', ' ').trim(); + StringBuilder sb = new StringBuilder(); + for (String word : s.split(" +")) { + if (word.isEmpty()) continue; + if (sb.length() > 0) sb.append(' '); + sb.append(Character.toUpperCase(word.charAt(0))).append(word.substring(1)); + } + return sb.isEmpty() ? id : sb.toString(); + } + private void refreshFractionCombo() { loadedFractions.clear(); loadedFractions.addAll( diff --git a/blight-editor/src/main/java/de/blight/editor/toolset/game/GameController.java b/blight-editor/src/main/java/de/blight/editor/toolset/game/GameController.java index e156ade..8a3468d 100644 --- a/blight-editor/src/main/java/de/blight/editor/toolset/game/GameController.java +++ b/blight-editor/src/main/java/de/blight/editor/toolset/game/GameController.java @@ -268,6 +268,7 @@ public class GameController { javaExe, "--add-opens", "java.base/java.lang=ALL-UNNAMED", "--add-opens", "java.desktop/sun.awt=ALL-UNNAMED", + "-Xmx2g", "-Djava.library.path=" + libPath, "-Dblight.project.root=" + projRoot, "-D" + de.blight.common.MapIO.PROP_SESSION_MAP + "=" + sessionMapPath, diff --git a/blight-editor/src/main/java/de/blight/editor/ui/RoutineEditorView.java b/blight-editor/src/main/java/de/blight/editor/ui/RoutineEditorView.java index 1cff012..d602e56 100644 --- a/blight-editor/src/main/java/de/blight/editor/ui/RoutineEditorView.java +++ b/blight-editor/src/main/java/de/blight/editor/ui/RoutineEditorView.java @@ -69,7 +69,12 @@ public class RoutineEditorView extends VBox { private final List placedItems = new ArrayList<>(); private final List npcIds = new ArrayList<>(); - private boolean updatingForm = false; + private boolean updatingForm = false; + private Runnable onDataChanged = null; + + public void setOnDataChanged(Runnable r) { this.onDataChanged = r; } + + private void fireDataChanged() { if (onDataChanged != null) onDataChanged.run(); } // ── Polling-Timer ────────────────────────────────────────────────────────── @@ -358,6 +363,7 @@ public class RoutineEditorView extends VBox { activeBlock.getActivity().getWaypoints().add(wp); refreshWaypointList(); updateMarkers(); + fireDataChanged(); })); delWp.setOnAction(e -> { @@ -366,6 +372,7 @@ public class RoutineEditorView extends VBox { wps.remove(wps.size() - 1); refreshWaypointList(); updateMarkers(); + fireDataChanged(); } }); @@ -408,6 +415,7 @@ public class RoutineEditorView extends VBox { activeBlock.setStartHour(startSpin.getValue()); activeBlock.setEndHour(endSpin.getValue()); refreshCoverage(); + fireDataChanged(); } private void applyActivityToBlock() { @@ -456,6 +464,7 @@ public class RoutineEditorView extends VBox { } // PATROL is managed directly in buildPatrolFields } + fireDataChanged(); } // ── Routine-Operationen ──────────────────────────────────────────────────── @@ -470,6 +479,7 @@ public class RoutineEditorView extends VBox { routines.add(r); refreshRoutineList(); routineList.getSelectionModel().selectLast(); + fireDataChanged(); }); } @@ -482,6 +492,7 @@ public class RoutineEditorView extends VBox { if (!name.isBlank()) { activeRoutine.setName(name.trim()); refreshRoutineList(); + fireDataChanged(); } }); } @@ -494,6 +505,7 @@ public class RoutineEditorView extends VBox { refreshRoutineList(); if (!routines.isEmpty()) routineList.getSelectionModel().select(Math.min(idx, routines.size() - 1)); + fireDataChanged(); } private void addBlock() { @@ -502,6 +514,7 @@ public class RoutineEditorView extends VBox { activeRoutine.getBlocks().add(b); refreshBlockList(); blockList.getSelectionModel().selectLast(); + fireDataChanged(); } private void deleteBlock() { @@ -514,6 +527,7 @@ public class RoutineEditorView extends VBox { refreshCoverage(); if (!activeRoutine.getBlocks().isEmpty()) blockList.getSelectionModel().select(Math.min(idx, activeRoutine.getBlocks().size() - 1)); + fireDataChanged(); } private void moveBlock(int delta) { diff --git a/blight-game/src/main/java/de/blight/game/control/NpcWeaponAttachmentControl.java b/blight-game/src/main/java/de/blight/game/control/NpcWeaponAttachmentControl.java new file mode 100644 index 0000000..01568c6 --- /dev/null +++ b/blight-game/src/main/java/de/blight/game/control/NpcWeaponAttachmentControl.java @@ -0,0 +1,255 @@ +package de.blight.game.control; + +import com.jme3.asset.AssetManager; +import com.jme3.math.Matrix4f; +import com.jme3.math.Quaternion; +import com.jme3.math.Vector3f; +import com.jme3.renderer.RenderManager; +import com.jme3.renderer.ViewPort; +import com.jme3.scene.Node; +import com.jme3.scene.Spatial; +import com.jme3.scene.control.AbstractControl; +import de.blight.common.model.Item; +import de.blight.common.model.ItemGrip; +import de.blight.game.animation.RetargetingSystem; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Hängt Waffe und Schild am NPC – 1:1 dieselbe Logik wie WeaponAttachmentControl (Held). + * - weggesteckt (drawn=false) → Spine2-Knochen (Rücken) + * - gezückt (drawn=true) → RightHand (Waffe) / LeftHand (Schild am Arm) + */ +public class NpcWeaponAttachmentControl extends AbstractControl { + + private static final Logger log = LoggerFactory.getLogger(NpcWeaponAttachmentControl.class); + + private static final String BONE_SPINE = "mixamorig:Spine2"; + private static final String BONE_HAND = "mixamorig:RightHand"; + private static final String BONE_PALM = "mixamorig:RightHandMiddle1"; + private static final String BONE_LEFT_FOREARM = "mixamorig:LeftHand"; + private static final float DEG_TO_RAD = (float) Math.PI / 180f; + private static final float HALF_PI = (float) Math.PI * 0.5f; + + private static final Vector3f BACK_GRIP_OFFSET = new Vector3f(0.0f, 0.40f, -0.13f); + private static final Vector3f HAND_GRIP_OFFSET = new Vector3f(0.1f, 0.00f, 0.05f); + private static final Vector3f SHIELD_BACK_OFFSET = new Vector3f(0.12f, -0.10f, -0.10f); + + private static final float SHIELD_ARM_ROT_X = -HALF_PI; + private static final float SHIELD_ARM_ROT_Y = 0f; + private static final float SHIELD_ARM_ROT_Z = 0f; + private static final float SHIELD_ARM_ELBOW_OFFSET = 0.25f; + + private final AssetManager assetManager; + private final Item weaponItem; + private final Item shieldItem; + + private com.jme3.anim.SkinningControl skinCtrl; + private Node backNode; + private Node handNode; + private Node palmNodeRef; + private Node leftForeArmNode; + + private Quaternion handBindRotInChar; + private Quaternion leftForeArmBindRotInChar; + private Vector3f palmOffsetLocal; + private int palmOffsetFrameDelay = 0; + + private Spatial weaponSpatial; + private Spatial shieldSpatial; + private boolean drawn = false; + + public NpcWeaponAttachmentControl(AssetManager assetManager, Item weaponItem, Item shieldItem) { + this.assetManager = assetManager; + this.weaponItem = weaponItem; + this.shieldItem = shieldItem; + } + + public void setDrawn(boolean drawn) { + this.drawn = drawn; + } + + @Override + protected void controlUpdate(float tpf) { + if (skinCtrl == null) { + skinCtrl = RetargetingSystem.findSkinningControl(spatial); + if (skinCtrl == null) { return; } + + try { backNode = skinCtrl.getAttachmentsNode(BONE_SPINE); } + catch (Exception e) { log.warn("[NpcWeaponAttach] Bone '{}' nicht gefunden: {}", BONE_SPINE, e.getMessage()); } + try { handNode = skinCtrl.getAttachmentsNode(BONE_HAND); } + catch (Exception e) { log.warn("[NpcWeaponAttach] Bone '{}' nicht gefunden: {}", BONE_HAND, e.getMessage()); } + try { + palmNodeRef = skinCtrl.getAttachmentsNode(BONE_PALM); + palmOffsetFrameDelay = 2; + } catch (Exception e) { log.warn("[NpcWeaponAttach] Bone '{}' nicht gefunden: {}", BONE_PALM, e.getMessage()); } + try { leftForeArmNode = skinCtrl.getAttachmentsNode(BONE_LEFT_FOREARM); } + catch (Exception e) { log.warn("[NpcWeaponAttach] Bone '{}' nicht gefunden: {}", BONE_LEFT_FOREARM, e.getMessage()); } + + handBindRotInChar = extractJointBindRot(skinCtrl, BONE_HAND); + leftForeArmBindRotInChar = extractJointBindRot(skinCtrl, BONE_LEFT_FOREARM); + + log.info("[NpcWeaponAttach] Setup: backNode={} handNode={} palmNode={} forearmNode={} weapon={} shield={}", + backNode != null, handNode != null, palmNodeRef != null, leftForeArmNode != null, + weaponItem != null ? weaponItem.getItemId() : "null", + shieldItem != null ? shieldItem.getItemId() : "null"); + + if (weaponItem != null) { weaponSpatial = loadAndAttach(weaponItem); } + if (shieldItem != null) { shieldSpatial = loadAndAttach(shieldItem); } + + log.info("[NpcWeaponAttach] Geladen: weaponSpatial={} shieldSpatial={}", + weaponSpatial != null, shieldSpatial != null); + } + + // Palm-Offset nach frühestens 2 Frames berechnen (SkinningControl muss vorher gelaufen sein) + if (palmOffsetFrameDelay > 0 && palmNodeRef != null && handNode != null) { + palmOffsetFrameDelay--; + if (palmOffsetFrameDelay == 0) { + palmOffsetLocal = computePalmOffset(handNode, palmNodeRef); + } + } + + Quaternion charWorldRotInv = spatial.getWorldRotation().inverse(); + Vector3f charWorldPos = spatial.getWorldTranslation(); + + // ── Waffe ──────────────────────────────────────────────────────────────── + if (weaponSpatial != null) { + Node boneNode = drawn ? handNode : backNode; + if (boneNode != null) { + Vector3f boneInChar = charWorldRotInv.mult(boneNode.getWorldTranslation().subtract(charWorldPos)); + Quaternion boneRotInChar = charWorldRotInv.mult(boneNode.getWorldRotation()); + if (drawn) { + Vector3f attachInChar = (palmOffsetLocal != null) + ? boneInChar.add(boneRotInChar.mult(palmOffsetLocal.mult(0.5f))) + : boneInChar; + applyHandTransform(weaponSpatial, attachInChar, boneRotInChar, + weaponItem != null ? weaponItem.getGrip() : null); + } else { + applyBackTransform(weaponSpatial, boneInChar, boneRotInChar, + weaponItem != null ? weaponItem.getGrip() : null); + } + } + } + + // ── Schild ──────────────────────────────────────────────────────────────── + if (shieldSpatial != null) { + ItemGrip grip = shieldItem != null ? shieldItem.getGrip() : null; + if (drawn && leftForeArmNode != null) { + Vector3f boneInChar = charWorldRotInv.mult(leftForeArmNode.getWorldTranslation().subtract(charWorldPos)); + Quaternion boneRotInChar = charWorldRotInv.mult(leftForeArmNode.getWorldRotation()); + applyShieldArmTransform(shieldSpatial, boneInChar, boneRotInChar, grip); + shieldSpatial.setCullHint(Spatial.CullHint.Inherit); + } else if (!drawn && backNode != null) { + Vector3f boneInChar = charWorldRotInv.mult(backNode.getWorldTranslation().subtract(charWorldPos)); + Quaternion boneRotInChar = charWorldRotInv.mult(backNode.getWorldRotation()); + applyShieldBackTransform(shieldSpatial, boneInChar, boneRotInChar, grip); + shieldSpatial.setCullHint(Spatial.CullHint.Inherit); + } else { + shieldSpatial.setCullHint(Spatial.CullHint.Always); + } + } + } + + private Spatial loadAndAttach(Item item) { + if (item.getModelRef() == null || item.getModelRef().getPath() == null) { return null; } + try { + Spatial s = assetManager.loadModel(item.getModelRef().getPath()); + ((Node) spatial).attachChild(s); + return s; + } catch (Exception e) { + log.warn("[NpcWeaponAttach] Modell nicht ladbar: {} — {}", item.getModelRef().getPath(), e.getMessage()); + return null; + } + } + + // ── Hand-Transform (1:1 von WeaponAttachmentControl) ───────────────────────── + + private void applyHandTransform(Spatial s, Vector3f bonePosInChar, + Quaternion boneRotInChar, ItemGrip grip) { + Quaternion gripRot = (grip != null) + ? new Quaternion().fromAngles(DEG_TO_RAD * grip.getRotX(), + DEG_TO_RAD * grip.getRotY(), + DEG_TO_RAD * grip.getRotZ()) + : new Quaternion(); + Vector3f gripPos = (grip != null) + ? new Vector3f(grip.getPosX(), grip.getPosY(), grip.getPosZ()) + : Vector3f.ZERO; + Quaternion animDelta = (handBindRotInChar != null) + ? boneRotInChar.mult(handBindRotInChar.inverse()) + : boneRotInChar; + Quaternion weaponRot = animDelta.mult(gripRot.inverse()); + Vector3f weaponPos = bonePosInChar.subtract(weaponRot.mult(gripPos)); + weaponPos = weaponPos.add(weaponRot.mult(HAND_GRIP_OFFSET)); + s.setLocalRotation(weaponRot); + s.setLocalTranslation(weaponPos); + } + + private void applyBackTransform(Spatial s, Vector3f boneInChar, + Quaternion boneRotInChar, ItemGrip grip) { + Quaternion backRot = new Quaternion().fromAngles(0f, 0f, HALF_PI); + Quaternion weaponRot = boneRotInChar.mult(backRot); + Vector3f target = boneInChar.add(boneRotInChar.mult(BACK_GRIP_OFFSET)); + Vector3f gripPos = grip != null + ? new Vector3f(grip.getPosX(), grip.getPosY(), grip.getPosZ()) : Vector3f.ZERO; + s.setLocalRotation(weaponRot); + s.setLocalTranslation(target.subtract(weaponRot.mult(gripPos))); + } + + // ── Schild-Transforms (1:1 von WeaponAttachmentControl) ────────────────────── + + private void applyShieldArmTransform(Spatial s, Vector3f bonePosInChar, + Quaternion boneRotInChar, ItemGrip grip) { + Quaternion animDelta = (leftForeArmBindRotInChar != null) + ? boneRotInChar.mult(leftForeArmBindRotInChar.inverse()) + : boneRotInChar; + Quaternion armRot = new Quaternion().fromAngles(SHIELD_ARM_ROT_X, SHIELD_ARM_ROT_Y, SHIELD_ARM_ROT_Z); + Quaternion shieldRot = animDelta.mult(armRot); + Vector3f gripPos = (grip != null) + ? new Vector3f(grip.getPosX(), grip.getPosY(), grip.getPosZ()) : Vector3f.ZERO; + Vector3f anchor = bonePosInChar.add(boneRotInChar.mult(new Vector3f(0f, -SHIELD_ARM_ELBOW_OFFSET, 0f))); + s.setLocalRotation(shieldRot); + s.setLocalTranslation(anchor.subtract(shieldRot.mult(gripPos))); + } + + private void applyShieldBackTransform(Spatial s, Vector3f boneInChar, + Quaternion boneRotInChar, ItemGrip grip) { + Quaternion backRot = new Quaternion().fromAngles(0f, (float) Math.PI, 0f); + Quaternion shieldRot = boneRotInChar.mult(backRot); + Vector3f target = boneInChar.add(boneRotInChar.mult(SHIELD_BACK_OFFSET)); + Vector3f gripPos = (grip != null) + ? new Vector3f(grip.getPosX(), grip.getPosY(), grip.getPosZ()) : Vector3f.ZERO; + s.setLocalRotation(shieldRot); + s.setLocalTranslation(target.subtract(shieldRot.mult(gripPos))); + } + + // ── Hilfsmethoden ──────────────────────────────────────────────────────────── + + private static Vector3f computePalmOffset(Node handNode, Node palmNode) { + Vector3f deltaWorld = palmNode.getWorldTranslation().subtract(handNode.getWorldTranslation()); + return handNode.getWorldRotation().inverse().mult(deltaWorld); + } + + private static Quaternion extractJointBindRot(com.jme3.anim.SkinningControl sc, String jointName) { + com.jme3.anim.Armature arm = sc.getArmature(); + if (arm == null) { return null; } + com.jme3.anim.Joint joint = arm.getJoint(jointName); + if (joint == null) { return null; } + Matrix4f invBind = joint.getInverseModelBindMatrix(); + if (invBind == null) { return null; } + Matrix4f bindMat = invBind.invert(); + if (bindMat == null) { return null; } + return bindMat.toRotationQuat().normalizeLocal(); + } + + @Override + public void setSpatial(Spatial spatial) { + super.setSpatial(spatial); + if (spatial == null) { + if (weaponSpatial != null) { weaponSpatial.removeFromParent(); weaponSpatial = null; } + if (shieldSpatial != null) { shieldSpatial.removeFromParent(); shieldSpatial = null; } + } + } + + @Override + protected void controlRender(RenderManager rm, ViewPort vp) {} +} 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 213ba32..2b67b81 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 @@ -38,14 +38,16 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.nio.file.Path; +import java.util.ArrayList; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; public class PlayerInputControl { private static final Logger log = LoggerFactory.getLogger(PlayerInputControl.class); - private static final float MOVE_SPEED = 0.07f; + private static final float MOVE_SPEED = CharacterNavigator.RUN_SPEED; private static final float SPRINT_MULT = 1.5f; private static final float WALK_MULT = 0.5f; private static final float ROTATE_SPEED = 10f; @@ -153,7 +155,7 @@ public class PlayerInputControl { private float heavyLungeEndRemaining = 0f; /** Gesamtdauer der aktuellen Kampfanim — für Kombo-Fenster-Timing (öffnet bei 50%). */ private float combatAnimDuration = 0f; - private MeleeAttackCallback meleeAttackCallback = null; + private final List meleeCallbacks = new ArrayList<>(); private WeaponAttachmentControl weaponAttach = null; private static final float HIT_WINDOW_START = 0.20f; private static final float HIT_WINDOW_END = 0.75f; @@ -430,7 +432,22 @@ public class PlayerInputControl { public ItemSubCategory getDrawnWeaponType() { return drawnWeaponType; } public boolean isLastAttackCritical() { return lastAttackCritical; } public com.jme3.anim.SkinningControl getSkinningControl() { return skinningControl; } - public void setMeleeAttackCallback(MeleeAttackCallback cb) { this.meleeAttackCallback = cb; } + + /** + * Geräusch-Radius des Spielers in Welteinheiten (m) – abhängig von der Bewegungsart: + * Sprint → 10 m, normales Laufen → 5 m, Gehen (walk-Taste) → 3 m, + * Schleichen / Stehen → 0 (kein Geräusch). + */ + public float getSoundRadius() { + boolean sneaking = sneakPhase != SneakPhase.NONE; + boolean moving = forward || backward || left || right; + if (!moving || sneaking) { return 0f; } + if (sprint) { return 10f; } + if (walk) { return 3f; } + return 5f; + } + public void addMeleeAttackCallback(MeleeAttackCallback cb) { if (cb != null) meleeCallbacks.add(cb); } + public void setMeleeAttackCallback(MeleeAttackCallback cb) { addMeleeAttackCallback(cb); } public void setWeaponAttachmentControl(WeaponAttachmentControl w) { this.weaponAttach = w; } public boolean isPaused() { return paused; } @@ -1345,6 +1362,13 @@ public class PlayerInputControl { || blockPhase == BlockPhase.EXITING; } + /** True wenn der Spieler gerade läuft oder rennt (nicht steht, schleicht oder geht). */ + public boolean isRunningOrSprinting() { + boolean moving = forward || backward || left || right; + boolean sneaking = sneakPhase != SneakPhase.NONE; + return moving && !sneaking && !walk; + } + /** Wird aufgerufen wenn ein blockierter Treffer landet. Kostet 30 Ausdauer; unter 0 → Block bricht. */ public void triggerBlockImpact() { if (blockPhase != BlockPhase.HOLDING || drawnWeaponType == null) return; @@ -1621,7 +1645,7 @@ public class PlayerInputControl { } private void fireWeaponCallback(float critMultiplier) { - if (meleeAttackCallback == null || weaponAttach == null) { + if (meleeCallbacks.isEmpty() || weaponAttach == null) { return; } com.jme3.scene.Spatial ws = weaponAttach.getWeaponSpatial(); @@ -1633,7 +1657,9 @@ public class PlayerInputControl { ? mainCharacter.getEquippedMeleeWeapon().getDamage() : 0; Vector3f grip = hn.getWorldTranslation().clone(); Vector3f bladeDir = ws.getWorldRotation().mult(com.jme3.math.Vector3f.UNIT_Y).normalizeLocal(); - meleeAttackCallback.onMeleeAttack(currentAttackId, grip, bladeDir, WEAPON_BLADE_LENGTH, base, critMultiplier); + for (MeleeAttackCallback cb : meleeCallbacks) { + cb.onMeleeAttack(currentAttackId, grip, bladeDir, WEAPON_BLADE_LENGTH, base, critMultiplier); + } } private float computeHeavyMultiplier() { diff --git a/blight-game/src/main/java/de/blight/game/navigation/CharacterNavigator.java b/blight-game/src/main/java/de/blight/game/navigation/CharacterNavigator.java index 2067bb1..6e37706 100644 --- a/blight-game/src/main/java/de/blight/game/navigation/CharacterNavigator.java +++ b/blight-game/src/main/java/de/blight/game/navigation/CharacterNavigator.java @@ -56,6 +56,10 @@ public class CharacterNavigator { public static final float WALK_SPEED = 0.035f; /** Renn-Geschwindigkeit (m/frame bei 60fps). */ public static final float RUN_SPEED = 0.07f; + /** Renn-Geschwindigkeit in m/s (für tpf-basierte Bewegung, z.B. NPCs ohne CharacterControl). */ + public static final float RUN_SPEED_MPS = 4.2f; + /** Geh-Geschwindigkeit in m/s. */ + public static final float WALK_SPEED_MPS = 2.1f; /** Minimale Bewegung (m/s) unter der ein Charakter als steckengeblieben gilt. */ private static final float STUCK_MIN_SPEED = 0.2f; /** Zeit (s) mit zu langsamer Bewegung bis zum Abbruch. */ diff --git a/blight-game/src/main/java/de/blight/game/npc/AggroEvent.java b/blight-game/src/main/java/de/blight/game/npc/AggroEvent.java new file mode 100644 index 0000000..06cb5d3 --- /dev/null +++ b/blight-game/src/main/java/de/blight/game/npc/AggroEvent.java @@ -0,0 +1,10 @@ +package de.blight.game.npc; + +public enum AggroEvent { + NONE, + WARN_1, // first warning issued (ENRAGED: first sight) + WARN_2, // second warning issued (ENRAGED: after 10 s) + ATTACK_START, // NPC commits to attacking + PURSUIT_BREAK, // NPC gives up the chase + SOUND_ALERT // player heard but not yet seen — NPC should turn toward sound source +} diff --git a/blight-game/src/main/java/de/blight/game/npc/NpcAggroBehavior.java b/blight-game/src/main/java/de/blight/game/npc/NpcAggroBehavior.java new file mode 100644 index 0000000..3acf8dc --- /dev/null +++ b/blight-game/src/main/java/de/blight/game/npc/NpcAggroBehavior.java @@ -0,0 +1,249 @@ +package de.blight.game.npc; + +import com.jme3.collision.CollisionResults; +import com.jme3.math.FastMath; +import com.jme3.math.Ray; +import com.jme3.math.Vector3f; +import com.jme3.scene.Node; +import de.blight.common.model.Status; +import de.blight.common.model.TextReference; + +import java.util.Random; + +/** + * Per-entity aggro state machine — works for NPCs, animals, or any creature. + * + * ENRAGED: warn once → 10 s → warn again → 10 s → attack + * ENEMY: attack immediately on sight + * Player attacks NPC directly → skip warnings, retaliate immediately + * + * Pursuit ends when the player is >25 m away OR the NPC has moved >100 m + * from the schedule anchor that was captured when ATTACKING began. + */ +public class NpcAggroBehavior { + + public enum AggroPhase { + CALM, + WARNED_ONCE, // first warning given; timer counting to second warning + WARNED_TWICE, // second warning given; timer counting to attack + ATTACKING + } + + // ── Shared phrase pools (same text for every NPC) ───────────────────────── + + public static final TextReference[] WARN1_TEXTS = { + new TextReference("npc.aggro.warn1.1"), + new TextReference("npc.aggro.warn1.2"), + new TextReference("npc.aggro.warn1.3"), + new TextReference("npc.aggro.warn1.4"), + new TextReference("npc.aggro.warn1.5"), + }; + + public static final TextReference[] WARN2_TEXTS = { + new TextReference("npc.aggro.warn2.1"), + new TextReference("npc.aggro.warn2.2"), + new TextReference("npc.aggro.warn2.3"), + new TextReference("npc.aggro.warn2.4"), + new TextReference("npc.aggro.warn2.5"), + }; + + public static final TextReference[] ATTACK_TEXTS = { + new TextReference("npc.aggro.attack.1"), + new TextReference("npc.aggro.attack.2"), + new TextReference("npc.aggro.attack.3"), + new TextReference("npc.aggro.attack.4"), + new TextReference("npc.aggro.attack.5"), + }; + + public static final TextReference[] BREAK_TEXTS = { + new TextReference("npc.aggro.break.1"), + new TextReference("npc.aggro.break.2"), + new TextReference("npc.aggro.break.3"), + new TextReference("npc.aggro.break.4"), + new TextReference("npc.aggro.break.5"), + }; + + /** + * Audio path convention: audio/npc/{npcId}/aggro_{category}_{1-5}.ogg + * category = "warn1" | "warn2" | "attack" + */ + public static String audioPath(String npcId, String category, int phraseIdx) { + return "audio/npc/" + npcId + "/aggro_" + category + "_" + (phraseIdx + 1) + ".ogg"; + } + + // ── Constants ───────────────────────────────────────────────────────────── + + private static final float WARN_INTERVAL = 10f; + private static final float FOV_HALF_ANGLE_COS = FastMath.cos(FastMath.DEG_TO_RAD * 45f); + public static final float PURSUIT_BREAK_DIST = 25f; + public static final float RE_AGGRO_DIST = 15f; // nach PURSUIT_BREAK erst ab hier wieder angreifen + public static final float SCHEDULE_MAX_DIST = 100f; + + // ── Per-instance state ──────────────────────────────────────────────────── + + private AggroPhase phase = AggroPhase.CALM; + private float stateTimer = 0f; + private Vector3f scheduleAnchor = null; + private boolean reAggroGuard = false; // gesetzt nach PURSUIT_BREAK bis Player < RE_AGGRO_DIST + + private final Random rng = new Random(); + + // ── Public API ──────────────────────────────────────────────────────────── + + public AggroPhase getPhase() { return phase; } + public boolean isAttacking() { return phase == AggroPhase.ATTACKING; } + + /** + * Frame update — call every frame for any entity whose status is ENRAGED or ENEMY. + * + * @param tpf time per frame + * @param npcPos NPC feet position (world space) + * @param npcForward NPC forward direction (world space, from spatial rotation) + * @param status current status of this entity + * @param playerPos player feet position (PhysicsLocation) + * @param playerSoundRadius sound detection radius: 10m=sprint, 5m=run, 3m=walk, 0=sneak/still + * @param rootNode scene root for LOS raycasting + * @param schedulePos where this entity should be per its current routine (may be null) + */ + public AggroEvent update(float tpf, + Vector3f npcPos, Vector3f npcForward, Status status, + Vector3f playerPos, float playerSoundRadius, + Node rootNode, Vector3f schedulePos) { + if (status != Status.ENRAGED && status != Status.ENEMY) { + if (phase != AggroPhase.CALM) { + phase = AggroPhase.CALM; + scheduleAnchor = null; + } + return AggroEvent.NONE; + } + + switch (phase) { + case CALM: { + if (canSeePlayer(npcPos, npcForward, playerPos, rootNode)) { + // Hysterese: nach PURSUIT_BREAK erst re-aggro wenn Player nah genug herankommt + if (reAggroGuard && dist2d(npcPos, playerPos) > RE_AGGRO_DIST) { + break; + } + reAggroGuard = false; + if (status == Status.ENEMY) { + return transitionToAttacking(npcPos, schedulePos); + } + phase = AggroPhase.WARNED_ONCE; + stateTimer = 0f; + return AggroEvent.WARN_1; + } + // Spieler nicht sichtbar – Geräusch-Check (kein FOV, kein LOS nötig) + if (playerSoundRadius > 0f && dist2d(npcPos, playerPos) <= playerSoundRadius) { + return AggroEvent.SOUND_ALERT; + } + break; + } + case WARNED_ONCE: { + if (!canSeePlayer(npcPos, npcForward, playerPos, rootNode)) { + phase = AggroPhase.CALM; + break; + } + stateTimer += tpf; + if (stateTimer >= WARN_INTERVAL) { + phase = AggroPhase.WARNED_TWICE; + stateTimer = 0f; + return AggroEvent.WARN_2; + } + break; + } + case WARNED_TWICE: { + if (!canSeePlayer(npcPos, npcForward, playerPos, rootNode)) { + // One more chance — back to first warning + phase = AggroPhase.WARNED_ONCE; + stateTimer = 0f; + break; + } + stateTimer += tpf; + if (stateTimer >= WARN_INTERVAL) { + return transitionToAttacking(npcPos, schedulePos); + } + break; + } + case ATTACKING: { + if (dist2d(npcPos, playerPos) > PURSUIT_BREAK_DIST) { + phase = AggroPhase.CALM; + scheduleAnchor = null; + reAggroGuard = true; + return AggroEvent.PURSUIT_BREAK; + } + if (scheduleAnchor != null && dist2d(npcPos, scheduleAnchor) > SCHEDULE_MAX_DIST) { + phase = AggroPhase.CALM; + scheduleAnchor = null; + reAggroGuard = true; + return AggroEvent.PURSUIT_BREAK; + } + break; + } + } + return AggroEvent.NONE; + } + + /** + * Call when the player lands a direct hit on this entity. + * Bypasses all warning phases — the entity retaliates immediately. + */ + public AggroEvent onAttackedByPlayer(Vector3f npcPos, Vector3f schedulePos) { + if (phase == AggroPhase.ATTACKING) { return AggroEvent.NONE; } + reAggroGuard = false; + return transitionToAttacking(npcPos, schedulePos); + } + + /** Returns a random phrase index in [0, 4]. */ + public int randomPhraseIdx() { return rng.nextInt(5); } + + // ── Static sight check ──────────────────────────────────────────────────── + + /** + * True if the player is inside the entity's horizontal FOV (±45°) and + * no scene geometry blocks line of sight. + * + * The check uses a horizontal cone only — vertical angle is ignored so + * NPCs on slightly different elevations still notice the player. + */ + public static boolean canSeePlayer(Vector3f npcPos, Vector3f npcForward, + Vector3f playerPos, Node rootNode) { + Vector3f npcEye = npcPos.add(0f, 1.6f, 0f); + Vector3f playerMid = playerPos.add(0f, 0.9f, 0f); + + // Horizontal FOV check + float fx = npcForward.x, fz = npcForward.z; + float px = playerMid.x - npcEye.x, pz = playerMid.z - npcEye.z; + float fLen = (float) Math.sqrt(fx * fx + fz * fz); + float pLen = (float) Math.sqrt(px * px + pz * pz); + if (fLen > 0.001f && pLen > 0.001f) { + float dot = (fx * px + fz * pz) / (fLen * pLen); + if (dot < FOV_HALF_ANGLE_COS) { return false; } + } + + // LOS raycast + Vector3f dir = playerMid.subtract(npcEye); + float fullDist = dir.length(); + if (fullDist < 0.001f) { return true; } + dir.divideLocal(fullDist); + + CollisionResults cr = new CollisionResults(); + rootNode.collideWith(new Ray(npcEye, dir), cr); + if (cr.size() == 0) { return true; } + float hitDist = cr.getClosestCollision().getDistance(); + if (hitDist < 0.5f) { return true; } // skip NPC's own geometry + return hitDist >= fullDist - 0.3f; + } + + // ── Private helpers ─────────────────────────────────────────────────────── + + private AggroEvent transitionToAttacking(Vector3f npcPos, Vector3f schedulePos) { + scheduleAnchor = (schedulePos != null) ? schedulePos.clone() : npcPos.clone(); + phase = AggroPhase.ATTACKING; + return AggroEvent.ATTACK_START; + } + + private static float dist2d(Vector3f a, Vector3f b) { + float dx = a.x - b.x, dz = a.z - b.z; + return (float) Math.sqrt(dx * dx + dz * dz); + } +} 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 ecb1ae8..8432b17 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 @@ -623,6 +623,8 @@ public class WorldScene extends BaseAppState { animLib.configureLocomotionSnap(AnimationLibrary.findAssetRoot(), setName); } animLib.applyAllTo(characterVisual != null ? characterVisual : character); + WorldNpcsState npcsState = app.getStateManager().getState(WorldNpcsState.class); + if (npcsState != null) npcsState.reapplyAnimations(); log.info("[AnimCtx] MainCharacter: {} animSetPath: {} clipCount: {} clips: {}", mc != null ? mc.getCharacterId() : "null", setName, 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 29d545e..d2b2a14 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 @@ -24,7 +24,8 @@ public class InteractionHudState extends BaseAppState { private static final Logger log = LoggerFactory.getLogger(InteractionHudState.class); - private static final float Y_OFFSET = 0.6f; + private static final float Y_OFFSET = 0.6f; + private static final ColorRGBA COL_DEFAULT = new ColorRGBA(1f, 0.95f, 0.6f, 1f); private Camera cam; private Node guiNode; @@ -52,7 +53,7 @@ public class InteractionHudState extends BaseAppState { labelText = new BitmapText(font); labelText.setSize(font.getCharSet().getRenderedSize() * 1.2f * 2f); labelText.setLocalScale(0.5f, 0.5f, 1f); - labelText.setColor(new ColorRGBA(1f, 0.95f, 0.6f, 1f)); + labelText.setColor(COL_DEFAULT.clone()); labelText.setCullHint(Spatial.CullHint.Always); guiNode.attachChild(labelText); } @@ -90,15 +91,18 @@ public class InteractionHudState extends BaseAppState { return; } - String labelKey = null; - Vector3f worldPos = null; + String labelKey = null; + Vector3f worldPos = null; + com.jme3.math.ColorRGBA labelColor = COL_DEFAULT; // 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(); + labelKey = key; + worldPos = worldNpcs.getHoveredWorldPos(); + com.jme3.math.ColorRGBA c = worldNpcs.getHoveredLabelColor(); + if (c != null) labelColor = c; } } @@ -143,9 +147,10 @@ public class InteractionHudState extends BaseAppState { if (keyBindings != null) { text = text.replace("{interact}", KeyNames.of(keyBindings.interact)); } + labelText.setColor(labelColor); labelText.setText(text); - float textW = labelText.getLineWidth(); - labelText.setLocalTranslation(screenV.x - textW * 0.5f, screenV.y, 1f); + float textW = labelText.getLineWidth() * 0.5f; + labelText.setLocalTranslation(screenV.x - textW, screenV.y, 1f); labelText.setCullHint(Spatial.CullHint.Inherit); } } diff --git a/blight-game/src/main/java/de/blight/game/state/WorldDummyState.java b/blight-game/src/main/java/de/blight/game/state/WorldDummyState.java index 751e900..feb0e2d 100644 --- a/blight-game/src/main/java/de/blight/game/state/WorldDummyState.java +++ b/blight-game/src/main/java/de/blight/game/state/WorldDummyState.java @@ -130,7 +130,7 @@ public class WorldDummyState extends BaseAppState implements MeleeAttackCallback loadDummies(); buildEnemyHud(); - playerInput.setMeleeAttackCallback(this); + playerInput.addMeleeAttackCallback(this); } @Override 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 index 6064b6c..66661ad 100644 --- a/blight-game/src/main/java/de/blight/game/state/WorldNpcsState.java +++ b/blight-game/src/main/java/de/blight/game/state/WorldNpcsState.java @@ -5,7 +5,10 @@ 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.BulletAppState; +import com.jme3.bullet.collision.shapes.CapsuleCollisionShape; import com.jme3.bullet.control.CharacterControl; +import com.jme3.bullet.control.RigidBodyControl; import com.jme3.collision.CollisionResult; import com.jme3.collision.CollisionResults; import com.jme3.input.MouseInput; @@ -13,10 +16,15 @@ 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.*; import com.jme3.renderer.Camera; +import com.jme3.renderer.queue.RenderQueue; import com.jme3.scene.*; +import com.jme3.scene.control.BillboardControl; import com.jme3.scene.shape.Box; +import com.jme3.scene.shape.Quad; +import com.jme3.util.BufferUtils; import com.jme3.anim.AnimComposer; import com.jme3.anim.Armature; import com.jme3.anim.ArmatureMask; @@ -24,6 +32,12 @@ import com.jme3.anim.Joint; import com.jme3.anim.SkinningControl; import com.jme3.anim.tween.Tween; import com.jme3.anim.tween.action.BaseAction; +import com.jme3.audio.AudioData; +import com.jme3.audio.AudioNode; +import com.jme3.font.BitmapFont; +import com.jme3.font.BitmapText; +import de.blight.game.config.MenuCanvas; +import de.blight.game.config.NinePatch; import de.blight.common.PlacedModel; import de.blight.common.PlacedModelIO; import de.blight.common.SaveGameIO; @@ -33,6 +47,13 @@ import de.blight.common.model.trigger.NpcStatusTrigger; import de.blight.game.animation.AnimationAction; import de.blight.game.animation.AnimationLibrary; import de.blight.game.config.KeyBindings; +import de.blight.game.combat.DamageCalculator; +import de.blight.game.combat.MeleeAttackCallback; +import de.blight.game.npc.AggroEvent; +import de.blight.game.npc.NpcAggroBehavior; +import de.blight.lang.TextResolver; +import de.blight.game.navigation.CharacterNavigator; +import de.blight.game.control.NpcWeaponAttachmentControl; import de.blight.game.control.PlayerInputControl; import de.blight.game.control.ThirdPersonCamera; import de.blight.game.scene.WorldScene; @@ -40,6 +61,7 @@ import de.blight.game.state.TerrainChunkState; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.nio.FloatBuffer; import java.util.*; /** @@ -49,7 +71,7 @@ import java.util.*; * 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 { +public class WorldNpcsState extends BaseAppState implements MeleeAttackCallback { private static final Logger log = LoggerFactory.getLogger(WorldNpcsState.class); @@ -90,17 +112,57 @@ public class WorldNpcsState extends BaseAppState { private final List spawned = new ArrayList<>(); private static class SpawnedNpc { - final NPC npc; - final Spatial visual; - final List pbrMaterials = new ArrayList<>(); + final NPC npc; + final Spatial visual; + final List pbrMaterials = new ArrayList<>(); float worldX, worldZ; - AnimationAction currentAction = null; + AnimationAction currentAction = null; + final NpcAggroBehavior aggroBehavior = new NpcAggroBehavior(); + boolean weaponDrawn = false; + float drawAnimTimer = 0f; + boolean drawAnimActive = false; + boolean drawMidFired = false; + boolean isSheathing = false; + AnimationAction combatRunAnim = null; + boolean isReturning = false; + Vector3f returnTarget = null; + float returnFaceTimer = 0f; // nach SOUND_ALERT: kurz zum Spieler drehen + // $AssimpFbx$_Rotation-Joints für Zieh-Animation (lazy init) + Joint jRightArmFbx = null; + Joint jRightForeArmFbx = null; + Joint jLeftArm = null; + Joint jLeftForeArm = null; + boolean drawJointsInit = false; + Quaternion drawStartArmQ = null; + Quaternion drawStartForeArmQ = null; + + // ── HP / Mana / Stamina ─────────────────────────────────────────────── + int maxHp; + int currentHp; + int maxMana; + int currentMana; + int maxStamina; + int currentStamina; + + // ── Welt-HP-Balken ──────────────────────────────────────────────────── + Node hpBarNode = null; + Geometry hpBarFg = null; + + // ── Physik-Kapsel (Kollision) ───────────────────────────────────────── + RigidBodyControl rbc = null; SpawnedNpc(NPC npc, Spatial visual, float x, float z) { this.npc = npc; this.visual = visual; this.worldX = x; this.worldZ = z; + + int hp = npc.getMaxHp() > 0 ? npc.getMaxHp() : DEFAULT_NPC_MAX_HP; + int mp = npc.getMaxMana() > 0 ? npc.getMaxMana() : DEFAULT_NPC_MAX_MANA; + int sta = npc.getMaxStamina() > 0 ? npc.getMaxStamina() : DEFAULT_NPC_MAX_STAMINA; + this.maxHp = hp; this.currentHp = hp; + this.maxMana = mp; this.currentMana = mp; + this.maxStamina = sta; this.currentStamina = sta; } NPC npc() { return npc; } @@ -121,6 +183,7 @@ public class WorldNpcsState extends BaseAppState { // ── NPC Head-Look während Dialog ────────────────────────────────────────── private static final String HEAD_LAYER = "__dlg_headlook__"; + private static final String NPC_DRAW_LAYER = "__npc_draw__"; private HeadLookTween dialogHeadTween = null; // Einmaliger Warmup: BVH-Baum des rootNode beim ersten NPC-Spawn aufbauen, @@ -153,6 +216,63 @@ public class WorldNpcsState extends BaseAppState { private final Quaternion scratchQ = new Quaternion(); private final com.jme3.math.ColorRGBA scratchEmissive = new com.jme3.math.ColorRGBA(); + // ── NPC Speech Display ──────────────────────────────────────────────────── + + private Node guiNode; + private BulletAppState bulletAppState; + private Node speechCanvasNode; + private Node speechPanelNode; + private BitmapText speechNameText; + private BitmapText speechText; + private SpawnedNpc speechSpeaker; + private float speechTimer; + private AudioNode speechAudioNode; + private static final float SPEECH_DURATION = 5f; + + private static final float SP_X = 0f; + private static final float SP_H = 42f; + private static final float SP_Y = MenuCanvas.REF_H - SP_H; + private static final float SP_W = MenuCanvas.REF_W; + private static final float SP_MARGIN = 24f; + private static final float SP_TEXT_Y = SP_Y + SP_H - 22f; + private static final ColorRGBA SP_COL_NAME = new ColorRGBA(1.00f, 0.90f, 0.55f, 1f); + private static final ColorRGBA SP_COL_TEXT = ColorRGBA.White; + + // ── NPC Kampf-Verfolgung ────────────────────────────────────────────────── + + private static final float NPC_MOVE_SPEED = CharacterNavigator.RUN_SPEED_MPS; + private static final float DRAW_ANIM_DURATION = 0.9f; + private static final float STEER_RAY_DIST = 1.8f; + private static final float NPC_ATTACK_RANGE = 1.5f; + private static final float NPC_CLOSE_IN_DIST = 3.0f; + private static final float NPC_CLOSE_IN_SPEED = CharacterNavigator.WALK_SPEED_MPS; + + // ── NPC HP / Kampf ──────────────────────────────────────────────────────── + private static final int DEFAULT_NPC_MAX_HP = 100; + private static final int DEFAULT_NPC_MAX_MANA = 100; + private static final int DEFAULT_NPC_MAX_STAMINA = 100; + + // ── Welt-HP-Bar (Billboard über NPC) ───────────────────────────────────── + private static final float WB_WIDTH = 0.8f; + private static final float WB_HEIGHT = 0.08f; + private static final float WB_Y_OFFSET = 2.2f; // über Füßen + + // ── Feind-HUD (oben Screen-Space) ───────────────────────────────────────── + private static final float EHB_W = 200f; + private static final float EHB_H = 18f; + private static final float EHB_BORDER = 2f; + private static final float EHB_Y_FROM_TOP = 55f; + + // ── Melee-Hit-Tracking ──────────────────────────────────────────────────── + private int lastAttackId = -1; + private final Set hitThisAttack = new HashSet<>(); + + // ── Feind-HUD ───────────────────────────────────────────────────────────── + private Node npcEnemyHudNode; + private Geometry npcEnemyHpFill; + private float npcEhbX, npcEhbY; + private int focusedNpcIdx = -1; + // ── Konstruktor ─────────────────────────────────────────────────────────── public WorldNpcsState(KeyBindings keyBindings, CharacterControl physicsChar, @@ -176,12 +296,36 @@ public class WorldNpcsState extends BaseAppState { this.cam = app.getCamera(); this.rootNode = this.app.getRootNode(); this.npcsRoot = new Node("npcsRoot"); + this.guiNode = this.app.getGuiNode(); + + BitmapFont font = assets.loadFont("Interface/Fonts/Default.fnt"); + + speechCanvasNode = MenuCanvas.createCanvas(app.getCamera()); + speechCanvasNode.setCullHint(Spatial.CullHint.Always); + guiNode.attachChild(speechCanvasNode); + + speechPanelNode = new Node("speech-panel"); + speechPanelNode.attachChild(NinePatch.panel(assets).build(SP_X, SP_Y, SP_W, SP_H, -1f)); + + speechNameText = makeSpeechText(font, 16f, SP_COL_NAME); + speechNameText.setLocalTranslation(SP_X + SP_MARGIN, SP_TEXT_Y, 2f); + speechPanelNode.attachChild(speechNameText); + + speechText = makeSpeechText(font, 16f, SP_COL_TEXT); + speechText.setLocalTranslation(SP_X + SP_MARGIN, SP_TEXT_Y, 2f); + speechPanelNode.attachChild(speechText); + + speechCanvasNode.attachChild(speechPanelNode); try { assets.registerLocator( AnimationLibrary.findAssetRoot().toAbsolutePath().toString(), FileLocator.class); } catch (Exception ignored) {} + + bulletAppState = app.getStateManager().getState(BulletAppState.class); + playerInput.addMeleeAttackCallback(this); + buildNpcEnemyHud(); } @Override @@ -210,8 +354,11 @@ public class WorldNpcsState extends BaseAppState { spawned.clear(); npcsRoot.detachAllChildren(); npcsRoot.removeFromParent(); - hoveredIdx = -1; - dialogActive = false; + hoveredIdx = -1; + dialogActive = false; + speechSpeaker = null; + stopSpeechAudio(); + if (speechCanvasNode != null) { speechCanvasNode.setCullHint(Spatial.CullHint.Always); } try { app.getInputManager().removeListener(interactListener); } catch (Exception ignored) {} try { app.getInputManager().deleteMapping(INTERACT_ACTION); } catch (Exception ignored) {} } @@ -239,6 +386,10 @@ public class WorldNpcsState extends BaseAppState { } updateNpcRotation(tpf); updateNpcRevert(tpf); + updateAggroBehaviors(tpf); + updateNpcCombat(tpf); + updateNpcEnemyHud(); + updateSpeechBubble(tpf); if (dialogActive && dialogHeadTween != null) { dialogHeadTween.targetPos = physicsChar.getPhysicsLocation(); } @@ -255,15 +406,21 @@ public class WorldNpcsState extends BaseAppState { 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(); + if (pos == null || (dist > UNLOAD_RADIUS && !s.aggroBehavior.isAttacking() && !s.isReturning)) { + unloadNpc(s); toRemoveBuf.add(s); if (rotNpc == s) rotNpc = null; if (revertNpc == s) revertNpc = null; - } else { - // Position aktualisieren wenn sich die Stunde geändert hat - s.visual().setLocalTranslation(pos); - playNpcAction(s, activityActionFor(s.npc(), currentHour)); + } else if (!s.aggroBehavior.isAttacking()) { + if (s.isReturning) { + // Stunde ändert sich während Rückkehr – Ziel aktualisieren statt teleportieren + s.returnTarget = pos.clone(); + } else { + // Position aktualisieren wenn sich die Stunde geändert hat; + // im Kampf übernimmt updateNpcCombat die Positionierung. + s.visual().setLocalTranslation(pos); + playNpcAction(s, activityActionFor(s.npc(), currentHour)); + } } } spawned.removeAll(toRemoveBuf); @@ -296,6 +453,8 @@ public class WorldNpcsState extends BaseAppState { vis.setLocalTranslation(pos); npcsRoot.attachChild(vis); SpawnedNpc s = new SpawnedNpc(npc, vis, pos.x, pos.z); + attachNpcHpBar(s); + attachNpcCapsule(s); de.blight.game.scene.WorldScene.collectPbrMaterials(vis, s.pbrMaterials); if (dayNight != null && !s.pbrMaterials.isEmpty()) { com.jme3.math.ColorRGBA ac = dayNight.getCustomAmbient(); @@ -653,11 +812,32 @@ public class WorldNpcsState extends BaseAppState { return dialogNpc != null ? dialogNpc.visual() : null; } + /** Wird von WorldScene nach configureLocomotionSnap() aufgerufen, damit NPC-Clips neu mit Snap eingebaut werden. */ + public void reapplyAnimations() { + if (animLib == null) return; + for (SpawnedNpc s : spawned) { + if (s.visual() != null) animLib.applyAllTo(s.visual()); + } + } + public String getHoveredLabelKey() { if (hoveredIdx < 0 || hoveredIdx >= spawned.size()) return null; return spawned.get(hoveredIdx).npc().getLabelKey(); } + public ColorRGBA getHoveredLabelColor() { + if (hoveredIdx < 0 || hoveredIdx >= spawned.size()) return null; + de.blight.common.model.NPC npc = spawned.get(hoveredIdx).npc(); + de.blight.common.model.Status s = npc.getStatus(); + if (s == null) s = de.blight.common.model.Status.NEUTRAL; + return switch (s) { + case FRIENDLY -> new ColorRGBA(0.4f, 0.75f, 1.0f, 1f); + case ENRAGED -> new ColorRGBA(1.0f, 0.55f, 0.05f, 1f); + case ENEMY -> new ColorRGBA(1.0f, 0.2f, 0.2f, 1f); + default -> ColorRGBA.White.clone(); + }; + } + public Vector3f getHoveredWorldPos() { if (hoveredIdx < 0 || hoveredIdx >= spawned.size()) return null; Vector3f pos = spawned.get(hoveredIdx).visual().getLocalTranslation(); @@ -845,41 +1025,758 @@ public class WorldNpcsState extends BaseAppState { } } + /** Arm-Tween für NPC-Waffen-Ziehen/Wegstecken – 1:1 dieselbe Logik wie DrawArmTween im Helden. */ + private static final class NpcDrawArmTween implements Tween { + + private static final Quaternion ARM_PEAK_Q = quatFromDeg( 54.1f, 150.0f, -50.0f); + private static final Quaternion FOREARM_PEAK_Q = quatFromDeg( 23.0f, 30.1f, -35.1f); + private static final float[] L_ARM_DEG = { 0f, 20f, -80f }; + private static final float[] L_FOREARM_DEG = { -80f, 0f, 0f }; + + private final SpawnedNpc s; + private final boolean withShield; + + NpcDrawArmTween(SpawnedNpc s, boolean withShield) { + this.s = s; + this.withShield = withShield; + } + + @Override public double getLength() { return Double.MAX_VALUE; } + + @Override + public boolean interpolate(double t) { + if (!s.drawAnimActive) { return true; } + float progress = s.drawAnimTimer / DRAW_ANIM_DURATION; + if (progress <= 0f || progress >= 1f) { return true; } + + float alpha; + if (progress < 0.5f) { + float p = progress * 2f; + alpha = p * p * (3f - 2f * p); + } else { + float p = (1f - progress) * 2f; + alpha = p * p * (3f - 2f * p); + } + + if (s.jRightArmFbx != null && s.drawStartArmQ != null) { + Quaternion q = new Quaternion(); + q.slerp(s.drawStartArmQ, ARM_PEAK_Q, alpha); + s.jRightArmFbx.setLocalRotation(q); + } + if (s.jRightForeArmFbx != null && s.drawStartForeArmQ != null) { + Quaternion q = new Quaternion(); + q.slerp(s.drawStartForeArmQ, FOREARM_PEAK_Q, alpha); + s.jRightForeArmFbx.setLocalRotation(q); + } + if (withShield) { + applyDelta(s.jLeftArm, L_ARM_DEG, alpha); + applyDelta(s.jLeftForeArm, L_FOREARM_DEG, alpha); + } + return true; + } + + private static void applyDelta(Joint joint, float[] deg, float alpha) { + if (joint == null) { return; } + Quaternion delta = new Quaternion().fromAngles( + FastMath.DEG_TO_RAD * deg[0] * alpha, + FastMath.DEG_TO_RAD * deg[1] * alpha, + FastMath.DEG_TO_RAD * deg[2] * alpha + ); + joint.setLocalRotation(delta.mult(joint.getLocalRotation())); + } + + private static Quaternion quatFromDeg(float x, float y, float z) { + return new Quaternion().fromAngles( + FastMath.DEG_TO_RAD * x, FastMath.DEG_TO_RAD * y, FastMath.DEG_TO_RAD * 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); } + // ── NPC Aggro System ────────────────────────────────────────────────────── + + private void updateAggroBehaviors(float tpf) { + if (dialogActive) { return; } + Vector3f playerPos = physicsChar.getPhysicsLocation(); + float soundRadius = playerInput.getSoundRadius(); + for (SpawnedNpc s : spawned) { + Status status = s.npc().getStatus(); + if (status != Status.ENRAGED && status != Status.ENEMY) { continue; } + Vector3f npcPos = s.visual().getWorldTranslation(); + Vector3f npcForward = s.visual().getWorldRotation().mult(Vector3f.UNIT_Z); + Vector3f schedPos = resolveRoutinePosition(s.npc(), lastHour); + AggroEvent event = s.aggroBehavior.update(tpf, npcPos, npcForward, status, + playerPos, soundRadius, rootNode, schedPos); + handleAggroEvent(s, event); + } + } + + private void handleAggroEvent(SpawnedNpc s, AggroEvent event) { + switch (event) { + case WARN_1 -> showNpcSpeech(s, NpcAggroBehavior.WARN1_TEXTS, "warn1"); + case WARN_2 -> showNpcSpeech(s, NpcAggroBehavior.WARN2_TEXTS, "warn2"); + case ATTACK_START -> { + showNpcSpeech(s, NpcAggroBehavior.ATTACK_TEXTS, "attack"); + startNpcWeaponDraw(s); + } + case PURSUIT_BREAK -> { + log.debug("[WorldNpcs] '{}' bricht Verfolgung ab.", s.npc().getCharacterId()); + showNpcSpeech(s, NpcAggroBehavior.BREAK_TEXTS, "break"); + resetNpcCombat(s); + Vector3f returnPos = resolveRoutinePosition(s.npc(), lastHour); + if (returnPos != null) { + s.isReturning = true; + s.returnTarget = returnPos.clone(); + } + } + case SOUND_ALERT -> { + faceToward(s, physicsChar.getPhysicsLocation()); + if (s.isReturning) { s.returnFaceTimer = 2.0f; } + } + default -> {} + } + } + + /** + * Called by the combat system when the player lands a hit on an NPC. + * Bypasses sight/warning phases — the NPC retaliates immediately. + */ + public void notifyNpcAttackedByPlayer(String npcId) { + for (SpawnedNpc s : spawned) { + if (!npcId.equals(s.npc().getCharacterId())) { continue; } + Status status = s.npc().getStatus(); + if (status != Status.ENRAGED && status != Status.ENEMY) { return; } + Vector3f npcPos = s.visual().getWorldTranslation(); + Vector3f schedPos = resolveRoutinePosition(s.npc(), lastHour); + AggroEvent event = s.aggroBehavior.onAttackedByPlayer(npcPos, schedPos); + handleAggroEvent(s, event); + return; + } + } + + private void showNpcSpeech(SpawnedNpc s, TextReference[] textPool, String audioCategory) { + int idx = s.aggroBehavior.randomPhraseIdx(); + String text = TextResolver.get().resolve(textPool[idx]); + String npcName = TextResolver.get().resolveId(s.npc().getLabelKey()) + ": "; + speechNameText.setText(npcName); + float nameW = speechNameText.getLineWidth() * 0.5f; + speechText.setText(text); + speechText.setLocalTranslation(SP_X + SP_MARGIN + nameW, SP_TEXT_Y, 2f); + speechSpeaker = s; + speechTimer = SPEECH_DURATION; + + stopSpeechAudio(); + String path = NpcAggroBehavior.audioPath(s.npc().getCharacterId(), audioCategory, idx); + try { + speechAudioNode = new AudioNode(assets, path, AudioData.DataType.Buffer); + speechAudioNode.setPositional(true); + speechAudioNode.setLocalTranslation(s.visual().getWorldTranslation().add(0f, 1.6f, 0f)); + speechAudioNode.setRefDistance(12f); + speechAudioNode.setMaxDistance(50f); + speechAudioNode.setLooping(false); + rootNode.attachChild(speechAudioNode); + speechAudioNode.play(); + } catch (Exception ignored) { + // Audio not yet recorded — text still shows + } + } + + private void stopSpeechAudio() { + if (speechAudioNode != null) { + speechAudioNode.stop(); + speechAudioNode.removeFromParent(); + speechAudioNode = null; + } + } + + private void updateSpeechBubble(float tpf) { + if (speechSpeaker == null) { + speechCanvasNode.setCullHint(Spatial.CullHint.Always); + return; + } + speechTimer -= tpf; + if (speechTimer <= 0f) { + speechSpeaker = null; + stopSpeechAudio(); + speechCanvasNode.setCullHint(Spatial.CullHint.Always); + return; + } + speechCanvasNode.setCullHint(Spatial.CullHint.Inherit); + } + + private BitmapText makeSpeechText(BitmapFont font, float size, ColorRGBA color) { + BitmapText t = new BitmapText(font); + t.setSize(size * 2f); + t.setLocalScale(0.5f, 0.5f, 1f); + t.setColor(color); + return t; + } + + private Spatial buildVisual(NPC npc) { + // Wrapper mit Scale (1,1,1) als Ankerpunkt – das geladene Modell hat ggf. eingebettete + // Skalierung (z.B. 0.01 für Blender-cm-Export). WeaponAttachmentControl rechnet in + // Welteinheiten, daher muss der Spatial, an dem die Waffe hängt, Scale=1 haben. + Node wrapper = new Node("npc_" + npc.getCharacterId()); String modelPath = npc.getModelPath(); if (modelPath != null && !modelPath.isBlank()) { try { Spatial model = assets.loadModel(modelPath); - model.setName("npc_" + npc.getCharacterId()); + model.setName("npc_" + npc.getCharacterId() + "_mesh"); de.blight.game.scene.WorldScene.applyFullAmbient(model); if (animLib != null) { animLib.applyAllTo(model); } - return model; + wrapper.attachChild(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; + if (wrapper.getChildren().isEmpty()) { + 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); + wrapper.attachChild(body); + wrapper.attachChild(head); + } + attachNpcWeapons(wrapper, npc); + return wrapper; + } + + private void attachNpcWeapons(Spatial visual, NPC npc) { + Item weapon = findItemById(npc.getEquippedWeaponId()); + Item shield = findItemById(npc.getEquippedShieldId()); + log.info("[WorldNpcs] NPC '{}': weapon={} shield={}", npc.getCharacterId(), + weapon != null ? weapon.getItemId() : "null", + shield != null ? shield.getItemId() : "null"); + if (weapon == null && shield == null) { return; } + visual.addControl(new NpcWeaponAttachmentControl(assets, weapon, shield)); + } + + private Item findItemById(String itemId) { + if (itemId == null || itemId.isBlank()) { return null; } + try { + java.nio.file.Path itemsDir = AnimationLibrary.findAssetRoot().resolve("items"); + try (java.util.stream.Stream walk = java.nio.file.Files.walk(itemsDir)) { + java.nio.file.Path found = walk + .filter(p -> p.getFileName().toString().equals(itemId + ".item")) + .findFirst().orElse(null); + if (found == null) { + log.warn("[WorldNpcs] Item-Datei nicht gefunden: {}.item in {}", itemId, itemsDir); + } + return found != null ? ItemIO.load(found) : null; + } + } catch (Exception e) { + log.warn("[WorldNpcs] Item '{}' nicht geladen: {}", itemId, e.getMessage()); + return null; + } + } + + // ── NPC Kampf-Verfolgung ────────────────────────────────────────────────── + + private void initDrawJoints(SpawnedNpc s) { + if (s.drawJointsInit) { return; } + s.drawJointsInit = true; + SkinningControl sc = de.blight.game.animation.RetargetingSystem.findSkinningControl(s.visual()); + if (sc == null) { return; } + Armature arm = sc.getArmature(); + s.jRightArmFbx = arm.getJoint("mixamorig:RightArm_$AssimpFbx$_Rotation"); + s.jRightForeArmFbx = arm.getJoint("mixamorig:RightForeArm_$AssimpFbx$_Rotation"); + s.jLeftArm = arm.getJoint("mixamorig:LeftArm"); + s.jLeftForeArm = arm.getJoint("mixamorig:LeftForeArm"); + log.debug("[NpcDrawAnim] Joints '{}': rightArmFbx={} foreArmFbx={} leftArm={} leftForeArm={}", + s.npc().getCharacterId(), + s.jRightArmFbx != null, s.jRightForeArmFbx != null, + s.jLeftArm != null, s.jLeftForeArm != null); + } + + private void startDrawLayerOn(SpawnedNpc s, boolean withShield) { + AnimComposer ac = de.blight.game.animation.RetargetingSystem.findAnimComposer(s.visual()); + SkinningControl sc = de.blight.game.animation.RetargetingSystem.findSkinningControl(s.visual()); + if (ac == null || sc == null) { s.drawAnimActive = false; return; } + Armature arm = sc.getArmature(); + // Alten Layer defensiv entfernen – verhindert "layer already exists"-Exception + try { ac.removeLayer(NPC_DRAW_LAYER); } catch (Exception ignored) {} + try { + ArmatureMask mask = ArmatureMask.createMask(arm, "mixamorig:RightArm"); + if (withShield && s.jLeftArm != null) { + mask.addFromJoint(arm, "mixamorig:LeftArm"); + } + NpcDrawArmTween tween = new NpcDrawArmTween(s, withShield); + BaseAction action = new BaseAction(tween); + ac.addAction(NPC_DRAW_LAYER, action); + ac.makeLayer(NPC_DRAW_LAYER, mask); + ac.setCurrentAction(NPC_DRAW_LAYER, NPC_DRAW_LAYER); + } catch (Exception e) { + log.warn("[NpcDrawAnim] Layer-Setup fehlgeschlagen für '{}': {}", s.npc().getCharacterId(), e.getMessage()); + s.drawAnimActive = false; + } + } + + private void removeDrawLayer(SpawnedNpc s) { + AnimComposer ac = de.blight.game.animation.RetargetingSystem.findAnimComposer(s.visual()); + if (ac == null) { return; } + try { ac.removeLayer(NPC_DRAW_LAYER); } catch (Exception ignored) {} + } + + private void startNpcWeaponDraw(SpawnedNpc s) { + s.isReturning = false; // Rückkehr abbrechen – NPC kämpft wieder + s.returnTarget = null; + s.drawAnimTimer = 0f; + s.drawMidFired = false; + s.isSheathing = false; + s.drawAnimActive = true; + s.combatRunAnim = resolveCombatRunAnimation(s.npc()); + initDrawJoints(s); + s.drawStartArmQ = s.jRightArmFbx != null ? s.jRightArmFbx.getLocalRotation().clone() : new Quaternion(); + s.drawStartForeArmQ = s.jRightForeArmFbx != null ? s.jRightForeArmFbx.getLocalRotation().clone() : new Quaternion(); + boolean hasShield = s.npc().getEquippedShieldId() != null && !s.npc().getEquippedShieldId().isBlank(); + startDrawLayerOn(s, hasShield); + playNpcAction(s, AnimationAction.IDLE); + } + + private void resetNpcCombat(SpawnedNpc s) { + if (s.drawAnimActive) { + s.drawAnimActive = false; + removeDrawLayer(s); + } + s.drawAnimTimer = 0f; + s.drawMidFired = false; + if (s.weaponDrawn) { + startNpcWeaponSheathe(s); + } else { + de.blight.game.control.NpcWeaponAttachmentControl wac = + s.visual().getControl(de.blight.game.control.NpcWeaponAttachmentControl.class); + if (wac != null) { wac.setDrawn(false); } + s.combatRunAnim = null; + playNpcAction(s, AnimationAction.IDLE); + } + } + + private void startNpcWeaponSheathe(SpawnedNpc s) { + s.drawAnimTimer = 0f; + s.drawMidFired = false; + s.isSheathing = true; + s.drawAnimActive = true; + initDrawJoints(s); + s.drawStartArmQ = s.jRightArmFbx != null ? s.jRightArmFbx.getLocalRotation().clone() : new Quaternion(); + s.drawStartForeArmQ = s.jRightForeArmFbx != null ? s.jRightForeArmFbx.getLocalRotation().clone() : new Quaternion(); + boolean hasShield = s.npc().getEquippedShieldId() != null && !s.npc().getEquippedShieldId().isBlank(); + startDrawLayerOn(s, hasShield); + playNpcAction(s, AnimationAction.IDLE); + } + + private AnimationAction resolveSheatheAnimation(NPC npc) { + Item w = findItemById(npc.getEquippedWeaponId()); + if (w == null) { return AnimationAction.IDLE; } + de.blight.common.model.ItemSubCategory sub = w.getSubCategory(); + if (sub == null) { return AnimationAction.IDLE; } + return switch (sub) { + case SWORD -> AnimationAction.SWORD_SHEATHE; + case AXE -> AnimationAction.AXE_SHEATHE; + case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_SHEATHE; + case BOW -> AnimationAction.BOW_SHEATHE; + case CROSSBOW -> AnimationAction.CROSSBOW_SHEATHE; + case MAGIC_CRYSTAL -> AnimationAction.CRYSTAL_SHEATHE; + default -> AnimationAction.IDLE; + }; + } + + private AnimationAction resolveCombatRunAnimation(NPC npc) { + Item w = findItemById(npc.getEquippedWeaponId()); + if (w == null) { return AnimationAction.RUN; } + de.blight.common.model.ItemSubCategory sub = w.getSubCategory(); + if (sub == null) { return AnimationAction.RUN; } + return switch (sub) { + case SWORD -> AnimationAction.SWORD_RUN; + case AXE -> AnimationAction.AXE_RUN; + case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_RUN; + case BOW -> AnimationAction.BOW_RUN; + case CROSSBOW -> AnimationAction.CROSSBOW_RUN; + case MAGIC_CRYSTAL -> AnimationAction.CRYSTAL_RUN; + default -> AnimationAction.RUN; + }; + } + + private void updateNpcCombat(float tpf) { + if (dialogActive) { return; } + Vector3f playerPos = physicsChar.getPhysicsLocation(); + for (SpawnedNpc s : spawned) { + + // Zieh/Wegsteck-Animation – läuft unabhängig vom Aggro-Zustand (auch nach PURSUIT_BREAK) + if (s.drawAnimActive) { + s.drawAnimTimer += tpf; + de.blight.game.control.NpcWeaponAttachmentControl wac = + s.visual().getControl(de.blight.game.control.NpcWeaponAttachmentControl.class); + + // Midpoint (50%): wie beim Helden – Waffe wechselt bei halber Animation + if (!s.drawMidFired && s.drawAnimTimer >= DRAW_ANIM_DURATION * 0.5f) { + s.drawMidFired = true; + if (s.isSheathing) { + if (wac != null) { wac.setDrawn(false); } + s.weaponDrawn = false; + } else { + if (wac != null) { wac.setDrawn(true); } + s.weaponDrawn = true; + } + } + + if (s.drawAnimTimer >= DRAW_ANIM_DURATION) { + s.drawAnimActive = false; + removeDrawLayer(s); + if (s.isSheathing) { + s.isSheathing = false; + s.combatRunAnim = null; + playNpcAction(s, AnimationAction.IDLE); + } else { + if (s.combatRunAnim != null) { playNpcAction(s, s.combatRunAnim); } + } + } + continue; + } + + // Rückkehr zur Tagesroutine nach Verfolgungsabbruch + if (s.isReturning && s.returnTarget != null) { + Vector3f npcPos = s.visual().getWorldTranslation(); + if (dist2d(npcPos, s.returnTarget) <= 1.5f) { + float y = terrainChunks != null ? terrainChunks.getHeightAt(s.returnTarget.x, s.returnTarget.z) : s.returnTarget.y; + s.visual().setLocalTranslation(s.returnTarget.x, y, s.returnTarget.z); + s.isReturning = false; + s.returnTarget = null; + playNpcAction(s, AnimationAction.IDLE); + } else { + // Richtung mit Hindernisumgehung; Fallback: direkte Linie wenn kein Weg gefunden + Vector3f moveDir = steerTowardPlayer(npcPos, s.returnTarget); + if (moveDir == null) { + moveDir = new Vector3f(s.returnTarget.x - npcPos.x, 0f, s.returnTarget.z - npcPos.z); + float len = moveDir.length(); + if (len > 0.001f) { moveDir.divideLocal(len); } else { moveDir = null; } + } + if (moveDir != null) { + float dx = moveDir.x * de.blight.game.navigation.CharacterNavigator.WALK_SPEED_MPS * tpf; + float dz = moveDir.z * de.blight.game.navigation.CharacterNavigator.WALK_SPEED_MPS * tpf; + float newX = npcPos.x + dx; + float newZ = npcPos.z + dz; + float newY = terrainChunks != null ? terrainChunks.getHeightAt(newX, newZ) : npcPos.y; + s.visual().setLocalTranslation(newX, newY, newZ); + // Blickrichtung: nach Geräusch-Alert kurz zum Spieler, sonst Laufrichtung + if (s.returnFaceTimer > 0f) { + s.returnFaceTimer -= tpf; + faceToward(s, playerPos); + } else { + faceToward(s, new Vector3f(npcPos.x + moveDir.x, npcPos.y, npcPos.z + moveDir.z)); + } + playNpcAction(s, AnimationAction.WALK); + } + } + continue; + } + + if (!s.aggroBehavior.isAttacking()) { continue; } + + Vector3f npcPos = s.visual().getWorldTranslation(); + float dist = dist2d(npcPos, playerPos); + + if (dist <= NPC_ATTACK_RANGE) { + // Kampfdistanz erreicht: stehen, drehen, combat-idle + faceToward(s, playerPos); + if (s.weaponDrawn) { playNpcAction(s, AnimationAction.IDLE); } + continue; + } + + // Close-in: Spieler ≤ 3m und flieht nicht → NPC geht langsam ran + if (dist <= NPC_CLOSE_IN_DIST && !playerInput.isRunningOrSprinting()) { + Vector3f moveDir = steerTowardPlayer(npcPos, playerPos); + if (moveDir != null) { + float dx = moveDir.x * NPC_CLOSE_IN_SPEED * tpf; + float dz = moveDir.z * NPC_CLOSE_IN_SPEED * tpf; + float newX = npcPos.x + dx; + float newZ = npcPos.z + dz; + float newY = terrainChunks != null ? terrainChunks.getHeightAt(newX, newZ) : npcPos.y; + s.visual().setLocalTranslation(newX, newY, newZ); + faceToward(s, new Vector3f(npcPos.x + moveDir.x, npcPos.y, npcPos.z + moveDir.z)); + if (s.weaponDrawn) { playNpcAction(s, AnimationAction.WALK); } + } else { + faceToward(s, playerPos); + if (s.weaponDrawn) { playNpcAction(s, AnimationAction.IDLE); } + } + continue; + } + + // Verfolgung: NPC rennt zum Spieler + Vector3f moveDir = steerTowardPlayer(npcPos, playerPos); + if (moveDir == null) { continue; } + + float dx = moveDir.x * NPC_MOVE_SPEED * tpf; + float dz = moveDir.z * NPC_MOVE_SPEED * tpf; + float newX = npcPos.x + dx; + float newZ = npcPos.z + dz; + float newY = terrainChunks != null ? terrainChunks.getHeightAt(newX, newZ) : npcPos.y; + s.visual().setLocalTranslation(newX, newY, newZ); + faceToward(s, new Vector3f(npcPos.x + moveDir.x, npcPos.y, npcPos.z + moveDir.z)); + + if (s.weaponDrawn && s.combatRunAnim != null) { + playNpcAction(s, s.combatRunAnim); + } + } + } + + private void faceToward(SpawnedNpc s, Vector3f target) { + Vector3f npcPos = s.visual().getWorldTranslation(); + float dx = target.x - npcPos.x; + float dz = target.z - npcPos.z; + if (dx * dx + dz * dz < 0.001f) { return; } + float angle = FastMath.atan2(dx, dz); + s.visual().setLocalRotation(new Quaternion().fromAngleAxis(angle, Vector3f.UNIT_Y)); + } + + private Vector3f steerTowardPlayer(Vector3f npcPos, Vector3f playerPos) { + Vector3f desired = new Vector3f(playerPos.x - npcPos.x, 0f, playerPos.z - npcPos.z); + if (desired.lengthSquared() < 0.001f) { return null; } + desired.normalizeLocal(); + + Vector3f rayOrigin = new Vector3f(npcPos.x, npcPos.y + 1.0f, npcPos.z); + + float step = FastMath.DEG_TO_RAD * 20f; + for (int i = 0; i <= 8; i++) { + float offset = i * step; + if (i == 0) { + if (isPathClear(rayOrigin, desired)) { return desired; } + } else { + Vector3f left = rotateY(desired, offset); + Vector3f right = rotateY(desired, -offset); + if (isPathClear(rayOrigin, left)) { return left; } + if (isPathClear(rayOrigin, right)) { return right; } + } + } + return desired; + } + + private static Vector3f rotateY(Vector3f v, float angle) { + float cos = FastMath.cos(angle); + float sin = FastMath.sin(angle); + return new Vector3f(cos * v.x + sin * v.z, 0f, -sin * v.x + cos * v.z); + } + + private boolean isPathClear(Vector3f origin, Vector3f dir) { + CollisionResults cr = new CollisionResults(); + rootNode.collideWith(new Ray(origin, dir), cr); + if (cr.size() == 0) { return true; } + float hitDist = cr.getClosestCollision().getDistance(); + if (hitDist < 0.5f) { return true; } // NPC Eigen-Geometrie + return hitDist >= STEER_RAY_DIST; + } + + // ── NPC HP-Balken ───────────────────────────────────────────────────────── + + private void attachNpcHpBar(SpawnedNpc s) { + Node hpBarNode = buildWorldHpBar(); + hpBarNode.setLocalTranslation(0f, WB_Y_OFFSET, 0f); + hpBarNode.setCullHint(Spatial.CullHint.Always); + ((Node) s.visual()).attachChild(hpBarNode); + s.hpBarNode = hpBarNode; + s.hpBarFg = (Geometry) ((Node) hpBarNode.getChild(0)).getChild(1); + } + + private Node buildWorldHpBar() { + Node hpBarNode = new Node("npcHpBar"); + Node billChild = new Node("npcHpBillboard"); + billChild.addControl(new BillboardControl()); + + Geometry bg = new Geometry("npcHpBg", new Quad(WB_WIDTH, WB_HEIGHT)); + bg.setLocalTranslation(-WB_WIDTH * 0.5f, -WB_HEIGHT * 0.5f, 0f); + Material bgMat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md"); + bgMat.setColor("Color", new ColorRGBA(0.15f, 0.15f, 0.15f, 0.85f)); + bgMat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha); + bg.setMaterial(bgMat); + bg.setQueueBucket(RenderQueue.Bucket.Transparent); + billChild.attachChild(bg); + + Geometry fg = new Geometry("npcHpFg", new Quad(WB_WIDTH, WB_HEIGHT - 0.01f)); + fg.setLocalTranslation(-WB_WIDTH * 0.5f, -WB_HEIGHT * 0.5f + 0.005f, 0.001f); + Material fgMat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md"); + fgMat.setColor("Color", new ColorRGBA(0.2f, 0.8f, 0.2f, 1f)); + fg.setMaterial(fgMat); + fg.setQueueBucket(RenderQueue.Bucket.Transparent); + billChild.attachChild(fg); + + hpBarNode.attachChild(billChild); + return hpBarNode; + } + + private void refreshNpcHpBar(SpawnedNpc s) { + if (s.hpBarFg == null) { return; } + float frac = (float) s.currentHp / s.maxHp; + frac = Math.max(0f, frac); + ColorRGBA col; + if (frac > 0.5f) col = new ColorRGBA(0.2f, 0.8f, 0.2f, 1f); + else if (frac > 0.2f) col = new ColorRGBA(0.9f, 0.8f, 0.1f, 1f); + else col = new ColorRGBA(0.9f, 0.1f, 0.1f, 1f); + s.hpBarFg.getMaterial().setColor("Color", col); + s.hpBarFg.setLocalScale(Math.max(0.001f, frac), 1f, 1f); + if (s.hpBarNode != null) { s.hpBarNode.setCullHint(Spatial.CullHint.Inherit); } + } + + // ── NPC Physik-Kapsel ───────────────────────────────────────────────────── + + private void attachNpcCapsule(SpawnedNpc s) { + if (bulletAppState == null) { return; } + try { + CapsuleCollisionShape shape = new CapsuleCollisionShape(0.3f, 1.0f, 1); + RigidBodyControl rbc = new RigidBodyControl(shape, 1f); + rbc.setKinematic(true); + ((Node) s.visual()).addControl(rbc); + bulletAppState.getPhysicsSpace().add(rbc); + s.rbc = rbc; + } catch (Exception e) { + log.warn("[WorldNpcs] Kapsel-Shape für '{}' fehlgeschlagen: {}", s.npc().getCharacterId(), e.getMessage()); + } + } + + private void unloadNpc(SpawnedNpc s) { + if (s.rbc != null && bulletAppState != null) { + bulletAppState.getPhysicsSpace().remove(s.rbc); + s.rbc = null; + } + s.visual().removeFromParent(); + } + + // ── MeleeAttackCallback ─────────────────────────────────────────────────── + + @Override + public void onMeleeAttack(int attackId, Vector3f gripPos, Vector3f bladeDir, + float bladeLength, int baseDamage, float critMultiplier) { + if (attackId != lastAttackId) { + lastAttackId = attackId; + hitThisAttack.clear(); + } + for (SpawnedNpc s : spawned) { + if (hitThisAttack.contains(s.npc().getCharacterId())) { continue; } + Vector3f npcPos = s.visual().getWorldTranslation(); + float dx = npcPos.x - gripPos.x; + float dy = (npcPos.y + 0.9f) - gripPos.y; + float dz = npcPos.z - gripPos.z; + if (dx * dx + dy * dy + dz * dz <= bladeLength * bladeLength) { + hitThisAttack.add(s.npc().getCharacterId()); + applyNpcDamage(s, DamageCalculator.calculate(baseDamage, critMultiplier), critMultiplier > 1.0f); + } + } + } + + private void applyNpcDamage(SpawnedNpc s, int damage, boolean isCrit) { + s.currentHp = Math.max(0, s.currentHp - damage); + refreshNpcHpBar(s); + log.info("[NPC {}] -{} HP → {}/{}{}", s.npc().getCharacterId(), damage, + s.currentHp, s.maxHp, s.currentHp <= 0 ? " K.O.!" : ""); + } + + // ── Feind-HUD ───────────────────────────────────────────────────────────── + + private void buildNpcEnemyHud() { + float sw = cam.getWidth(); + float sh = cam.getHeight(); + npcEhbX = sw * 0.5f - (EHB_W + EHB_BORDER * 2) * 0.5f; + npcEhbY = sh - EHB_Y_FROM_TOP - (EHB_H + EHB_BORDER * 2); + + npcEnemyHudNode = new Node("npcEnemyHud"); + + Geometry bg = solidRect("nehpBg", + EHB_W + EHB_BORDER * 2, EHB_H + EHB_BORDER * 2, + new ColorRGBA(0.08f, 0.08f, 0.08f, 0.85f)); + bg.setLocalTranslation(npcEhbX, npcEhbY, 1f); + bg.setQueueBucket(RenderQueue.Bucket.Gui); + npcEnemyHudNode.attachChild(bg); + + com.jme3.scene.Mesh fillMesh = new com.jme3.scene.Mesh(); + buildNpcFillMesh(fillMesh, EHB_W, 1f); + Material fillMat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md"); + fillMat.setColor("Color", new ColorRGBA(0.85f, 0.10f, 0.10f, 1f)); + fillMat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha); + npcEnemyHpFill = new Geometry("nehpFill", fillMesh); + npcEnemyHpFill.setMaterial(fillMat); + npcEnemyHpFill.setQueueBucket(RenderQueue.Bucket.Gui); + npcEnemyHpFill.setLocalTranslation(npcEhbX + EHB_BORDER, npcEhbY + EHB_BORDER, 2f); + npcEnemyHudNode.attachChild(npcEnemyHpFill); + + guiNode.attachChild(npcEnemyHudNode); + npcEnemyHudNode.setCullHint(Spatial.CullHint.Always); + } + + private Geometry solidRect(String name, float w, float h, ColorRGBA color) { + Geometry g = new Geometry(name, new Quad(w, h)); + Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md"); + mat.setColor("Color", color); + mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha); + g.setMaterial(mat); + return g; + } + + private static void buildNpcFillMesh(com.jme3.scene.Mesh mesh, float w, float maxU) { + mesh.setBuffer(com.jme3.scene.VertexBuffer.Type.Position, 3, + BufferUtils.createFloatBuffer(0, 0, 0, w, 0, 0, w, EHB_H, 0, 0, EHB_H, 0)); + mesh.setBuffer(com.jme3.scene.VertexBuffer.Type.TexCoord, 2, + BufferUtils.createFloatBuffer(0f, 0f, maxU, 0f, maxU, 1f, 0f, 1f)); + mesh.setBuffer(com.jme3.scene.VertexBuffer.Type.Index, 3, + BufferUtils.createShortBuffer((short) 0, (short) 1, (short) 2, + (short) 0, (short) 2, (short) 3)); + mesh.setMode(com.jme3.scene.Mesh.Mode.Triangles); + mesh.updateBound(); + } + + private void updateNpcEnemyHud() { + if (npcEnemyHudNode == null) { return; } + boolean weaponOut = playerInput.isWeaponDrawn(); + if (!weaponOut) { + npcEnemyHudNode.setCullHint(Spatial.CullHint.Always); + focusedNpcIdx = -1; + return; + } + Vector3f playerPos = physicsChar.getPhysicsLocation(); + focusedNpcIdx = -1; + float bestDistSq = 6f * 6f; + for (int i = 0; i < spawned.size(); i++) { + SpawnedNpc s = spawned.get(i); + Vector3f npcPos = s.visual().getWorldTranslation(); + float dx = npcPos.x - playerPos.x; + float dz = npcPos.z - playerPos.z; + float distSq = dx * dx + dz * dz; + if (distSq < bestDistSq) { + bestDistSq = distSq; + focusedNpcIdx = i; + } + } + if (focusedNpcIdx >= 0) { + SpawnedNpc s = spawned.get(focusedNpcIdx); + float ratio = s.maxHp > 0 ? (float) s.currentHp / s.maxHp : 0f; + applyNpcEnemyHpRatio(ratio); + npcEnemyHudNode.setCullHint(Spatial.CullHint.Inherit); + } else { + npcEnemyHudNode.setCullHint(Spatial.CullHint.Always); + } + } + + private void applyNpcEnemyHpRatio(float ratio) { + ratio = Math.max(0f, Math.min(1f, ratio)); + float w = EHB_W * ratio; + com.jme3.scene.Mesh mesh = npcEnemyHpFill.getMesh(); + FloatBuffer pos = (FloatBuffer) mesh.getBuffer(com.jme3.scene.VertexBuffer.Type.Position).getData(); + pos.put(3, w); pos.put(6, w); + mesh.getBuffer(com.jme3.scene.VertexBuffer.Type.Position).setUpdateNeeded(); + FloatBuffer uv = (FloatBuffer) mesh.getBuffer(com.jme3.scene.VertexBuffer.Type.TexCoord).getData(); + uv.put(2, ratio); uv.put(4, ratio); + mesh.getBuffer(com.jme3.scene.VertexBuffer.Type.TexCoord).setUpdateNeeded(); + mesh.updateBound(); } } diff --git a/blight-lang/src/main/resources/lang/messages_de.properties b/blight-lang/src/main/resources/lang/messages_de.properties index d5c54c1..10c3aae 100644 --- a/blight-lang/src/main/resources/lang/messages_de.properties +++ b/blight-lang/src/main/resources/lang/messages_de.properties @@ -249,3 +249,33 @@ einfacher_schild.name=Einfacher Schild rostiges_kurzschwert.name=Rostiges Kurzschwert blutagave.name=Blutagave erzmoss.name=Erzmoss + +# NPC Aggro – erste Warnung (ENRAGED, erster Sichtkontakt) +npc.aggro.warn1.1=Verschwinde – das ist deine letzte freundliche Warnung! +npc.aggro.warn1.2=Noch einen Schritt und du bereust es! +npc.aggro.warn1.3=Geh weg, solange du noch kannst! +npc.aggro.warn1.4=Ich warne dich: Komm mir nicht zu nah! +npc.aggro.warn1.5=Geh jetzt, oder du wirst es bereuen! + +# NPC Aggro – zweite Warnung (ENRAGED, nach 10 s) +npc.aggro.warn2.1=Ich meine es ernst! Das ist deine letzte Chance! +npc.aggro.warn2.2=Immer noch da? Das war deine letzte Warnung! +npc.aggro.warn2.3=Du spielst mit dem Feuer – verschwinde jetzt! +npc.aggro.warn2.4=Noch einmal: Geh weg! Das nächste Mal greife ich an! +npc.aggro.warn2.5=Du testest meine Geduld. Geh – jetzt sofort! + +# NPC Aggro – Angriff (Übergang zu ATTACKING) +npc.aggro.attack.1=Genug geredet! Jetzt stirbst du! +npc.aggro.attack.2=Du hättest besser gehen sollen! +npc.aggro.attack.3=Das hast du dir selbst eingebrockt! +npc.aggro.attack.4=Auf dich habe ich lange genug gewartet! +npc.aggro.attack.5=Dann eben auf die harte Tour! +npc.aggro.break.1=Irgendwann krieg ich dich noch! +npc.aggro.break.2=Das war dein Glückstag. Aber ich vergesse das nicht! +npc.aggro.break.3=Lauf nur! Du kannst nicht für immer fliehen! +npc.aggro.break.4=Beim nächsten Mal kommst du nicht so leicht davon! +npc.aggro.break.5=Wir sehen uns wieder – darauf kannst du dich verlassen! + +npc.bandit1.name=Bandit +npc.bjoernson.name=Bjoernson +npc.silas.name=Silas diff --git a/blight-lang/src/main/resources/lang/messages_en.properties b/blight-lang/src/main/resources/lang/messages_en.properties index 742447b..39e2c57 100644 --- a/blight-lang/src/main/resources/lang/messages_en.properties +++ b/blight-lang/src/main/resources/lang/messages_en.properties @@ -104,3 +104,33 @@ einfacher_schild.name=Simple Shield rostiges_kurzschwert.name=Rusty Short Sword blutagave.name=Blood Agave erzmoss.name=Ore Moss + +# NPC Aggro – first warning +npc.aggro.warn1.1=Leave – that is your first and last friendly warning! +npc.aggro.warn1.2=Take one more step and you'll regret it! +npc.aggro.warn1.3=Get out while you still can! +npc.aggro.warn1.4=I'm warning you: don't get too close to me! +npc.aggro.warn1.5=You'd better leave now, or you'll be sorry! + +# NPC Aggro – second warning +npc.aggro.warn2.1=I mean it! This is your last chance! +npc.aggro.warn2.2=Still here? That was your final warning! +npc.aggro.warn2.3=You're playing with fire – get out now! +npc.aggro.warn2.4=Once more: leave! Next time I will attack! +npc.aggro.warn2.5=You are testing my patience. Go – right now! + +# NPC Aggro – attack start +npc.aggro.attack.1=Enough talk! Now you die! +npc.aggro.attack.2=You should have left when you had the chance! +npc.aggro.attack.3=You brought this on yourself! +npc.aggro.attack.4=I have been patient long enough! +npc.aggro.attack.5=Then we'll do it the hard way! +npc.aggro.break.1=I'll get you eventually! +npc.aggro.break.2=Lucky day for you. But I won't forget this! +npc.aggro.break.3=Run! You can't hide forever! +npc.aggro.break.4=Next time you won't get off so easily! +npc.aggro.break.5=We'll meet again – count on it! + +npc.bandit1.name=Bandit +npc.bjoernson.name=Bjoernson +npc.silas.name=Silas diff --git a/doc/Banditen Plot.odt b/doc/Banditen Plot.odt new file mode 100644 index 0000000..423ea05 Binary files /dev/null and b/doc/Banditen Plot.odt differ diff --git a/doc/Das Protektorat/Personen.odt b/doc/Das Protektorat/Personen.odt new file mode 100644 index 0000000..62cbd1e Binary files /dev/null and b/doc/Das Protektorat/Personen.odt differ diff --git a/doc/Freibeuter.odt b/doc/Freibeuter.odt index 06b36cf..9c0f824 100644 Binary files a/doc/Freibeuter.odt and b/doc/Freibeuter.odt differ diff --git a/doc/obsidian vault/Blight/.obsidian/app.json b/doc/obsidian vault/Blight/.obsidian/app.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/doc/obsidian vault/Blight/.obsidian/app.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/doc/obsidian vault/Blight/.obsidian/appearance.json b/doc/obsidian vault/Blight/.obsidian/appearance.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/doc/obsidian vault/Blight/.obsidian/appearance.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/doc/obsidian vault/Blight/.obsidian/core-plugins.json b/doc/obsidian vault/Blight/.obsidian/core-plugins.json new file mode 100644 index 0000000..639b90d --- /dev/null +++ b/doc/obsidian vault/Blight/.obsidian/core-plugins.json @@ -0,0 +1,33 @@ +{ + "file-explorer": true, + "global-search": true, + "switcher": true, + "graph": true, + "backlink": true, + "canvas": true, + "outgoing-link": true, + "tag-pane": true, + "footnotes": false, + "properties": true, + "page-preview": true, + "daily-notes": true, + "templates": true, + "note-composer": true, + "command-palette": true, + "slash-command": false, + "editor-status": true, + "bookmarks": true, + "markdown-importer": false, + "zk-prefixer": false, + "random-note": false, + "outline": true, + "word-count": true, + "slides": false, + "audio-recorder": false, + "workspaces": false, + "file-recovery": true, + "publish": false, + "sync": true, + "bases": true, + "webviewer": false +} \ No newline at end of file diff --git a/doc/obsidian vault/Blight/.obsidian/graph.json b/doc/obsidian vault/Blight/.obsidian/graph.json new file mode 100644 index 0000000..42a46ec --- /dev/null +++ b/doc/obsidian vault/Blight/.obsidian/graph.json @@ -0,0 +1,22 @@ +{ + "collapse-filter": true, + "search": "", + "showTags": false, + "showAttachments": false, + "hideUnresolved": false, + "showOrphans": true, + "collapse-color-groups": true, + "colorGroups": [], + "collapse-display": true, + "showArrow": false, + "textFadeMultiplier": 0, + "nodeSizeMultiplier": 1, + "lineSizeMultiplier": 1, + "collapse-forces": true, + "centerStrength": 0.518713248970312, + "repelStrength": 10, + "linkStrength": 1, + "linkDistance": 250, + "scale": 1, + "close": true +} \ No newline at end of file diff --git a/doc/obsidian vault/Blight/.obsidian/workspace.json b/doc/obsidian vault/Blight/.obsidian/workspace.json new file mode 100644 index 0000000..a8079c7 --- /dev/null +++ b/doc/obsidian vault/Blight/.obsidian/workspace.json @@ -0,0 +1,199 @@ +{ + "main": { + "id": "7b9a110b77821d4d", + "type": "split", + "children": [ + { + "id": "ed89ee9654f5ebf5", + "type": "tabs", + "children": [ + { + "id": "7ebae0b046782c7e", + "type": "leaf", + "state": { + "type": "canvas", + "state": { + "file": "Unbenannt.canvas", + "viewState": { + "x": 0, + "y": 0, + "zoom": 0.5 + } + }, + "icon": "lucide-layout-dashboard", + "title": "Unbenannt" + } + } + ] + } + ], + "direction": "vertical" + }, + "left": { + "id": "4ca45f64b0438d69", + "type": "split", + "children": [ + { + "id": "8b57a21b332b9e1e", + "type": "tabs", + "children": [ + { + "id": "2878c097984db309", + "type": "leaf", + "state": { + "type": "file-explorer", + "state": { + "sortOrder": "alphabetical", + "autoReveal": false, + "showSearch": false, + "searchQuery": "" + }, + "icon": "lucide-folder-closed", + "title": "Dateiexplorer" + } + }, + { + "id": "f90108046c83d5a8", + "type": "leaf", + "state": { + "type": "search", + "state": { + "query": "", + "matchingCase": false, + "explainSearch": false, + "collapseAll": false, + "extraContext": false, + "sortOrder": "alphabetical" + }, + "icon": "lucide-search", + "title": "Suchen" + } + }, + { + "id": "9af167f81ede0735", + "type": "leaf", + "state": { + "type": "bookmarks", + "state": { + "showSearch": false, + "searchQuery": "" + }, + "icon": "lucide-bookmark", + "title": "Lesezeichen" + } + } + ] + } + ], + "direction": "horizontal", + "width": 300 + }, + "right": { + "id": "c5159cfc5125a4eb", + "type": "split", + "children": [ + { + "id": "76a1f1e9289390f8", + "type": "tabs", + "children": [ + { + "id": "c06cb7b2d16b12f1", + "type": "leaf", + "state": { + "type": "backlink", + "state": { + "file": "Unbenannt.canvas", + "collapseAll": false, + "extraContext": false, + "sortOrder": "alphabetical", + "showSearch": false, + "searchQuery": "", + "backlinkCollapsed": false, + "unlinkedCollapsed": true + }, + "icon": "links-coming-in", + "title": "Rückverweise" + } + }, + { + "id": "d9e6d009c66c5672", + "type": "leaf", + "state": { + "type": "outgoing-link", + "state": { + "file": "Unbenannt.canvas", + "linksCollapsed": false, + "unlinkedCollapsed": true + }, + "icon": "links-going-out", + "title": "Ausgehende Links" + } + }, + { + "id": "85e009dc475e5afc", + "type": "leaf", + "state": { + "type": "tag", + "state": { + "sortOrder": "frequency", + "useHierarchy": true, + "showSearch": false, + "searchQuery": "" + }, + "icon": "lucide-tags", + "title": "Tags" + } + }, + { + "id": "02f8ceaf43ea7c76", + "type": "leaf", + "state": { + "type": "all-properties", + "state": { + "sortOrder": "frequency", + "showSearch": false, + "searchQuery": "" + }, + "icon": "lucide-archive", + "title": "Alle Eigenschaften" + } + }, + { + "id": "b5e839340a93fa4f", + "type": "leaf", + "state": { + "type": "outline", + "state": { + "file": "Unbenannt.canvas", + "followCursor": false, + "showSearch": false, + "searchQuery": "" + }, + "icon": "lucide-list", + "title": "Gliederung" + } + } + ] + } + ], + "direction": "horizontal", + "width": 300, + "collapsed": true + }, + "left-ribbon": { + "hiddenItems": { + "switcher:Schnellauswahl öffnen": false, + "graph:Graph-Ansicht öffnen": false, + "canvas:Neuen Canvas erstellen": false, + "daily-notes:Heutige Notiz öffnen": false, + "templates:Vorlage einfügen": false, + "command-palette:Befehlspalette öffnen": false, + "bases:Neue Base erstellen": false + } + }, + "active": "7ebae0b046782c7e", + "lastOpenFiles": [ + "Willkommen.md", + "Unbenannt.canvas" + ] +} \ No newline at end of file diff --git a/doc/obsidian vault/Blight/Unbenannt.canvas b/doc/obsidian vault/Blight/Unbenannt.canvas new file mode 100644 index 0000000..caab975 --- /dev/null +++ b/doc/obsidian vault/Blight/Unbenannt.canvas @@ -0,0 +1,8 @@ +{ + "nodes":[ + {"id":"c8af93c7b5619ebe","x":-510,"y":10,"width":250,"height":60,"type":"text","text":""}, + {"id":"3ed71b638a8ad392","x":-495,"y":-287,"width":250,"height":60,"type":"text","text":"oubsdf\n"}, + {"id":"f30680cddfc95cf3","x":-320,"y":-96,"width":250,"height":60,"type":"text","text":"asdf"} + ], + "edges":[] +} \ No newline at end of file diff --git a/doc/obsidian vault/Blight/Willkommen.md b/doc/obsidian vault/Blight/Willkommen.md new file mode 100644 index 0000000..067c7a5 --- /dev/null +++ b/doc/obsidian vault/Blight/Willkommen.md @@ -0,0 +1,5 @@ +Das ist dein neuer *Vault*. + +Notiere dir etwas oder, [[Neuer Link|erstelle einen neuen Link]], oder probiere [die Importer-Erweiterung aus](https://help.obsidian.md/Plugins/Importer)! + +Wenn du soweit bist kannst du diese Seite löschen und den Vault ganz nach deinen Wünschen und Vorstellungen gestalten. \ No newline at end of file