Begonnen: Verhalten von Gegnern bei annäherung, Verfolgung etc
This commit is contained in:
BIN
assets/imported/models/bandit1.fbx
Normal file
BIN
assets/imported/models/bandit1.fbx
Normal file
Binary file not shown.
BIN
blight-assets/src/main/resources/Characters/Models/bandit1.j3o
Normal file
BIN
blight-assets/src/main/resources/Characters/Models/bandit1.j3o
Normal file
Binary file not shown.
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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": [],
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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/). */
|
||||
|
||||
@@ -26,6 +26,19 @@ public class NPC extends GameCharacter implements Interactable {
|
||||
|
||||
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) ─────────────────────────────────
|
||||
|
||||
/** 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" : "";
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String> filenames = collectTextureFilenames(model);
|
||||
@@ -1591,18 +1607,49 @@ public class AnimPreviewState extends BaseAppState {
|
||||
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[] 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;
|
||||
if (pngPos >= 0 && pngPos - namePos <= 65536) {
|
||||
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);
|
||||
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;
|
||||
}
|
||||
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++) {
|
||||
|
||||
@@ -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<String> charTraderItemsView;
|
||||
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<de.blight.common.model.Fraction> 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 (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 routineTab = new Tab("Tagesabläufe", routineSplit);
|
||||
Tab monologTab = new Tab("Monologe", monologueEditorView);
|
||||
tabs.getTabs().addAll(charTab, dialogTab, monologTab);
|
||||
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");
|
||||
@@ -1114,7 +1139,6 @@ public class ContentController {
|
||||
|
||||
charEditContainer.getChildren().addAll(
|
||||
dl.apply("ID:"), charIdField,
|
||||
dl.apply("Name:"), charNameField,
|
||||
dl.apply("Typ:"), charTypeCombo
|
||||
);
|
||||
|
||||
@@ -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<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,
|
||||
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);
|
||||
}
|
||||
@@ -1334,7 +1400,6 @@ public class ContentController {
|
||||
|
||||
private void clearCharacterForm() {
|
||||
if (charIdField != null) charIdField.clear();
|
||||
if (charNameField != null) charNameField.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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -70,6 +70,11 @@ public class RoutineEditorView extends VBox {
|
||||
private final List<String> npcIds = new ArrayList<>();
|
||||
|
||||
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) {
|
||||
|
||||
@@ -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) {}
|
||||
}
|
||||
@@ -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<MeleeAttackCallback> 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() {
|
||||
|
||||
@@ -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. */
|
||||
|
||||
10
blight-game/src/main/java/de/blight/game/npc/AggroEvent.java
Normal file
10
blight-game/src/main/java/de/blight/game/npc/AggroEvent.java
Normal 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
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -25,6 +25,7 @@ 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 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);
|
||||
}
|
||||
@@ -92,6 +93,7 @@ public class InteractionHudState extends BaseAppState {
|
||||
|
||||
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) {
|
||||
@@ -99,6 +101,8 @@ public class InteractionHudState extends BaseAppState {
|
||||
if (key != null && !key.isBlank()) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ public class WorldDummyState extends BaseAppState implements MeleeAttackCallback
|
||||
|
||||
loadDummies();
|
||||
buildEnemyHud();
|
||||
playerInput.setMeleeAttackCallback(this);
|
||||
playerInput.addMeleeAttackCallback(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
BIN
doc/Banditen Plot.odt
Normal file
BIN
doc/Banditen Plot.odt
Normal file
Binary file not shown.
BIN
doc/Das Protektorat/Personen.odt
Normal file
BIN
doc/Das Protektorat/Personen.odt
Normal file
Binary file not shown.
Binary file not shown.
1
doc/obsidian vault/Blight/.obsidian/app.json
vendored
Normal file
1
doc/obsidian vault/Blight/.obsidian/app.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
1
doc/obsidian vault/Blight/.obsidian/appearance.json
vendored
Normal file
1
doc/obsidian vault/Blight/.obsidian/appearance.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
33
doc/obsidian vault/Blight/.obsidian/core-plugins.json
vendored
Normal file
33
doc/obsidian vault/Blight/.obsidian/core-plugins.json
vendored
Normal 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
|
||||
}
|
||||
22
doc/obsidian vault/Blight/.obsidian/graph.json
vendored
Normal file
22
doc/obsidian vault/Blight/.obsidian/graph.json
vendored
Normal 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
|
||||
}
|
||||
199
doc/obsidian vault/Blight/.obsidian/workspace.json
vendored
Normal file
199
doc/obsidian vault/Blight/.obsidian/workspace.json
vendored
Normal 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"
|
||||
]
|
||||
}
|
||||
8
doc/obsidian vault/Blight/Unbenannt.canvas
Normal file
8
doc/obsidian vault/Blight/Unbenannt.canvas
Normal 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":[]
|
||||
}
|
||||
5
doc/obsidian vault/Blight/Willkommen.md
Normal file
5
doc/obsidian vault/Blight/Willkommen.md
Normal 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.
|
||||
Reference in New Issue
Block a user