Begonnen: Verhalten von Gegnern bei annäherung, Verfolgung etc

This commit is contained in:
2026-09-27 00:12:26 +02:00
parent 94e0cac10d
commit da8e5e99f1
34 changed files with 2220 additions and 887 deletions

Binary file not shown.

View File

@@ -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"
}

View File

@@ -1,364 +1,25 @@
{ {
"status": "NEUTRAL", "status": "NEUTRAL",
"trader": false, "trader": false,
"dialogOptions": { "dialogOptions": {},
"begruessung": { "dialogRoots": [],
"id": "begruessung", "dialogNext": {},
"requiresChapter": 0, "dialogDisables": {},
"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"
]
},
"defaultMessages": {}, "defaultMessages": {},
"routines": [ "routines": [
{ {
"name": "chapter0", "name": "patrol_pass",
"blocks": [ "blocks": [
{ {
"startHour": 0, "startHour": 0,
"endHour": 24, "endHour": 23,
"activity": { "activity": {
"type": "STAND", "type": "STAND",
"position": { "position": {
"x": 0.0, "x": -7.9,
"y": 1.0, "y": 2.0,
"z": 0.0 "z": -1.0,
"yaw": 0.0
} }
} }
} }
@@ -366,10 +27,7 @@
} }
], ],
"characterId": "bjoernson", "characterId": "bjoernson",
"name": { "modelPath": "Characters/Models/bjoernson.j3o",
"id": "bjoernson.name"
},
"modelPath": "Models/Chars/bjoernson.j3o",
"animSetPath": "human", "animSetPath": "human",
"type": "NPC" "type": "NPC"
} }

View File

@@ -12,6 +12,19 @@
"openManaRegeneration": 0, "openManaRegeneration": 0,
"openStaminaRegeneration": 0, "openStaminaRegeneration": 0,
"restorationValue": 10, "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": [], "playedMonologueIds": [],
"firedTriggerIds": [], "firedTriggerIds": [],
"visitedZoneIds": [], "visitedZoneIds": [],

View File

@@ -1,442 +1,12 @@
{ {
"status": "NEUTRAL", "status": "NEUTRAL",
"trader": false, "trader": false,
"dialogOptions": { "dialogOptions": {},
"begruessung": { "dialogRoots": [],
"id": "begruessung", "dialogNext": {},
"requiresChapter": 0, "dialogDisables": {},
"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"
]
},
"defaultMessages": {}, "defaultMessages": {},
"routines": [ "routines": [],
{
"name": "chapter0",
"blocks": [
{
"startHour": 0,
"endHour": 24,
"activity": {
"type": "STAND",
"position": {
"x": -17.0,
"y": 1.2,
"z": -1315.6
}
}
}
]
}
],
"characterId": "silas", "characterId": "silas",
"name": { "name": {
"id": "silas.name" "id": "silas.name"

View File

@@ -8,8 +8,6 @@ import lombok.Setter;
public abstract class GameCharacter { public abstract class GameCharacter {
/** Eindeutige ID — entspricht dem Dateinamen ohne Extension. */ /** Eindeutige ID — entspricht dem Dateinamen ohne Extension. */
private String characterId; 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). */ /** Relativer Pfad zum .j3o-Modell (relativ zu blight-assets/src/main/resources). */
private String modelPath; private String modelPath;
/** Relativer Pfad zum .j3o-Animations-Set (in animations/sets/). */ /** Relativer Pfad zum .j3o-Animations-Set (in animations/sets/). */

View File

@@ -26,6 +26,19 @@ public class NPC extends GameCharacter implements Interactable {
private List<Item> items; private List<Item> 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) ───────────────────────────────── // ── Flache Dialog-Struktur (serialisiert) ─────────────────────────────────
/** Alle Dialog-Optionen dieses NPCs, flach nach ID indiziert. */ /** Alle Dialog-Optionen dieses NPCs, flach nach ID indiziert. */
@@ -90,13 +103,12 @@ public class NPC extends GameCharacter implements Interactable {
@Override @Override
public String getDisplayText() { 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 @Override
public String getLabelKey() { public String getLabelKey() {
TextReference nameRef = getName();
if (nameRef != null && nameRef.id() != null && !nameRef.id().isBlank()) return nameRef.id();
return getCharacterId() != null ? "npc." + getCharacterId() + ".name" : ""; return getCharacterId() != null ? "npc." + getCharacterId() + ".name" : "";
} }

View File

@@ -4,17 +4,14 @@ package de.blight.common.model;
* Zeitblock innerhalb eines Tagesablaufs. * Zeitblock innerhalb eines Tagesablaufs.
* *
* {@code startHour} und {@code endHour} sind ganzzahlige Stunden (0–23). * {@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: * Ein Block, der über Mitternacht geht (z.B. 22–06), ist erlaubt:
* in diesem Fall ist {@code endHour < startHour}. * in diesem Fall ist {@code endHour < startHour}.
*
* Die Dauer berechnet sich als:
* endHour >= startHour → endHour - startHour
* endHour < startHour → 24 - startHour + endHour
*/ */
public class RoutineBlock { public class RoutineBlock {
private int startHour; // 0-23 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; private RoutineActivity activity;
public RoutineBlock() {} public RoutineBlock() {}
@@ -34,23 +31,22 @@ public class RoutineBlock {
public RoutineActivity getActivity() { return activity; } public RoutineActivity getActivity() { return activity; }
public void setActivity(RoutineActivity a) { this.activity = a; } 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() { public int durationHours() {
if (endHour > startHour) return endHour - startHour; if (endHour >= startHour) return endHour - startHour + 1;
if (endHour < startHour) return 24 - startHour + endHour; return 24 - startHour + endHour + 1;
return 0;
} }
/** 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) { public boolean covers(int hour) {
if (endHour > startHour) return hour >= startHour && hour < endHour; if (endHour >= startHour) return hour >= startHour && hour <= endHour;
// Über-Mitternacht-Block // Ü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() { public String displayLabel() {
String act = activity != null ? activity.summary() : "—"; 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);
} }
} }

View File

@@ -1276,6 +1276,17 @@ public class AnimPreviewState extends BaseAppState {
assets.registerLocator(fbmDir.getAbsolutePath(), com.jme3.asset.plugins.FileLocator.class); 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 = com.github.stephengold.wrench.LwjglAssetKey key =
new com.github.stephengold.wrench.LwjglAssetKey(file.getName()); new com.github.stephengold.wrench.LwjglAssetKey(file.getName());
key.setVerboseLogging(true); key.setVerboseLogging(true);
@@ -1289,6 +1300,8 @@ public class AnimPreviewState extends BaseAppState {
return; 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); java.io.File embeddedDir = extractEmbeddedTextures(file, model);
if (embeddedDir != null) { if (embeddedDir != null) {
assets.registerLocator(embeddedDir.getAbsolutePath(), com.jme3.asset.plugins.FileLocator.class); 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. * 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) { private java.io.File extractEmbeddedTextures(java.io.File fbxFile, Spatial model) {
java.util.Set<String> filenames = collectTextureFilenames(model); java.util.Set<String> 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"), java.io.File tempDir = new java.io.File(System.getProperty("java.io.tmpdir"),
"fbxtex_" + Math.abs(fbxFile.getAbsolutePath().hashCode())); "fbxtex_" + Math.abs(fbxFile.getAbsolutePath().hashCode()));
tempDir.mkdirs(); tempDir.mkdirs();
byte[] pngMagic = {(byte)0x89,0x50,0x4E,0x47,0x0D,0x0A,0x1A,0x0A}; 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[] 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; boolean any = false;
for (String filename : filenames) { for (String filename : filenames) {
byte[] nameBytes = filename.getBytes(java.nio.charset.StandardCharsets.US_ASCII); byte[] nameBytes = filename.getBytes(java.nio.charset.StandardCharsets.US_ASCII);
int namePos = binaryIndexOf(data, nameBytes, 0); int namePos = binaryIndexOf(data, nameBytes, 0);
if (namePos < 0) continue; if (namePos < 0) continue;
// ── PNG ───────────────────────────────────────────────────────
int pngPos = binaryIndexOf(data, pngMagic, namePos); int pngPos = binaryIndexOf(data, pngMagic, namePos);
if (pngPos < 0 || pngPos - namePos > 65536) continue; if (pngPos >= 0 && pngPos - namePos <= 65536) {
int endPos = binaryIndexOf(data, iend, pngPos + 8); int endPos = binaryIndexOf(data, iend, pngPos + 8);
if (endPos < 0) continue; if (endPos >= 0) {
byte[] png = java.util.Arrays.copyOfRange(data, pngPos, endPos + iend.length); byte[] png = java.util.Arrays.copyOfRange(data, pngPos, endPos + iend.length);
java.nio.file.Files.write(new java.io.File(tempDir, filename).toPath(), png); java.nio.file.Files.write(new java.io.File(tempDir, filename).toPath(), png);
LOG.info("[FbxImport] Embedded-Textur extrahiert: {} ({} B)", filename, png.length); 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; any = true;
} }
return any ? tempDir : null; 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) { private static int binaryIndexOf(byte[] data, byte[] pattern, int from) {
outer: outer:
for (int i = from, max = data.length - pattern.length; i <= max; i++) { for (int i = from, max = data.length - pattern.length; i <= max; i++) {

View File

@@ -59,6 +59,7 @@ public class ContentController {
private de.blight.editor.ui.RoutineEditorView routineEditorView; private de.blight.editor.ui.RoutineEditorView routineEditorView;
private de.blight.editor.ui.MonologueEditorView monologueEditorView; private de.blight.editor.ui.MonologueEditorView monologueEditorView;
private StackPane routineJmeSlot; private StackPane routineJmeSlot;
private boolean loadingCharacter = false;
// ── Character-Formular-Felder ────────────────────────────────────────────── // ── Character-Formular-Felder ──────────────────────────────────────────────
private VBox charEditContainer; private VBox charEditContainer;
@@ -69,7 +70,9 @@ public class ContentController {
private CheckBox charTraderCheck; private CheckBox charTraderCheck;
private ListView<String> charTraderItemsView; private ListView<String> charTraderItemsView;
private TextField charMsgFriendly, charMsgNeutral, charMsgEnraged, charMsgEnemy; private TextField charMsgFriendly, charMsgNeutral, charMsgEnraged, charMsgEnemy;
private TextField charNameField, charIdField; private TextField charMaxHpField, charMaxManaField, charMaxStaminaField;
private ComboBox<String> charWeaponCombo, charShieldCombo;
private TextField charIdField;
private ComboBox<String> charTypeCombo, charModelCombo, charAnimSetCombo; private ComboBox<String> charTypeCombo, charModelCombo, charAnimSetCombo;
private ComboBox<de.blight.common.model.Fraction> charFractionCombo; private ComboBox<de.blight.common.model.Fraction> charFractionCombo;
private Label charEditorStatusLabel; private Label charEditorStatusLabel;
@@ -263,11 +266,11 @@ public class ContentController {
public void loadCharacterById(String id) { public void loadCharacterById(String id) {
if (id == null || id.isBlank()) return; if (id == null || id.isBlank()) return;
Path charDir = ctx.assetRoot().resolve("Characters"); Path charDir = ctx.assetRoot().resolve("Characters");
loadingCharacter = true;
try { try {
de.blight.common.model.GameCharacter c = de.blight.common.model.GameCharacter c =
de.blight.common.model.CharacterIO.load(charDir.resolve(id + ".character")); de.blight.common.model.CharacterIO.load(charDir.resolve(id + ".character"));
if (charIdField != null) charIdField.setText(c.getCharacterId() != null ? c.getCharacterId() : ""); if (charIdField != null) charIdField.setText(c.getCharacterId() != null ? c.getCharacterId() : "");
if (charNameField != null) charNameField.setText(c.getName() != null ? c.getName().id() : "");
if (charTypeCombo != null) if (charTypeCombo != null)
charTypeCombo.setValue(c instanceof de.blight.common.model.MainCharacter ? "MainCharacter" : "NPC"); charTypeCombo.setValue(c instanceof de.blight.common.model.MainCharacter ? "MainCharacter" : "NPC");
if (charModelCombo != null) charModelCombo.setValue(c.getModelPath()); 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 (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 (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 (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) { if (dialogEditorView != null) {
dialogEditorView.setDisable(false); dialogEditorView.setDisable(false);
dialogEditorView.loadNpc(npc); dialogEditorView.loadNpc(npc);
@@ -325,6 +339,8 @@ public class ContentController {
if (charEditorStatusLabel != null) charEditorStatusLabel.setText("Geladen: " + id); if (charEditorStatusLabel != null) charEditorStatusLabel.setText("Geladen: " + id);
} catch (Exception e) { } catch (Exception e) {
if (charEditorStatusLabel != null) charEditorStatusLabel.setText("Fehler: " + e.getMessage()); 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 = new de.blight.editor.ui.DialogEditorView();
dialogEditorView.setDisable(true); dialogEditorView.setDisable(true);
monologueEditorView = new de.blight.editor.ui.MonologueEditorView(); 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(); TabPane tabs = new TabPane();
tabs.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE); tabs.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE);
Tab charTab = new Tab("Charakter", buildCharacterSettingsPane()); Tab charTab = new Tab("Charakter", buildCharacterSettingsPane());
Tab dialogTab = new Tab("Dialog", dialogEditorView); Tab dialogTab = new Tab("Dialog", dialogEditorView);
Tab monologTab = new Tab("Monologe", monologueEditorView); Tab routineTab = new Tab("Tagesabläufe", routineSplit);
tabs.getTabs().addAll(charTab, dialogTab, monologTab); Tab monologTab = new Tab("Monologe", monologueEditorView);
tabs.getTabs().addAll(charTab, dialogTab, routineTab, monologTab);
tabs.getSelectionModel().select(charTab); tabs.getSelectionModel().select(charTab);
setContentCenter(tabs); setContentCenter(tabs);
@@ -1085,9 +1113,6 @@ public class ContentController {
charIdField = new TextField(); charIdField = new TextField();
charIdField.setPromptText("eindeutige ID (Dateiname)"); charIdField.setPromptText("eindeutige ID (Dateiname)");
charNameField = new TextField();
charNameField.setPromptText("Anzeigename");
charNameField.setEditable(false);
charTypeCombo = new ComboBox<>(); charTypeCombo = new ComboBox<>();
charTypeCombo.getItems().addAll("MainCharacter", "NPC"); charTypeCombo.getItems().addAll("MainCharacter", "NPC");
@@ -1113,9 +1138,8 @@ public class ContentController {
charAnimSetCombo.setOnAction(e -> updateCharActionCombosFromSet()); charAnimSetCombo.setOnAction(e -> updateCharActionCombosFromSet());
charEditContainer.getChildren().addAll( charEditContainer.getChildren().addAll(
dl.apply("ID:"), charIdField, dl.apply("ID:"), charIdField,
dl.apply("Name:"), charNameField, dl.apply("Typ:"), charTypeCombo
dl.apply("Typ:"), charTypeCombo
); );
charFractionCombo = new ComboBox<>(); charFractionCombo = new ComboBox<>();
@@ -1180,6 +1204,40 @@ public class ContentController {
charMsgEnemy = new TextField(); charMsgEnemy = new TextField();
charMsgEnemy.setPromptText("TextReference-Schlüssel"); 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<de.blight.common.model.Item> 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, charNpcSection = new VBox(4,
dl.apply("Status:"), charStatusCombo, dl.apply("Status:"), charStatusCombo,
dl.apply("Handel:"), charTraderCheck, dl.apply("Handel:"), charTraderCheck,
@@ -1188,7 +1246,14 @@ public class ContentController {
dl.apply("Friendly:"), charMsgFriendly, dl.apply("Friendly:"), charMsgFriendly,
dl.apply("Neutral:"), charMsgNeutral, dl.apply("Neutral:"), charMsgNeutral,
dl.apply("Enraged:"), charMsgEnraged, 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()); boolean npcInitial = "NPC".equals(charTypeCombo.getValue());
charNpcSection.setVisible(npcInitial); charNpcSection.setVisible(npcInitial);
charNpcSection.setManaged(npcInitial); charNpcSection.setManaged(npcInitial);
@@ -1238,17 +1303,11 @@ public class ContentController {
); );
// Buttons // Buttons
Button saveBtn = new Button("Speichern");
Button clearBtn = new Button("Leeren"); Button clearBtn = new Button("Leeren");
saveBtn.setMaxWidth(Double.MAX_VALUE);
clearBtn.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()); clearBtn.setOnAction(e -> clearCharacterForm());
HBox.setHgrow(saveBtn, Priority.ALWAYS);
HBox.setHgrow(clearBtn, Priority.ALWAYS); HBox.setHgrow(clearBtn, Priority.ALWAYS);
HBox btnBar = new HBox(6, saveBtn, clearBtn); charEditContainer.getChildren().addAll(new Separator(), clearBtn);
charEditContainer.getChildren().addAll(new Separator(), btnBar);
// Auto-Save-Listener // Auto-Save-Listener
charIdField.focusedProperty().addListener((obs, ov, nv) -> { if (!nv) autoSaveCharacter(charDir); }); 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); }); charMsgNeutral.focusedProperty().addListener((obs, ov, nv) -> { if (!nv) autoSaveCharacter(charDir); });
charMsgEnraged.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); }); 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); inner.getChildren().add(charEditContainer);
@@ -1275,6 +1340,7 @@ public class ContentController {
} }
private void autoSaveCharacter(Path charDir) { private void autoSaveCharacter(Path charDir) {
if (loadingCharacter) return;
if (charIdField == null || charIdField.getText().trim().isBlank()) return; if (charIdField == null || charIdField.getText().trim().isBlank()) return;
saveCharacter(charDir); saveCharacter(charDir);
} }
@@ -1333,8 +1399,7 @@ public class ContentController {
} }
private void clearCharacterForm() { private void clearCharacterForm() {
if (charIdField != null) charIdField.clear(); if (charIdField != null) charIdField.clear();
if (charNameField != null) charNameField.clear();
if (charTypeCombo != null) charTypeCombo.setValue("NPC"); if (charTypeCombo != null) charTypeCombo.setValue("NPC");
if (charModelCombo != null) charModelCombo.setValue(null); if (charModelCombo != null) charModelCombo.setValue(null);
if (charAnimSetCombo != null) charAnimSetCombo.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.MainCharacter()
: new de.blight.common.model.NPC(); : new de.blight.common.model.NPC();
c.setCharacterId(id); 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.setModelPath(charModelCombo != null ? charModelCombo.getValue() : null);
c.setAnimSetPath(charAnimSetCombo != null ? charAnimSetCombo.getValue() : null); c.setAnimSetPath(charAnimSetCombo != null ? charAnimSetCombo.getValue() : null);
if (c instanceof de.blight.common.model.NPC npc) { if (c instanceof de.blight.common.model.NPC npc) {
@@ -1404,12 +1467,24 @@ public class ContentController {
} else { } else {
npc.setItems(null); 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) { if (c instanceof de.blight.common.model.MainCharacter mc) {
mc.setAbilities(buildAbilities()); mc.setAbilities(buildAbilities());
} }
try { try {
de.blight.common.model.CharacterIO.save(c, charDir); 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(); refreshContentCharacterTree();
if (charEditorStatusLabel != null) charEditorStatusLabel.setText("Gespeichert: " + id); if (charEditorStatusLabel != null) charEditorStatusLabel.setText("Gespeichert: " + id);
} catch (Exception e) { } 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() { private void refreshFractionCombo() {
loadedFractions.clear(); loadedFractions.clear();
loadedFractions.addAll( loadedFractions.addAll(

View File

@@ -268,6 +268,7 @@ public class GameController {
javaExe, javaExe,
"--add-opens", "java.base/java.lang=ALL-UNNAMED", "--add-opens", "java.base/java.lang=ALL-UNNAMED",
"--add-opens", "java.desktop/sun.awt=ALL-UNNAMED", "--add-opens", "java.desktop/sun.awt=ALL-UNNAMED",
"-Xmx2g",
"-Djava.library.path=" + libPath, "-Djava.library.path=" + libPath,
"-Dblight.project.root=" + projRoot, "-Dblight.project.root=" + projRoot,
"-D" + de.blight.common.MapIO.PROP_SESSION_MAP + "=" + sessionMapPath, "-D" + de.blight.common.MapIO.PROP_SESSION_MAP + "=" + sessionMapPath,

View File

@@ -69,7 +69,12 @@ public class RoutineEditorView extends VBox {
private final List<PlacedItem> placedItems = new ArrayList<>(); private final List<PlacedItem> placedItems = new ArrayList<>();
private final List<String> npcIds = new ArrayList<>(); private final List<String> 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 ────────────────────────────────────────────────────────── // ── Polling-Timer ──────────────────────────────────────────────────────────
@@ -358,6 +363,7 @@ public class RoutineEditorView extends VBox {
activeBlock.getActivity().getWaypoints().add(wp); activeBlock.getActivity().getWaypoints().add(wp);
refreshWaypointList(); refreshWaypointList();
updateMarkers(); updateMarkers();
fireDataChanged();
})); }));
delWp.setOnAction(e -> { delWp.setOnAction(e -> {
@@ -366,6 +372,7 @@ public class RoutineEditorView extends VBox {
wps.remove(wps.size() - 1); wps.remove(wps.size() - 1);
refreshWaypointList(); refreshWaypointList();
updateMarkers(); updateMarkers();
fireDataChanged();
} }
}); });
@@ -408,6 +415,7 @@ public class RoutineEditorView extends VBox {
activeBlock.setStartHour(startSpin.getValue()); activeBlock.setStartHour(startSpin.getValue());
activeBlock.setEndHour(endSpin.getValue()); activeBlock.setEndHour(endSpin.getValue());
refreshCoverage(); refreshCoverage();
fireDataChanged();
} }
private void applyActivityToBlock() { private void applyActivityToBlock() {
@@ -456,6 +464,7 @@ public class RoutineEditorView extends VBox {
} }
// PATROL is managed directly in buildPatrolFields // PATROL is managed directly in buildPatrolFields
} }
fireDataChanged();
} }
// ── Routine-Operationen ──────────────────────────────────────────────────── // ── Routine-Operationen ────────────────────────────────────────────────────
@@ -470,6 +479,7 @@ public class RoutineEditorView extends VBox {
routines.add(r); routines.add(r);
refreshRoutineList(); refreshRoutineList();
routineList.getSelectionModel().selectLast(); routineList.getSelectionModel().selectLast();
fireDataChanged();
}); });
} }
@@ -482,6 +492,7 @@ public class RoutineEditorView extends VBox {
if (!name.isBlank()) { if (!name.isBlank()) {
activeRoutine.setName(name.trim()); activeRoutine.setName(name.trim());
refreshRoutineList(); refreshRoutineList();
fireDataChanged();
} }
}); });
} }
@@ -494,6 +505,7 @@ public class RoutineEditorView extends VBox {
refreshRoutineList(); refreshRoutineList();
if (!routines.isEmpty()) if (!routines.isEmpty())
routineList.getSelectionModel().select(Math.min(idx, routines.size() - 1)); routineList.getSelectionModel().select(Math.min(idx, routines.size() - 1));
fireDataChanged();
} }
private void addBlock() { private void addBlock() {
@@ -502,6 +514,7 @@ public class RoutineEditorView extends VBox {
activeRoutine.getBlocks().add(b); activeRoutine.getBlocks().add(b);
refreshBlockList(); refreshBlockList();
blockList.getSelectionModel().selectLast(); blockList.getSelectionModel().selectLast();
fireDataChanged();
} }
private void deleteBlock() { private void deleteBlock() {
@@ -514,6 +527,7 @@ public class RoutineEditorView extends VBox {
refreshCoverage(); refreshCoverage();
if (!activeRoutine.getBlocks().isEmpty()) if (!activeRoutine.getBlocks().isEmpty())
blockList.getSelectionModel().select(Math.min(idx, activeRoutine.getBlocks().size() - 1)); blockList.getSelectionModel().select(Math.min(idx, activeRoutine.getBlocks().size() - 1));
fireDataChanged();
} }
private void moveBlock(int delta) { private void moveBlock(int delta) {

View File

@@ -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) {}
}

View File

@@ -38,14 +38,16 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
public class PlayerInputControl { public class PlayerInputControl {
private static final Logger log = LoggerFactory.getLogger(PlayerInputControl.class); 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 SPRINT_MULT = 1.5f;
private static final float WALK_MULT = 0.5f; private static final float WALK_MULT = 0.5f;
private static final float ROTATE_SPEED = 10f; private static final float ROTATE_SPEED = 10f;
@@ -153,7 +155,7 @@ public class PlayerInputControl {
private float heavyLungeEndRemaining = 0f; private float heavyLungeEndRemaining = 0f;
/** Gesamtdauer der aktuellen Kampfanim — für Kombo-Fenster-Timing (öffnet bei 50%). */ /** Gesamtdauer der aktuellen Kampfanim — für Kombo-Fenster-Timing (öffnet bei 50%). */
private float combatAnimDuration = 0f; private float combatAnimDuration = 0f;
private MeleeAttackCallback meleeAttackCallback = null; private final List<MeleeAttackCallback> meleeCallbacks = new ArrayList<>();
private WeaponAttachmentControl weaponAttach = null; private WeaponAttachmentControl weaponAttach = null;
private static final float HIT_WINDOW_START = 0.20f; private static final float HIT_WINDOW_START = 0.20f;
private static final float HIT_WINDOW_END = 0.75f; private static final float HIT_WINDOW_END = 0.75f;
@@ -430,7 +432,22 @@ public class PlayerInputControl {
public ItemSubCategory getDrawnWeaponType() { return drawnWeaponType; } public ItemSubCategory getDrawnWeaponType() { return drawnWeaponType; }
public boolean isLastAttackCritical() { return lastAttackCritical; } public boolean isLastAttackCritical() { return lastAttackCritical; }
public com.jme3.anim.SkinningControl getSkinningControl() { return skinningControl; } 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 void setWeaponAttachmentControl(WeaponAttachmentControl w) { this.weaponAttach = w; }
public boolean isPaused() { return paused; } public boolean isPaused() { return paused; }
@@ -1345,6 +1362,13 @@ public class PlayerInputControl {
|| blockPhase == BlockPhase.EXITING; || 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. */ /** Wird aufgerufen wenn ein blockierter Treffer landet. Kostet 30 Ausdauer; unter 0 → Block bricht. */
public void triggerBlockImpact() { public void triggerBlockImpact() {
if (blockPhase != BlockPhase.HOLDING || drawnWeaponType == null) return; if (blockPhase != BlockPhase.HOLDING || drawnWeaponType == null) return;
@@ -1621,7 +1645,7 @@ public class PlayerInputControl {
} }
private void fireWeaponCallback(float critMultiplier) { private void fireWeaponCallback(float critMultiplier) {
if (meleeAttackCallback == null || weaponAttach == null) { if (meleeCallbacks.isEmpty() || weaponAttach == null) {
return; return;
} }
com.jme3.scene.Spatial ws = weaponAttach.getWeaponSpatial(); com.jme3.scene.Spatial ws = weaponAttach.getWeaponSpatial();
@@ -1633,7 +1657,9 @@ public class PlayerInputControl {
? mainCharacter.getEquippedMeleeWeapon().getDamage() : 0; ? mainCharacter.getEquippedMeleeWeapon().getDamage() : 0;
Vector3f grip = hn.getWorldTranslation().clone(); Vector3f grip = hn.getWorldTranslation().clone();
Vector3f bladeDir = ws.getWorldRotation().mult(com.jme3.math.Vector3f.UNIT_Y).normalizeLocal(); 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() { private float computeHeavyMultiplier() {

View File

@@ -56,6 +56,10 @@ public class CharacterNavigator {
public static final float WALK_SPEED = 0.035f; public static final float WALK_SPEED = 0.035f;
/** Renn-Geschwindigkeit (m/frame bei 60fps). */ /** Renn-Geschwindigkeit (m/frame bei 60fps). */
public static final float RUN_SPEED = 0.07f; 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. */ /** Minimale Bewegung (m/s) unter der ein Charakter als steckengeblieben gilt. */
private static final float STUCK_MIN_SPEED = 0.2f; private static final float STUCK_MIN_SPEED = 0.2f;
/** Zeit (s) mit zu langsamer Bewegung bis zum Abbruch. */ /** Zeit (s) mit zu langsamer Bewegung bis zum Abbruch. */

View File

@@ -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
}

View File

@@ -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);
}
}

View File

@@ -623,6 +623,8 @@ public class WorldScene extends BaseAppState {
animLib.configureLocomotionSnap(AnimationLibrary.findAssetRoot(), setName); animLib.configureLocomotionSnap(AnimationLibrary.findAssetRoot(), setName);
} }
animLib.applyAllTo(characterVisual != null ? characterVisual : character); 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: {}", log.info("[AnimCtx] MainCharacter: {} animSetPath: {} clipCount: {} clips: {}",
mc != null ? mc.getCharacterId() : "null", mc != null ? mc.getCharacterId() : "null",
setName, setName,

View File

@@ -24,7 +24,8 @@ public class InteractionHudState extends BaseAppState {
private static final Logger log = LoggerFactory.getLogger(InteractionHudState.class); 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 Camera cam;
private Node guiNode; private Node guiNode;
@@ -52,7 +53,7 @@ public class InteractionHudState extends BaseAppState {
labelText = new BitmapText(font); labelText = new BitmapText(font);
labelText.setSize(font.getCharSet().getRenderedSize() * 1.2f * 2f); labelText.setSize(font.getCharSet().getRenderedSize() * 1.2f * 2f);
labelText.setLocalScale(0.5f, 0.5f, 1f); 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); labelText.setCullHint(Spatial.CullHint.Always);
guiNode.attachChild(labelText); guiNode.attachChild(labelText);
} }
@@ -90,15 +91,18 @@ public class InteractionHudState extends BaseAppState {
return; return;
} }
String labelKey = null; String labelKey = null;
Vector3f worldPos = null; Vector3f worldPos = null;
com.jme3.math.ColorRGBA labelColor = COL_DEFAULT;
// NPC hat höchste Priorität, dann Interactable, dann Item // NPC hat höchste Priorität, dann Interactable, dann Item
if (worldNpcs != null) { if (worldNpcs != null) {
String key = worldNpcs.getHoveredLabelKey(); String key = worldNpcs.getHoveredLabelKey();
if (key != null && !key.isBlank()) { if (key != null && !key.isBlank()) {
labelKey = key; labelKey = key;
worldPos = worldNpcs.getHoveredWorldPos(); 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) { if (keyBindings != null) {
text = text.replace("{interact}", KeyNames.of(keyBindings.interact)); text = text.replace("{interact}", KeyNames.of(keyBindings.interact));
} }
labelText.setColor(labelColor);
labelText.setText(text); labelText.setText(text);
float textW = labelText.getLineWidth(); float textW = labelText.getLineWidth() * 0.5f;
labelText.setLocalTranslation(screenV.x - textW * 0.5f, screenV.y, 1f); labelText.setLocalTranslation(screenV.x - textW, screenV.y, 1f);
labelText.setCullHint(Spatial.CullHint.Inherit); labelText.setCullHint(Spatial.CullHint.Inherit);
} }
} }

View File

@@ -130,7 +130,7 @@ public class WorldDummyState extends BaseAppState implements MeleeAttackCallback
loadDummies(); loadDummies();
buildEnemyHud(); buildEnemyHud();
playerInput.setMeleeAttackCallback(this); playerInput.addMeleeAttackCallback(this);
} }
@Override @Override

View File

@@ -249,3 +249,33 @@ einfacher_schild.name=Einfacher Schild
rostiges_kurzschwert.name=Rostiges Kurzschwert rostiges_kurzschwert.name=Rostiges Kurzschwert
blutagave.name=Blutagave blutagave.name=Blutagave
erzmoss.name=Erzmoss 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

View File

@@ -104,3 +104,33 @@ einfacher_schild.name=Simple Shield
rostiges_kurzschwert.name=Rusty Short Sword rostiges_kurzschwert.name=Rusty Short Sword
blutagave.name=Blood Agave blutagave.name=Blood Agave
erzmoss.name=Ore Moss 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

BIN
doc/Banditen Plot.odt Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1 @@
{}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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"
]
}

View File

@@ -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":[]
}

View File

@@ -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.