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",
|
"status": "NEUTRAL",
|
||||||
"trader": false,
|
"trader": false,
|
||||||
"dialogOptions": {
|
"dialogOptions": {},
|
||||||
"begruessung": {
|
"dialogRoots": [],
|
||||||
"id": "begruessung",
|
"dialogNext": {},
|
||||||
"requiresChapter": 0,
|
"dialogDisables": {},
|
||||||
"textHero": {
|
|
||||||
"id": "dialog.bjoernson.begruessung.textmainchar"
|
|
||||||
},
|
|
||||||
"textNpc": {
|
|
||||||
"id": "dialog.bjoernson.begruessung.textnpc"
|
|
||||||
},
|
|
||||||
"heroSteps": [],
|
|
||||||
"npcSteps": [],
|
|
||||||
"abortsQuests": [],
|
|
||||||
"enablesTrade": false,
|
|
||||||
"onChosenTriggers": []
|
|
||||||
},
|
|
||||||
"wer_bist_du": {
|
|
||||||
"id": "wer_bist_du",
|
|
||||||
"requiresChapter": 0,
|
|
||||||
"textHero": {
|
|
||||||
"id": "dialog.bjoernson.wer_bist_du.textmainchar"
|
|
||||||
},
|
|
||||||
"textNpc": {
|
|
||||||
"id": "dialog.bjoernson.wer_bist_du.textnpc"
|
|
||||||
},
|
|
||||||
"heroSteps": [],
|
|
||||||
"npcSteps": [],
|
|
||||||
"abortsQuests": [],
|
|
||||||
"enablesTrade": false,
|
|
||||||
"onChosenTriggers": []
|
|
||||||
},
|
|
||||||
"aus_dem_norden": {
|
|
||||||
"id": "aus_dem_norden",
|
|
||||||
"requiresChapter": 0,
|
|
||||||
"textHero": {
|
|
||||||
"id": "dialog.bjoernson.aus_dem_norden.textmainchar"
|
|
||||||
},
|
|
||||||
"heroSteps": [],
|
|
||||||
"npcSteps": [
|
|
||||||
{
|
|
||||||
"text": { "id": "dialog.bjoernson.aus_dem_norden.textnpc" },
|
|
||||||
"audio": { "key": "dialog.bjoernson.aus_dem_norden.textnpc" }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": { "id": "dialog.bjoernson.aus_dem_norden.textnpc.1" },
|
|
||||||
"audio": { "key": "dialog.bjoernson.aus_dem_norden.textnpc.1" }
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"abortsQuests": [],
|
|
||||||
"enablesTrade": false,
|
|
||||||
"onChosenTriggers": []
|
|
||||||
},
|
|
||||||
"bier_angebot": {
|
|
||||||
"id": "bier_angebot",
|
|
||||||
"requiresChapter": 0,
|
|
||||||
"textHero": {
|
|
||||||
"id": "dialog.bjoernson.bier_angebot.textmainchar"
|
|
||||||
},
|
|
||||||
"textNpc": {
|
|
||||||
"id": "dialog.bjoernson.bier_angebot.textnpc"
|
|
||||||
},
|
|
||||||
"heroSteps": [],
|
|
||||||
"npcSteps": [],
|
|
||||||
"abortsQuests": [],
|
|
||||||
"enablesTrade": false,
|
|
||||||
"onChosenTriggers": []
|
|
||||||
},
|
|
||||||
"abgemacht_bier": {
|
|
||||||
"id": "abgemacht_bier",
|
|
||||||
"requiresChapter": 0,
|
|
||||||
"textHero": {
|
|
||||||
"id": "dialog.bjoernson.abgemacht_bier.textmainchar"
|
|
||||||
},
|
|
||||||
"recievesQuest": {
|
|
||||||
"questId": "kuehles_bier"
|
|
||||||
},
|
|
||||||
"heroSteps": [],
|
|
||||||
"npcSteps": [],
|
|
||||||
"abortsQuests": [],
|
|
||||||
"enablesTrade": false,
|
|
||||||
"onChosenTriggers": []
|
|
||||||
},
|
|
||||||
"bier_geliefert": {
|
|
||||||
"id": "bier_geliefert",
|
|
||||||
"requiresChapter": 0,
|
|
||||||
"requiresQuestOpen": {
|
|
||||||
"questId": "kuehles_bier"
|
|
||||||
},
|
|
||||||
"textHero": {
|
|
||||||
"id": "dialog.bjoernson.bier_geliefert.textmainchar"
|
|
||||||
},
|
|
||||||
"textNpc": {
|
|
||||||
"id": "dialog.bjoernson.bier_geliefert.textnpc"
|
|
||||||
},
|
|
||||||
"fulfillsQuest": {
|
|
||||||
"questId": "kuehles_bier"
|
|
||||||
},
|
|
||||||
"heroSteps": [],
|
|
||||||
"npcSteps": [],
|
|
||||||
"abortsQuests": [],
|
|
||||||
"enablesTrade": false,
|
|
||||||
"onChosenTriggers": []
|
|
||||||
},
|
|
||||||
"schwert_annehmen": {
|
|
||||||
"id": "schwert_annehmen",
|
|
||||||
"requiresChapter": 1,
|
|
||||||
"textHero": {
|
|
||||||
"id": "dialog.bjoernson.schwert_annehmen.textmainchar"
|
|
||||||
},
|
|
||||||
"heroSteps": [],
|
|
||||||
"npcSteps": [
|
|
||||||
{
|
|
||||||
"text": { "id": "dialog.bjoernson.schwert_annehmen.textnpc" },
|
|
||||||
"audio": { "key": "dialog.bjoernson.schwert_annehmen.textnpc" }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": { "id": "dialog.bjoernson.schwert_annehmen.textnpc.1" },
|
|
||||||
"audio": { "key": "dialog.bjoernson.schwert_annehmen.textnpc.1" }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": { "id": "dialog.bjoernson.schwert_annehmen.textnpc.2" },
|
|
||||||
"audio": { "key": "dialog.bjoernson.schwert_annehmen.textnpc.2" }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": { "id": "dialog.bjoernson.schwert_annehmen.textnpc.3" },
|
|
||||||
"audio": { "key": "dialog.bjoernson.schwert_annehmen.textnpc.3" }
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"abortsQuests": [],
|
|
||||||
"enablesTrade": false,
|
|
||||||
"onChosenTriggers": []
|
|
||||||
},
|
|
||||||
"ich_kuemmere_mich": {
|
|
||||||
"id": "ich_kuemmere_mich",
|
|
||||||
"requiresChapter": 0,
|
|
||||||
"textHero": {
|
|
||||||
"id": "dialog.bjoernson.ich_kuemmere_mich.textmainchar"
|
|
||||||
},
|
|
||||||
"textNpc": {
|
|
||||||
"id": "dialog.bjoernson.ich_kuemmere_mich.textnpc"
|
|
||||||
},
|
|
||||||
"recievesQuest": {
|
|
||||||
"questId": "blight_wuehler"
|
|
||||||
},
|
|
||||||
"heroSteps": [],
|
|
||||||
"npcSteps": [],
|
|
||||||
"abortsQuests": [],
|
|
||||||
"enablesTrade": false,
|
|
||||||
"onChosenTriggers": []
|
|
||||||
},
|
|
||||||
"erz_quest": {
|
|
||||||
"id": "erz_quest",
|
|
||||||
"requiresChapter": 0,
|
|
||||||
"textHero": {
|
|
||||||
"id": "dialog.bjoernson.erz_quest.textmainchar"
|
|
||||||
},
|
|
||||||
"recievesQuest": {
|
|
||||||
"questId": "magnetiterz"
|
|
||||||
},
|
|
||||||
"heroSteps": [],
|
|
||||||
"npcSteps": [],
|
|
||||||
"abortsQuests": [],
|
|
||||||
"enablesTrade": false,
|
|
||||||
"onChosenTriggers": []
|
|
||||||
},
|
|
||||||
"wie_schuerfern": {
|
|
||||||
"id": "wie_schuerfern",
|
|
||||||
"requiresChapter": 0,
|
|
||||||
"textHero": {
|
|
||||||
"id": "dialog.bjoernson.wie_schuerfern.textmainchar"
|
|
||||||
},
|
|
||||||
"heroSteps": [],
|
|
||||||
"npcSteps": [
|
|
||||||
{
|
|
||||||
"text": { "id": "dialog.bjoernson.wie_schuerfern.textnpc" },
|
|
||||||
"audio": { "key": "dialog.bjoernson.wie_schuerfern.textnpc" }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": { "id": "dialog.bjoernson.wie_schuerfern.textnpc.1" },
|
|
||||||
"audio": { "key": "dialog.bjoernson.wie_schuerfern.textnpc.1" }
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"abortsQuests": [],
|
|
||||||
"enablesTrade": false,
|
|
||||||
"onChosenTriggers": []
|
|
||||||
},
|
|
||||||
"danke_bjoernson": {
|
|
||||||
"id": "danke_bjoernson",
|
|
||||||
"requiresChapter": 0,
|
|
||||||
"textHero": {
|
|
||||||
"id": "dialog.bjoernson.danke_bjoernson.textmainchar"
|
|
||||||
},
|
|
||||||
"textNpc": {
|
|
||||||
"id": "dialog.bjoernson.danke_bjoernson.textnpc"
|
|
||||||
},
|
|
||||||
"heroSteps": [],
|
|
||||||
"npcSteps": [],
|
|
||||||
"abortsQuests": [],
|
|
||||||
"enablesTrade": false,
|
|
||||||
"onChosenTriggers": []
|
|
||||||
},
|
|
||||||
"quests_erledigt": {
|
|
||||||
"id": "quests_erledigt",
|
|
||||||
"requiresChapter": 0,
|
|
||||||
"requiresQuestComplete": {
|
|
||||||
"questId": "blight_wuehler"
|
|
||||||
},
|
|
||||||
"textHero": {
|
|
||||||
"id": "dialog.bjoernson.quests_erledigt.textmainchar"
|
|
||||||
},
|
|
||||||
"textNpc": {
|
|
||||||
"id": "dialog.bjoernson.quests_erledigt.textnpc"
|
|
||||||
},
|
|
||||||
"heroSteps": [],
|
|
||||||
"npcSteps": [],
|
|
||||||
"abortsQuests": [],
|
|
||||||
"enablesTrade": false,
|
|
||||||
"onChosenTriggers": []
|
|
||||||
},
|
|
||||||
"ich_bin_zaeh": {
|
|
||||||
"id": "ich_bin_zaeh",
|
|
||||||
"requiresChapter": 0,
|
|
||||||
"textHero": {
|
|
||||||
"id": "dialog.bjoernson.ich_bin_zaeh.textmainchar"
|
|
||||||
},
|
|
||||||
"textNpc": {
|
|
||||||
"id": "dialog.bjoernson.ich_bin_zaeh.textnpc"
|
|
||||||
},
|
|
||||||
"heroSteps": [],
|
|
||||||
"npcSteps": [],
|
|
||||||
"abortsQuests": [],
|
|
||||||
"enablesTrade": false,
|
|
||||||
"onChosenTriggers": []
|
|
||||||
},
|
|
||||||
"wie_stehts_abmachung": {
|
|
||||||
"id": "wie_stehts_abmachung",
|
|
||||||
"requiresChapter": 0,
|
|
||||||
"textHero": {
|
|
||||||
"id": "dialog.bjoernson.wie_stehts_abmachung.textmainchar"
|
|
||||||
},
|
|
||||||
"textNpc": {
|
|
||||||
"id": "dialog.bjoernson.wie_stehts_abmachung.textnpc"
|
|
||||||
},
|
|
||||||
"heroSteps": [],
|
|
||||||
"npcSteps": [],
|
|
||||||
"abortsQuests": [],
|
|
||||||
"enablesTrade": false,
|
|
||||||
"onChosenTriggers": []
|
|
||||||
},
|
|
||||||
"nur_die_waffe": {
|
|
||||||
"id": "nur_die_waffe",
|
|
||||||
"requiresChapter": 0,
|
|
||||||
"textHero": {
|
|
||||||
"id": "dialog.bjoernson.nur_die_waffe.textmainchar"
|
|
||||||
},
|
|
||||||
"textNpc": {
|
|
||||||
"id": "dialog.bjoernson.nur_die_waffe.textnpc"
|
|
||||||
},
|
|
||||||
"heroSteps": [],
|
|
||||||
"npcSteps": [],
|
|
||||||
"abortsQuests": [],
|
|
||||||
"enablesTrade": false,
|
|
||||||
"onChosenTriggers": []
|
|
||||||
},
|
|
||||||
"selber_lernen": {
|
|
||||||
"id": "selber_lernen",
|
|
||||||
"requiresChapter": 0,
|
|
||||||
"textHero": {
|
|
||||||
"id": "dialog.bjoernson.selber_lernen.textmainchar"
|
|
||||||
},
|
|
||||||
"textNpc": {
|
|
||||||
"id": "dialog.bjoernson.selber_lernen.textnpc"
|
|
||||||
},
|
|
||||||
"heroSteps": [],
|
|
||||||
"npcSteps": [],
|
|
||||||
"abortsQuests": [],
|
|
||||||
"enablesTrade": true,
|
|
||||||
"onChosenTriggers": []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dialogRoots": [
|
|
||||||
"begruessung",
|
|
||||||
"bier_geliefert",
|
|
||||||
"schwert_annehmen",
|
|
||||||
"quests_erledigt"
|
|
||||||
],
|
|
||||||
"dialogNext": {
|
|
||||||
"begruessung": [
|
|
||||||
"wer_bist_du",
|
|
||||||
"bier_angebot"
|
|
||||||
],
|
|
||||||
"wer_bist_du": [
|
|
||||||
"aus_dem_norden",
|
|
||||||
"bier_angebot"
|
|
||||||
],
|
|
||||||
"aus_dem_norden": [
|
|
||||||
"bier_angebot"
|
|
||||||
],
|
|
||||||
"bier_angebot": [
|
|
||||||
"abgemacht_bier"
|
|
||||||
],
|
|
||||||
"schwert_annehmen": [
|
|
||||||
"ich_kuemmere_mich"
|
|
||||||
],
|
|
||||||
"ich_kuemmere_mich": [
|
|
||||||
"erz_quest",
|
|
||||||
"wie_schuerfern",
|
|
||||||
"danke_bjoernson"
|
|
||||||
],
|
|
||||||
"erz_quest": [
|
|
||||||
"wie_schuerfern",
|
|
||||||
"danke_bjoernson"
|
|
||||||
],
|
|
||||||
"quests_erledigt": [
|
|
||||||
"ich_bin_zaeh"
|
|
||||||
],
|
|
||||||
"ich_bin_zaeh": [
|
|
||||||
"wie_stehts_abmachung"
|
|
||||||
],
|
|
||||||
"wie_stehts_abmachung": [
|
|
||||||
"nur_die_waffe",
|
|
||||||
"selber_lernen"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"dialogDisables": {
|
|
||||||
"abgemacht_bier": [
|
|
||||||
"begruessung",
|
|
||||||
"wer_bist_du",
|
|
||||||
"aus_dem_norden",
|
|
||||||
"bier_angebot"
|
|
||||||
],
|
|
||||||
"nur_die_waffe": [
|
|
||||||
"selber_lernen",
|
|
||||||
"quests_erledigt",
|
|
||||||
"ich_bin_zaeh",
|
|
||||||
"wie_stehts_abmachung"
|
|
||||||
],
|
|
||||||
"selber_lernen": [
|
|
||||||
"nur_die_waffe",
|
|
||||||
"quests_erledigt",
|
|
||||||
"ich_bin_zaeh",
|
|
||||||
"wie_stehts_abmachung"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"defaultMessages": {},
|
"defaultMessages": {},
|
||||||
"routines": [
|
"routines": [
|
||||||
{
|
{
|
||||||
"name": "chapter0",
|
"name": "patrol_pass",
|
||||||
"blocks": [
|
"blocks": [
|
||||||
{
|
{
|
||||||
"startHour": 0,
|
"startHour": 0,
|
||||||
"endHour": 24,
|
"endHour": 23,
|
||||||
"activity": {
|
"activity": {
|
||||||
"type": "STAND",
|
"type": "STAND",
|
||||||
"position": {
|
"position": {
|
||||||
"x": 0.0,
|
"x": -7.9,
|
||||||
"y": 1.0,
|
"y": 2.0,
|
||||||
"z": 0.0
|
"z": -1.0,
|
||||||
|
"yaw": 0.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -366,10 +27,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"characterId": "bjoernson",
|
"characterId": "bjoernson",
|
||||||
"name": {
|
"modelPath": "Characters/Models/bjoernson.j3o",
|
||||||
"id": "bjoernson.name"
|
|
||||||
},
|
|
||||||
"modelPath": "Models/Chars/bjoernson.j3o",
|
|
||||||
"animSetPath": "human",
|
"animSetPath": "human",
|
||||||
"type": "NPC"
|
"type": "NPC"
|
||||||
}
|
}
|
||||||
@@ -12,6 +12,19 @@
|
|||||||
"openManaRegeneration": 0,
|
"openManaRegeneration": 0,
|
||||||
"openStaminaRegeneration": 0,
|
"openStaminaRegeneration": 0,
|
||||||
"restorationValue": 10,
|
"restorationValue": 10,
|
||||||
|
"abilities": {
|
||||||
|
"lvlMagic": 1,
|
||||||
|
"lvlStaffCombat": 1,
|
||||||
|
"lvlSwordsmanship": 1,
|
||||||
|
"lvlArchery": 1,
|
||||||
|
"lvlHeavyWeapons": 1,
|
||||||
|
"lvlCrossbow": 1,
|
||||||
|
"lvlThievery": 0,
|
||||||
|
"lvlAlchemy": 0,
|
||||||
|
"lvlEngineering": 0,
|
||||||
|
"lvlSmithery": 0,
|
||||||
|
"lvlEnchanting": 0
|
||||||
|
},
|
||||||
"playedMonologueIds": [],
|
"playedMonologueIds": [],
|
||||||
"firedTriggerIds": [],
|
"firedTriggerIds": [],
|
||||||
"visitedZoneIds": [],
|
"visitedZoneIds": [],
|
||||||
|
|||||||
@@ -1,442 +1,12 @@
|
|||||||
{
|
{
|
||||||
"status": "NEUTRAL",
|
"status": "NEUTRAL",
|
||||||
"trader": false,
|
"trader": false,
|
||||||
"dialogOptions": {
|
"dialogOptions": {},
|
||||||
"begruessung": {
|
"dialogRoots": [],
|
||||||
"id": "begruessung",
|
"dialogNext": {},
|
||||||
"requiresChapter": 0,
|
"dialogDisables": {},
|
||||||
"textHero": {
|
|
||||||
"id": "dialog.silas.begruessung.textmainchar"
|
|
||||||
},
|
|
||||||
"textNpc": {
|
|
||||||
"id": "dialog.silas.begruessung.textnpc"
|
|
||||||
},
|
|
||||||
"abortsQuests": [],
|
|
||||||
"enablesTrade": false,
|
|
||||||
"onChosenTriggers": []
|
|
||||||
},
|
|
||||||
"antwort_sklave": {
|
|
||||||
"id": "antwort_sklave",
|
|
||||||
"requiresChapter": 0,
|
|
||||||
"textHero": {
|
|
||||||
"id": "dialog.silas.antwort_sklave.textmainchar"
|
|
||||||
},
|
|
||||||
"textNpc": {
|
|
||||||
"id": "dialog.silas.antwort_sklave.textnpc"
|
|
||||||
},
|
|
||||||
"abortsQuests": [],
|
|
||||||
"enablesTrade": false,
|
|
||||||
"onChosenTriggers": []
|
|
||||||
},
|
|
||||||
"antwort_gedaechtnislos": {
|
|
||||||
"id": "antwort_gedaechtnislos",
|
|
||||||
"requiresChapter": 0,
|
|
||||||
"textHero": {
|
|
||||||
"id": "dialog.silas.antwort_gedaechtnislos.textmainchar"
|
|
||||||
},
|
|
||||||
"textNpc": {
|
|
||||||
"id": "dialog.silas.antwort_gedaechtnislos.textnpc"
|
|
||||||
},
|
|
||||||
"abortsQuests": [],
|
|
||||||
"enablesTrade": false,
|
|
||||||
"onChosenTriggers": []
|
|
||||||
},
|
|
||||||
"antwort_gesandter": {
|
|
||||||
"id": "antwort_gesandter",
|
|
||||||
"requiresChapter": 0,
|
|
||||||
"textHero": {
|
|
||||||
"id": "dialog.silas.antwort_gesandter.textmainchar"
|
|
||||||
},
|
|
||||||
"textNpc": {
|
|
||||||
"id": "dialog.silas.antwort_gesandter.textnpc"
|
|
||||||
},
|
|
||||||
"abortsQuests": [],
|
|
||||||
"enablesTrade": false,
|
|
||||||
"onChosenTriggers": []
|
|
||||||
},
|
|
||||||
"wer_bist_du": {
|
|
||||||
"id": "wer_bist_du",
|
|
||||||
"requiresChapter": 0,
|
|
||||||
"textHero": {
|
|
||||||
"id": "dialog.silas.wer_bist_du.textmainchar"
|
|
||||||
},
|
|
||||||
"textNpc": {
|
|
||||||
"id": "dialog.silas.wer_bist_du.textnpc"
|
|
||||||
},
|
|
||||||
"abortsQuests": [],
|
|
||||||
"enablesTrade": false,
|
|
||||||
"onChosenTriggers": []
|
|
||||||
},
|
|
||||||
"wer_seid_ihr": {
|
|
||||||
"id": "wer_seid_ihr",
|
|
||||||
"requiresChapter": 0,
|
|
||||||
"textHero": {
|
|
||||||
"id": "dialog.silas.wer_seid_ihr.textmainchar"
|
|
||||||
},
|
|
||||||
"textNpc": {
|
|
||||||
"id": "dialog.silas.wer_seid_ihr.textnpc"
|
|
||||||
},
|
|
||||||
"abortsQuests": [],
|
|
||||||
"enablesTrade": false,
|
|
||||||
"onChosenTriggers": []
|
|
||||||
},
|
|
||||||
"was_leuchten": {
|
|
||||||
"id": "was_leuchten",
|
|
||||||
"requiresChapter": 0,
|
|
||||||
"textHero": {
|
|
||||||
"id": "dialog.silas.was_leuchten.textmainchar"
|
|
||||||
},
|
|
||||||
"textNpc": {
|
|
||||||
"id": "dialog.silas.was_leuchten.textnpc"
|
|
||||||
},
|
|
||||||
"abortsQuests": [],
|
|
||||||
"enablesTrade": false,
|
|
||||||
"onChosenTriggers": []
|
|
||||||
},
|
|
||||||
"ausruestung": {
|
|
||||||
"id": "ausruestung",
|
|
||||||
"requiresChapter": 0,
|
|
||||||
"textHero": {
|
|
||||||
"id": "dialog.silas.ausruestung.textmainchar"
|
|
||||||
},
|
|
||||||
"textNpc": {
|
|
||||||
"id": "dialog.silas.ausruestung.textnpc"
|
|
||||||
},
|
|
||||||
"abortsQuests": [],
|
|
||||||
"enablesTrade": false,
|
|
||||||
"onChosenTriggers": []
|
|
||||||
},
|
|
||||||
"gibts_was_rum": {
|
|
||||||
"id": "gibts_was_rum",
|
|
||||||
"requiresChapter": 0,
|
|
||||||
"textHero": {
|
|
||||||
"id": "dialog.silas.gibts_was_rum.textmainchar"
|
|
||||||
},
|
|
||||||
"textNpc": {
|
|
||||||
"id": "dialog.silas.gibts_was_rum.textnpc"
|
|
||||||
},
|
|
||||||
"abortsQuests": [],
|
|
||||||
"enablesTrade": false,
|
|
||||||
"onChosenTriggers": []
|
|
||||||
},
|
|
||||||
"tipp_rum": {
|
|
||||||
"id": "tipp_rum",
|
|
||||||
"requiresChapter": 0,
|
|
||||||
"textHero": {
|
|
||||||
"id": "dialog.silas.tipp_rum.textmainchar"
|
|
||||||
},
|
|
||||||
"textNpc": {
|
|
||||||
"id": "dialog.silas.tipp_rum.textnpc"
|
|
||||||
},
|
|
||||||
"abortsQuests": [],
|
|
||||||
"enablesTrade": false,
|
|
||||||
"onChosenTriggers": []
|
|
||||||
},
|
|
||||||
"welchen_weg": {
|
|
||||||
"id": "welchen_weg",
|
|
||||||
"requiresChapter": 0,
|
|
||||||
"textHero": {
|
|
||||||
"id": "dialog.silas.welchen_weg.textmainchar"
|
|
||||||
},
|
|
||||||
"textNpc": {
|
|
||||||
"id": "dialog.silas.welchen_weg.textnpc"
|
|
||||||
},
|
|
||||||
"abortsQuests": [],
|
|
||||||
"enablesTrade": false,
|
|
||||||
"onChosenTriggers": []
|
|
||||||
},
|
|
||||||
"abschluss_phase1": {
|
|
||||||
"id": "abschluss_phase1",
|
|
||||||
"requiresChapter": 0,
|
|
||||||
"textHero": {
|
|
||||||
"id": "dialog.silas.abschluss_phase1.textmainchar"
|
|
||||||
},
|
|
||||||
"textNpc": {
|
|
||||||
"id": "dialog.silas.abschluss_phase1.textnpc"
|
|
||||||
},
|
|
||||||
"abortsQuests": [],
|
|
||||||
"enablesTrade": false,
|
|
||||||
"onChosenTriggers": []
|
|
||||||
},
|
|
||||||
"rueckkehr_beobachtet": {
|
|
||||||
"id": "rueckkehr_beobachtet",
|
|
||||||
"requiresChapter": 0,
|
|
||||||
"textHero": {
|
|
||||||
"id": "dialog.silas.rueckkehr_beobachtet.textmainchar"
|
|
||||||
},
|
|
||||||
"textNpc": {
|
|
||||||
"id": "dialog.silas.rueckkehr_beobachtet.textnpc"
|
|
||||||
},
|
|
||||||
"abortsQuests": [],
|
|
||||||
"enablesTrade": false,
|
|
||||||
"onChosenTriggers": []
|
|
||||||
},
|
|
||||||
"ich_bin_zaeh": {
|
|
||||||
"id": "ich_bin_zaeh",
|
|
||||||
"requiresChapter": 0,
|
|
||||||
"textHero": {
|
|
||||||
"id": "dialog.silas.ich_bin_zaeh.textmainchar"
|
|
||||||
},
|
|
||||||
"textNpc": {
|
|
||||||
"id": "dialog.silas.ich_bin_zaeh.textnpc"
|
|
||||||
},
|
|
||||||
"abortsQuests": [],
|
|
||||||
"enablesTrade": false,
|
|
||||||
"onChosenTriggers": []
|
|
||||||
},
|
|
||||||
"weg_ins_innere": {
|
|
||||||
"id": "weg_ins_innere",
|
|
||||||
"requiresChapter": 0,
|
|
||||||
"textHero": {
|
|
||||||
"id": "dialog.silas.weg_ins_innere.textmainchar"
|
|
||||||
},
|
|
||||||
"textNpc": {
|
|
||||||
"id": "dialog.silas.weg_ins_innere.textnpc"
|
|
||||||
},
|
|
||||||
"abortsQuests": [],
|
|
||||||
"enablesTrade": false,
|
|
||||||
"onChosenTriggers": []
|
|
||||||
},
|
|
||||||
"was_ueber_insel": {
|
|
||||||
"id": "was_ueber_insel",
|
|
||||||
"requiresChapter": 0,
|
|
||||||
"textHero": {
|
|
||||||
"id": "dialog.silas.was_ueber_insel.textmainchar"
|
|
||||||
},
|
|
||||||
"textNpc": {
|
|
||||||
"id": "dialog.silas.was_ueber_insel.textnpc"
|
|
||||||
},
|
|
||||||
"abortsQuests": [],
|
|
||||||
"enablesTrade": false,
|
|
||||||
"onChosenTriggers": []
|
|
||||||
},
|
|
||||||
"info_osten": {
|
|
||||||
"id": "info_osten",
|
|
||||||
"requiresChapter": 0,
|
|
||||||
"textHero": {
|
|
||||||
"id": "dialog.silas.info_osten.textmainchar"
|
|
||||||
},
|
|
||||||
"textNpc": {
|
|
||||||
"id": "dialog.silas.info_osten.textnpc"
|
|
||||||
},
|
|
||||||
"abortsQuests": [],
|
|
||||||
"enablesTrade": false,
|
|
||||||
"onChosenTriggers": []
|
|
||||||
},
|
|
||||||
"info_westen": {
|
|
||||||
"id": "info_westen",
|
|
||||||
"requiresChapter": 0,
|
|
||||||
"textHero": {
|
|
||||||
"id": "dialog.silas.info_westen.textmainchar"
|
|
||||||
},
|
|
||||||
"textNpc": {
|
|
||||||
"id": "dialog.silas.info_westen.textnpc"
|
|
||||||
},
|
|
||||||
"abortsQuests": [],
|
|
||||||
"enablesTrade": false,
|
|
||||||
"onChosenTriggers": []
|
|
||||||
},
|
|
||||||
"info_dorf": {
|
|
||||||
"id": "info_dorf",
|
|
||||||
"requiresChapter": 0,
|
|
||||||
"textHero": {
|
|
||||||
"id": "dialog.silas.info_dorf.textmainchar"
|
|
||||||
},
|
|
||||||
"textNpc": {
|
|
||||||
"id": "dialog.silas.info_dorf.textnpc"
|
|
||||||
},
|
|
||||||
"abortsQuests": [],
|
|
||||||
"enablesTrade": false,
|
|
||||||
"onChosenTriggers": []
|
|
||||||
},
|
|
||||||
"info_pass": {
|
|
||||||
"id": "info_pass",
|
|
||||||
"requiresChapter": 0,
|
|
||||||
"textHero": {
|
|
||||||
"id": "dialog.silas.info_pass.textmainchar"
|
|
||||||
},
|
|
||||||
"textNpc": {
|
|
||||||
"id": "dialog.silas.info_pass.textnpc"
|
|
||||||
},
|
|
||||||
"abortsQuests": [],
|
|
||||||
"enablesTrade": false,
|
|
||||||
"onChosenTriggers": []
|
|
||||||
},
|
|
||||||
"ich_weiss_genug": {
|
|
||||||
"id": "ich_weiss_genug",
|
|
||||||
"requiresChapter": 0,
|
|
||||||
"textHero": {
|
|
||||||
"id": "dialog.silas.ich_weiss_genug.textmainchar"
|
|
||||||
},
|
|
||||||
"textNpc": {
|
|
||||||
"id": "dialog.silas.ich_weiss_genug.textnpc"
|
|
||||||
},
|
|
||||||
"abortsQuests": [],
|
|
||||||
"enablesTrade": false,
|
|
||||||
"onChosenTriggers": []
|
|
||||||
},
|
|
||||||
"karte_zeichnen": {
|
|
||||||
"id": "karte_zeichnen",
|
|
||||||
"requiresChapter": 0,
|
|
||||||
"textHero": {
|
|
||||||
"id": "dialog.silas.karte_zeichnen.textmainchar"
|
|
||||||
},
|
|
||||||
"textNpc": {
|
|
||||||
"id": "dialog.silas.karte_zeichnen.textnpc"
|
|
||||||
},
|
|
||||||
"abortsQuests": [],
|
|
||||||
"enablesTrade": false,
|
|
||||||
"onChosenTriggers": []
|
|
||||||
},
|
|
||||||
"dankeschoen": {
|
|
||||||
"id": "dankeschoen",
|
|
||||||
"requiresChapter": 0,
|
|
||||||
"textHero": {
|
|
||||||
"id": "dialog.silas.dankeschoen.textmainchar"
|
|
||||||
},
|
|
||||||
"textNpc": {
|
|
||||||
"id": "dialog.silas.dankeschoen.textnpc"
|
|
||||||
},
|
|
||||||
"abortsQuests": [],
|
|
||||||
"enablesTrade": false,
|
|
||||||
"onChosenTriggers": []
|
|
||||||
},
|
|
||||||
"erstaunt_ruhe": {
|
|
||||||
"id": "erstaunt_ruhe",
|
|
||||||
"requiresChapter": 0,
|
|
||||||
"textHero": {
|
|
||||||
"id": "dialog.silas.erstaunt_ruhe.textmainchar"
|
|
||||||
},
|
|
||||||
"textNpc": {
|
|
||||||
"id": "dialog.silas.erstaunt_ruhe.textnpc"
|
|
||||||
},
|
|
||||||
"abortsQuests": [],
|
|
||||||
"enablesTrade": false,
|
|
||||||
"onChosenTriggers": []
|
|
||||||
},
|
|
||||||
"abschluss_letzte_worte": {
|
|
||||||
"id": "abschluss_letzte_worte",
|
|
||||||
"requiresChapter": 0,
|
|
||||||
"textHero": {
|
|
||||||
"id": "dialog.silas.abschluss_letzte_worte.textmainchar"
|
|
||||||
},
|
|
||||||
"textNpc": {
|
|
||||||
"id": "dialog.silas.abschluss_letzte_worte.textnpc"
|
|
||||||
},
|
|
||||||
"abortsQuests": [],
|
|
||||||
"enablesTrade": false,
|
|
||||||
"onChosenTriggers": []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dialogRoots": [
|
|
||||||
"begruessung"
|
|
||||||
],
|
|
||||||
"dialogNext": {
|
|
||||||
"begruessung": [
|
|
||||||
"antwort_sklave",
|
|
||||||
"antwort_gedaechtnislos",
|
|
||||||
"antwort_gesandter"
|
|
||||||
],
|
|
||||||
"antwort_sklave": [
|
|
||||||
"wer_bist_du",
|
|
||||||
"was_leuchten",
|
|
||||||
"ausruestung",
|
|
||||||
"gibts_was_rum",
|
|
||||||
"welchen_weg"
|
|
||||||
],
|
|
||||||
"antwort_gedaechtnislos": [
|
|
||||||
"wer_bist_du",
|
|
||||||
"was_leuchten",
|
|
||||||
"ausruestung",
|
|
||||||
"tipp_rum",
|
|
||||||
"welchen_weg"
|
|
||||||
],
|
|
||||||
"antwort_gesandter": [
|
|
||||||
"wer_bist_du",
|
|
||||||
"was_leuchten",
|
|
||||||
"ausruestung",
|
|
||||||
"tipp_rum",
|
|
||||||
"welchen_weg"
|
|
||||||
],
|
|
||||||
"wer_bist_du": [
|
|
||||||
"wer_seid_ihr"
|
|
||||||
],
|
|
||||||
"welchen_weg": [
|
|
||||||
"abschluss_phase1"
|
|
||||||
],
|
|
||||||
"abschluss_phase1": [
|
|
||||||
"rueckkehr_beobachtet"
|
|
||||||
],
|
|
||||||
"rueckkehr_beobachtet": [
|
|
||||||
"ich_bin_zaeh"
|
|
||||||
],
|
|
||||||
"ich_bin_zaeh": [
|
|
||||||
"weg_ins_innere",
|
|
||||||
"was_ueber_insel"
|
|
||||||
],
|
|
||||||
"was_ueber_insel": [
|
|
||||||
"info_osten",
|
|
||||||
"info_westen",
|
|
||||||
"info_dorf",
|
|
||||||
"info_pass",
|
|
||||||
"ich_weiss_genug"
|
|
||||||
],
|
|
||||||
"info_pass": [
|
|
||||||
"karte_zeichnen"
|
|
||||||
],
|
|
||||||
"ich_weiss_genug": [
|
|
||||||
"karte_zeichnen"
|
|
||||||
],
|
|
||||||
"karte_zeichnen": [
|
|
||||||
"dankeschoen"
|
|
||||||
],
|
|
||||||
"dankeschoen": [
|
|
||||||
"erstaunt_ruhe"
|
|
||||||
],
|
|
||||||
"erstaunt_ruhe": [
|
|
||||||
"abschluss_letzte_worte"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"dialogDisables": {
|
|
||||||
"antwort_sklave": [
|
|
||||||
"antwort_gedaechtnislos",
|
|
||||||
"antwort_gesandter"
|
|
||||||
],
|
|
||||||
"antwort_gedaechtnislos": [
|
|
||||||
"antwort_sklave",
|
|
||||||
"antwort_gesandter"
|
|
||||||
],
|
|
||||||
"antwort_gesandter": [
|
|
||||||
"antwort_sklave",
|
|
||||||
"antwort_gedaechtnislos"
|
|
||||||
],
|
|
||||||
"ich_weiss_genug": [
|
|
||||||
"info_osten",
|
|
||||||
"info_westen",
|
|
||||||
"info_dorf",
|
|
||||||
"info_pass"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"defaultMessages": {},
|
"defaultMessages": {},
|
||||||
"routines": [
|
"routines": [],
|
||||||
{
|
|
||||||
"name": "chapter0",
|
|
||||||
"blocks": [
|
|
||||||
{
|
|
||||||
"startHour": 0,
|
|
||||||
"endHour": 24,
|
|
||||||
"activity": {
|
|
||||||
"type": "STAND",
|
|
||||||
"position": {
|
|
||||||
"x": -17.0,
|
|
||||||
"y": 1.2,
|
|
||||||
"z": -1315.6
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"characterId": "silas",
|
"characterId": "silas",
|
||||||
"name": {
|
"name": {
|
||||||
"id": "silas.name"
|
"id": "silas.name"
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ import lombok.Setter;
|
|||||||
public abstract class GameCharacter {
|
public abstract class GameCharacter {
|
||||||
/** Eindeutige ID — entspricht dem Dateinamen ohne Extension. */
|
/** Eindeutige ID — entspricht dem Dateinamen ohne Extension. */
|
||||||
private String characterId;
|
private String characterId;
|
||||||
/** Anzeigename des Charakters — wird zur Laufzeit per TextReference aufgelöst. */
|
|
||||||
private TextReference name;
|
|
||||||
/** Relativer Pfad zum .j3o-Modell (relativ zu blight-assets/src/main/resources). */
|
/** Relativer Pfad zum .j3o-Modell (relativ zu blight-assets/src/main/resources). */
|
||||||
private String modelPath;
|
private String modelPath;
|
||||||
/** Relativer Pfad zum .j3o-Animations-Set (in animations/sets/). */
|
/** Relativer Pfad zum .j3o-Animations-Set (in animations/sets/). */
|
||||||
|
|||||||
@@ -26,6 +26,19 @@ public class NPC extends GameCharacter implements Interactable {
|
|||||||
|
|
||||||
private List<Item> items;
|
private List<Item> items;
|
||||||
|
|
||||||
|
/** Basis-HP; 0 = Spielwelt-Standardwert (100). */
|
||||||
|
private int maxHp;
|
||||||
|
/** Basis-Mana; 0 = Spielwelt-Standardwert (100). */
|
||||||
|
private int maxMana;
|
||||||
|
/** Basis-Ausdauer; 0 = Spielwelt-Standardwert (100). */
|
||||||
|
private int maxStamina;
|
||||||
|
|
||||||
|
/** Item-ID der primären Nahkampfwaffe (am Rücken getragen). Null = keine Waffe. */
|
||||||
|
private String equippedWeaponId;
|
||||||
|
|
||||||
|
/** Item-ID des Schilds (am Rücken getragen; nur bei Schwertkämpfern sinnvoll). Null = kein Schild. */
|
||||||
|
private String equippedShieldId;
|
||||||
|
|
||||||
// ── Flache Dialog-Struktur (serialisiert) ─────────────────────────────────
|
// ── Flache Dialog-Struktur (serialisiert) ─────────────────────────────────
|
||||||
|
|
||||||
/** Alle Dialog-Optionen dieses NPCs, flach nach ID indiziert. */
|
/** Alle Dialog-Optionen dieses NPCs, flach nach ID indiziert. */
|
||||||
@@ -90,13 +103,12 @@ public class NPC extends GameCharacter implements Interactable {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getDisplayText() {
|
public String getDisplayText() {
|
||||||
return TextRegistry.resolve(getName(), getCharacterId() != null ? getCharacterId() : "NPC");
|
String key = getLabelKey();
|
||||||
|
return TextRegistry.resolve(new TextReference(key), getCharacterId() != null ? getCharacterId() : "NPC");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getLabelKey() {
|
public String getLabelKey() {
|
||||||
TextReference nameRef = getName();
|
|
||||||
if (nameRef != null && nameRef.id() != null && !nameRef.id().isBlank()) return nameRef.id();
|
|
||||||
return getCharacterId() != null ? "npc." + getCharacterId() + ".name" : "";
|
return getCharacterId() != null ? "npc." + getCharacterId() + ".name" : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,17 +4,14 @@ package de.blight.common.model;
|
|||||||
* Zeitblock innerhalb eines Tagesablaufs.
|
* Zeitblock innerhalb eines Tagesablaufs.
|
||||||
*
|
*
|
||||||
* {@code startHour} und {@code endHour} sind ganzzahlige Stunden (0–23).
|
* {@code startHour} und {@code endHour} sind ganzzahlige Stunden (0–23).
|
||||||
|
* {@code endHour} ist inklusiv: ein Block mit endHour=22 läuft bis 22:59:59.
|
||||||
* Ein Block, der über Mitternacht geht (z.B. 22–06), ist erlaubt:
|
* Ein Block, der über Mitternacht geht (z.B. 22–06), ist erlaubt:
|
||||||
* in diesem Fall ist {@code endHour < startHour}.
|
* in diesem Fall ist {@code endHour < startHour}.
|
||||||
*
|
|
||||||
* Die Dauer berechnet sich als:
|
|
||||||
* endHour >= startHour → endHour - startHour
|
|
||||||
* endHour < startHour → 24 - startHour + endHour
|
|
||||||
*/
|
*/
|
||||||
public class RoutineBlock {
|
public class RoutineBlock {
|
||||||
|
|
||||||
private int startHour; // 0-23
|
private int startHour; // 0-23
|
||||||
private int endHour; // 0-23 (exklusiv: Block endet um endHour:00)
|
private int endHour; // 0-23 (inklusiv: Block endet um endHour:59:59)
|
||||||
private RoutineActivity activity;
|
private RoutineActivity activity;
|
||||||
|
|
||||||
public RoutineBlock() {}
|
public RoutineBlock() {}
|
||||||
@@ -34,23 +31,22 @@ public class RoutineBlock {
|
|||||||
public RoutineActivity getActivity() { return activity; }
|
public RoutineActivity getActivity() { return activity; }
|
||||||
public void setActivity(RoutineActivity a) { this.activity = a; }
|
public void setActivity(RoutineActivity a) { this.activity = a; }
|
||||||
|
|
||||||
/** Anzahl der durch diesen Block abgedeckten Stunden. */
|
/** Anzahl der durch diesen Block abgedeckten Stunden (inkl. endHour). */
|
||||||
public int durationHours() {
|
public int durationHours() {
|
||||||
if (endHour > startHour) return endHour - startHour;
|
if (endHour >= startHour) return endHour - startHour + 1;
|
||||||
if (endHour < startHour) return 24 - startHour + endHour;
|
return 24 - startHour + endHour + 1;
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Gibt true wenn die gegebene Stunde (0-23) in diesem Block liegt. */
|
/** Gibt true wenn die gegebene Stunde (0-23) in diesem Block liegt (endHour inklusiv). */
|
||||||
public boolean covers(int hour) {
|
public boolean covers(int hour) {
|
||||||
if (endHour > startHour) return hour >= startHour && hour < endHour;
|
if (endHour >= startHour) return hour >= startHour && hour <= endHour;
|
||||||
// Über-Mitternacht-Block
|
// Über-Mitternacht-Block
|
||||||
return hour >= startHour || hour < endHour;
|
return hour >= startHour || hour <= endHour;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Formatiert den Block als "HH:00 – HH:00 | Aktivität". */
|
/** Formatiert den Block als "HH:00 – HH:59 | Aktivität". */
|
||||||
public String displayLabel() {
|
public String displayLabel() {
|
||||||
String act = activity != null ? activity.summary() : "—";
|
String act = activity != null ? activity.summary() : "—";
|
||||||
return String.format("%02d:00 – %02d:00 %s", startHour, endHour, act);
|
return String.format("%02d:00 – %02d:59 %s", startHour, endHour, act);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1276,6 +1276,17 @@ public class AnimPreviewState extends BaseAppState {
|
|||||||
assets.registerLocator(fbmDir.getAbsolutePath(), com.jme3.asset.plugins.FileLocator.class);
|
assets.registerLocator(fbmDir.getAbsolutePath(), com.jme3.asset.plugins.FileLocator.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Eingebettete Texturen VOR dem Wrench-Load extrahieren: Wrench ersetzt
|
||||||
|
// Texturen mit nicht auffindbaren Pfaden sofort durch Platzhalter; danach
|
||||||
|
// ist der Original-Dateiname nicht mehr aus dem Modell ablesbar.
|
||||||
|
java.io.File preExtractDir = extractEmbeddedTexturesPreLoad(file);
|
||||||
|
if (preExtractDir != null) {
|
||||||
|
assets.registerLocator(preExtractDir.getAbsolutePath(), com.jme3.asset.plugins.FileLocator.class);
|
||||||
|
LOG.info("[CharImport] {} Textur(en) vorab extrahiert nach '{}'.",
|
||||||
|
preExtractDir.list() != null ? preExtractDir.list().length : 0,
|
||||||
|
preExtractDir.getAbsolutePath());
|
||||||
|
}
|
||||||
|
|
||||||
com.github.stephengold.wrench.LwjglAssetKey key =
|
com.github.stephengold.wrench.LwjglAssetKey key =
|
||||||
new com.github.stephengold.wrench.LwjglAssetKey(file.getName());
|
new com.github.stephengold.wrench.LwjglAssetKey(file.getName());
|
||||||
key.setVerboseLogging(true);
|
key.setVerboseLogging(true);
|
||||||
@@ -1289,6 +1300,8 @@ public class AnimPreviewState extends BaseAppState {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Post-Load-Fallback: sollte durch Pre-Load nicht mehr nötig sein,
|
||||||
|
// aber schadet nicht als Sicherheitsnetz.
|
||||||
java.io.File embeddedDir = extractEmbeddedTextures(file, model);
|
java.io.File embeddedDir = extractEmbeddedTextures(file, model);
|
||||||
if (embeddedDir != null) {
|
if (embeddedDir != null) {
|
||||||
assets.registerLocator(embeddedDir.getAbsolutePath(), com.jme3.asset.plugins.FileLocator.class);
|
assets.registerLocator(embeddedDir.getAbsolutePath(), com.jme3.asset.plugins.FileLocator.class);
|
||||||
@@ -1578,8 +1591,11 @@ public class AnimPreviewState extends BaseAppState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sucht eingebettete PNG-Texturen im FBX-Binär (Dateiname → PNG-Magic → IEND)
|
* Sucht eingebettete PNG- und JPEG-Texturen im FBX-Binär (Dateiname → Bild-Magic → Daten)
|
||||||
* und schreibt sie in ein temporäres Verzeichnis.
|
* und schreibt sie in ein temporäres Verzeichnis.
|
||||||
|
*
|
||||||
|
* PNG: Dateiname → PNG-Magic → IEND-Marker
|
||||||
|
* JPEG: Dateiname → (optional: FBX R-Property-Header mit Länge) → JPEG-Magic → Daten
|
||||||
*/
|
*/
|
||||||
private java.io.File extractEmbeddedTextures(java.io.File fbxFile, Spatial model) {
|
private java.io.File extractEmbeddedTextures(java.io.File fbxFile, Spatial model) {
|
||||||
java.util.Set<String> filenames = collectTextureFilenames(model);
|
java.util.Set<String> filenames = collectTextureFilenames(model);
|
||||||
@@ -1589,20 +1605,51 @@ public class AnimPreviewState extends BaseAppState {
|
|||||||
java.io.File tempDir = new java.io.File(System.getProperty("java.io.tmpdir"),
|
java.io.File tempDir = new java.io.File(System.getProperty("java.io.tmpdir"),
|
||||||
"fbxtex_" + Math.abs(fbxFile.getAbsolutePath().hashCode()));
|
"fbxtex_" + Math.abs(fbxFile.getAbsolutePath().hashCode()));
|
||||||
tempDir.mkdirs();
|
tempDir.mkdirs();
|
||||||
byte[] pngMagic = {(byte)0x89,0x50,0x4E,0x47,0x0D,0x0A,0x1A,0x0A};
|
byte[] pngMagic = {(byte)0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A};
|
||||||
byte[] iend = {0x00,0x00,0x00,0x00,0x49,0x45,0x4E,0x44,(byte)0xAE,0x42,0x60,(byte)0x82};
|
byte[] iend = {0x00,0x00,0x00,0x00, 0x49,0x45,0x4E,0x44, (byte)0xAE,0x42,0x60,(byte)0x82};
|
||||||
|
byte[] jpegMagic = {(byte)0xFF, (byte)0xD8, (byte)0xFF};
|
||||||
|
byte[] jpegEoi = {(byte)0xFF, (byte)0xD9};
|
||||||
boolean any = false;
|
boolean any = false;
|
||||||
for (String filename : filenames) {
|
for (String filename : filenames) {
|
||||||
byte[] nameBytes = filename.getBytes(java.nio.charset.StandardCharsets.US_ASCII);
|
byte[] nameBytes = filename.getBytes(java.nio.charset.StandardCharsets.US_ASCII);
|
||||||
int namePos = binaryIndexOf(data, nameBytes, 0);
|
int namePos = binaryIndexOf(data, nameBytes, 0);
|
||||||
if (namePos < 0) continue;
|
if (namePos < 0) continue;
|
||||||
|
|
||||||
|
// ── PNG ───────────────────────────────────────────────────────
|
||||||
int pngPos = binaryIndexOf(data, pngMagic, namePos);
|
int pngPos = binaryIndexOf(data, pngMagic, namePos);
|
||||||
if (pngPos < 0 || pngPos - namePos > 65536) continue;
|
if (pngPos >= 0 && pngPos - namePos <= 65536) {
|
||||||
int endPos = binaryIndexOf(data, iend, pngPos + 8);
|
int endPos = binaryIndexOf(data, iend, pngPos + 8);
|
||||||
if (endPos < 0) continue;
|
if (endPos >= 0) {
|
||||||
byte[] png = java.util.Arrays.copyOfRange(data, pngPos, endPos + iend.length);
|
byte[] png = java.util.Arrays.copyOfRange(data, pngPos, endPos + iend.length);
|
||||||
java.nio.file.Files.write(new java.io.File(tempDir, filename).toPath(), png);
|
java.nio.file.Files.write(new java.io.File(tempDir, filename).toPath(), png);
|
||||||
LOG.info("[FbxImport] Embedded-Textur extrahiert: {} ({} B)", filename, png.length);
|
LOG.info("[FbxImport] Embedded PNG extrahiert: {} ({} B)", filename, png.length);
|
||||||
|
any = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── JPEG ──────────────────────────────────────────────────────
|
||||||
|
int jpegPos = binaryIndexOf(data, jpegMagic, namePos);
|
||||||
|
if (jpegPos < 0 || jpegPos - namePos > 65536) continue;
|
||||||
|
|
||||||
|
// FBX R-Property: 0x52 + 4-Byte LE-Größe direkt vor den JPEG-Daten
|
||||||
|
int jpegSize = 0;
|
||||||
|
if (jpegPos >= 5 && data[jpegPos - 5] == (byte) 0x52) {
|
||||||
|
jpegSize = java.nio.ByteBuffer.wrap(data, jpegPos - 4, 4)
|
||||||
|
.order(java.nio.ByteOrder.LITTLE_ENDIAN).getInt();
|
||||||
|
}
|
||||||
|
byte[] jpeg;
|
||||||
|
if (jpegSize > 1024 && jpegPos + jpegSize <= data.length) {
|
||||||
|
// Exakte Größe aus FBX-Header bekannt
|
||||||
|
jpeg = java.util.Arrays.copyOfRange(data, jpegPos, jpegPos + jpegSize);
|
||||||
|
} else {
|
||||||
|
// Fallback: bis EOI-Marker scannen
|
||||||
|
int eoiPos = binaryIndexOf(data, jpegEoi, jpegPos + 2);
|
||||||
|
if (eoiPos < 0) continue;
|
||||||
|
jpeg = java.util.Arrays.copyOfRange(data, jpegPos, eoiPos + 2);
|
||||||
|
}
|
||||||
|
java.nio.file.Files.write(new java.io.File(tempDir, filename).toPath(), jpeg);
|
||||||
|
LOG.info("[FbxImport] Embedded JPEG extrahiert: {} ({} B)", filename, jpeg.length);
|
||||||
any = true;
|
any = true;
|
||||||
}
|
}
|
||||||
return any ? tempDir : null;
|
return any ? tempDir : null;
|
||||||
@@ -1612,6 +1659,122 @@ public class AnimPreviewState extends BaseAppState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liest das FBX-Binary, findet alle eingebetteten Bild-Texturen anhand von
|
||||||
|
* FBX-R-Property-Headern und Dateinamen-Strings, und schreibt sie (URL-dekodiert)
|
||||||
|
* in ein temporäres Verzeichnis — BEVOR das Modell mit dem Wrench-Loader geladen wird.
|
||||||
|
*/
|
||||||
|
private java.io.File extractEmbeddedTexturesPreLoad(java.io.File fbxFile) {
|
||||||
|
try {
|
||||||
|
byte[] data = java.nio.file.Files.readAllBytes(fbxFile.toPath());
|
||||||
|
java.io.File tempDir = new java.io.File(System.getProperty("java.io.tmpdir"),
|
||||||
|
"fbxtex_pre_" + Math.abs(fbxFile.getAbsolutePath().hashCode()));
|
||||||
|
tempDir.mkdirs();
|
||||||
|
boolean any = false;
|
||||||
|
|
||||||
|
byte[] pngMagic = {(byte)0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A};
|
||||||
|
byte[] iend = {0x00,0x00,0x00,0x00, 0x49,0x45,0x4E,0x44, (byte)0xAE,0x42,0x60,(byte)0x82};
|
||||||
|
byte[] jpegMagic = {(byte)0xFF, (byte)0xD8, (byte)0xFF};
|
||||||
|
|
||||||
|
// ── JPEG: R-Property Header (0x52 + 4-Byte LE Größe + JPEG Magic) ──
|
||||||
|
int searchPos = 0;
|
||||||
|
while (true) {
|
||||||
|
int jpegPos = binaryIndexOf(data, jpegMagic, searchPos);
|
||||||
|
if (jpegPos < 0) break;
|
||||||
|
searchPos = jpegPos + 1;
|
||||||
|
if (jpegPos < 5 || data[jpegPos - 5] != (byte) 0x52) continue;
|
||||||
|
int size = java.nio.ByteBuffer.wrap(data, jpegPos - 4, 4)
|
||||||
|
.order(java.nio.ByteOrder.LITTLE_ENDIAN).getInt();
|
||||||
|
if (size < 1024 || jpegPos + size > data.length) continue;
|
||||||
|
|
||||||
|
// Dateiname: letztes .jpg/.jpeg-Vorkommen vor jpegPos (max 65536 Bytes rückwärts)
|
||||||
|
String decodedName = findImageFilenameBeforePos(data, jpegPos, new String[]{".jpg", ".jpeg"});
|
||||||
|
if (decodedName == null) decodedName = "texture_" + jpegPos + ".jpg";
|
||||||
|
|
||||||
|
java.io.File dest = new java.io.File(tempDir, decodedName);
|
||||||
|
if (!dest.exists()) {
|
||||||
|
java.nio.file.Files.write(dest.toPath(),
|
||||||
|
java.util.Arrays.copyOfRange(data, jpegPos, jpegPos + size));
|
||||||
|
LOG.info("[CharImport] JPEG vorab extrahiert: '{}' ({} B)", decodedName, size);
|
||||||
|
any = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── PNG: PNG-Magic → IEND ──────────────────────────────────────────
|
||||||
|
searchPos = 0;
|
||||||
|
while (true) {
|
||||||
|
int pngPos = binaryIndexOf(data, pngMagic, searchPos);
|
||||||
|
if (pngPos < 0) break;
|
||||||
|
searchPos = pngPos + 1;
|
||||||
|
int endPos = binaryIndexOf(data, iend, pngPos + 8);
|
||||||
|
if (endPos < 0) continue;
|
||||||
|
int size = endPos + iend.length - pngPos;
|
||||||
|
if (size < 256) continue;
|
||||||
|
|
||||||
|
String decodedName = findImageFilenameBeforePos(data, pngPos, new String[]{".png"});
|
||||||
|
if (decodedName == null) decodedName = "texture_" + pngPos + ".png";
|
||||||
|
|
||||||
|
java.io.File dest = new java.io.File(tempDir, decodedName);
|
||||||
|
if (!dest.exists()) {
|
||||||
|
java.nio.file.Files.write(dest.toPath(),
|
||||||
|
java.util.Arrays.copyOfRange(data, pngPos, pngPos + size));
|
||||||
|
LOG.info("[CharImport] PNG vorab extrahiert: '{}' ({} B)", decodedName, size);
|
||||||
|
any = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return any ? tempDir : null;
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOG.warn("[CharImport] Pre-Load-Texturextraktion fehlgeschlagen: {}", e.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sucht rückwärts ab {@code beforePos} (max 65536 Bytes) nach einem
|
||||||
|
* Dateinamen-String mit einer der angegebenen Endungen und gibt den
|
||||||
|
* URL-dekodierten Basis-Dateinamen zurück.
|
||||||
|
*/
|
||||||
|
private static String findImageFilenameBeforePos(byte[] data, int beforePos, String[] extensions) {
|
||||||
|
int scanFrom = Math.max(0, beforePos - 65536);
|
||||||
|
int bestPos = -1;
|
||||||
|
String bestExt = null;
|
||||||
|
for (String ext : extensions) {
|
||||||
|
byte[] extBytes = ext.getBytes(java.nio.charset.StandardCharsets.US_ASCII);
|
||||||
|
int pos = scanFrom;
|
||||||
|
while (pos < beforePos) {
|
||||||
|
int found = -1;
|
||||||
|
outer:
|
||||||
|
for (int i = pos, max = beforePos - extBytes.length; i <= max; i++) {
|
||||||
|
for (int j = 0; j < extBytes.length; j++) {
|
||||||
|
if (data[i + j] != extBytes[j]) continue outer;
|
||||||
|
}
|
||||||
|
found = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (found < 0) break;
|
||||||
|
if (found > bestPos) { bestPos = found; bestExt = ext; }
|
||||||
|
pos = found + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (bestPos < 0 || bestExt == null) return null;
|
||||||
|
|
||||||
|
// Rückwärts zum Anfang des Dateinamens (Stopp bei /, \ oder Non-Printable)
|
||||||
|
int nameStart = bestPos - 1;
|
||||||
|
while (nameStart > 0 && data[nameStart] >= 0x20
|
||||||
|
&& data[nameStart] != '/' && data[nameStart] != '\\') {
|
||||||
|
nameStart--;
|
||||||
|
}
|
||||||
|
nameStart++;
|
||||||
|
String raw = new String(data, nameStart, bestPos + bestExt.length() - nameStart,
|
||||||
|
java.nio.charset.StandardCharsets.US_ASCII);
|
||||||
|
try {
|
||||||
|
return new java.io.File(java.net.URLDecoder.decode(raw, "UTF-8")).getName();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return new java.io.File(raw.replace("+", " ")).getName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static int binaryIndexOf(byte[] data, byte[] pattern, int from) {
|
private static int binaryIndexOf(byte[] data, byte[] pattern, int from) {
|
||||||
outer:
|
outer:
|
||||||
for (int i = from, max = data.length - pattern.length; i <= max; i++) {
|
for (int i = from, max = data.length - pattern.length; i <= max; i++) {
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ public class ContentController {
|
|||||||
private de.blight.editor.ui.RoutineEditorView routineEditorView;
|
private de.blight.editor.ui.RoutineEditorView routineEditorView;
|
||||||
private de.blight.editor.ui.MonologueEditorView monologueEditorView;
|
private de.blight.editor.ui.MonologueEditorView monologueEditorView;
|
||||||
private StackPane routineJmeSlot;
|
private StackPane routineJmeSlot;
|
||||||
|
private boolean loadingCharacter = false;
|
||||||
|
|
||||||
// ── Character-Formular-Felder ──────────────────────────────────────────────
|
// ── Character-Formular-Felder ──────────────────────────────────────────────
|
||||||
private VBox charEditContainer;
|
private VBox charEditContainer;
|
||||||
@@ -69,7 +70,9 @@ public class ContentController {
|
|||||||
private CheckBox charTraderCheck;
|
private CheckBox charTraderCheck;
|
||||||
private ListView<String> charTraderItemsView;
|
private ListView<String> charTraderItemsView;
|
||||||
private TextField charMsgFriendly, charMsgNeutral, charMsgEnraged, charMsgEnemy;
|
private TextField charMsgFriendly, charMsgNeutral, charMsgEnraged, charMsgEnemy;
|
||||||
private TextField charNameField, charIdField;
|
private TextField charMaxHpField, charMaxManaField, charMaxStaminaField;
|
||||||
|
private ComboBox<String> charWeaponCombo, charShieldCombo;
|
||||||
|
private TextField charIdField;
|
||||||
private ComboBox<String> charTypeCombo, charModelCombo, charAnimSetCombo;
|
private ComboBox<String> charTypeCombo, charModelCombo, charAnimSetCombo;
|
||||||
private ComboBox<de.blight.common.model.Fraction> charFractionCombo;
|
private ComboBox<de.blight.common.model.Fraction> charFractionCombo;
|
||||||
private Label charEditorStatusLabel;
|
private Label charEditorStatusLabel;
|
||||||
@@ -263,11 +266,11 @@ public class ContentController {
|
|||||||
public void loadCharacterById(String id) {
|
public void loadCharacterById(String id) {
|
||||||
if (id == null || id.isBlank()) return;
|
if (id == null || id.isBlank()) return;
|
||||||
Path charDir = ctx.assetRoot().resolve("Characters");
|
Path charDir = ctx.assetRoot().resolve("Characters");
|
||||||
|
loadingCharacter = true;
|
||||||
try {
|
try {
|
||||||
de.blight.common.model.GameCharacter c =
|
de.blight.common.model.GameCharacter c =
|
||||||
de.blight.common.model.CharacterIO.load(charDir.resolve(id + ".character"));
|
de.blight.common.model.CharacterIO.load(charDir.resolve(id + ".character"));
|
||||||
if (charIdField != null) charIdField.setText(c.getCharacterId() != null ? c.getCharacterId() : "");
|
if (charIdField != null) charIdField.setText(c.getCharacterId() != null ? c.getCharacterId() : "");
|
||||||
if (charNameField != null) charNameField.setText(c.getName() != null ? c.getName().id() : "");
|
|
||||||
if (charTypeCombo != null)
|
if (charTypeCombo != null)
|
||||||
charTypeCombo.setValue(c instanceof de.blight.common.model.MainCharacter ? "MainCharacter" : "NPC");
|
charTypeCombo.setValue(c instanceof de.blight.common.model.MainCharacter ? "MainCharacter" : "NPC");
|
||||||
if (charModelCombo != null) charModelCombo.setValue(c.getModelPath());
|
if (charModelCombo != null) charModelCombo.setValue(c.getModelPath());
|
||||||
@@ -303,6 +306,17 @@ public class ContentController {
|
|||||||
if (charMsgNeutral != null) charMsgNeutral.setText(msgKey(msgs, de.blight.common.model.Status.NEUTRAL));
|
if (charMsgNeutral != null) charMsgNeutral.setText(msgKey(msgs, de.blight.common.model.Status.NEUTRAL));
|
||||||
if (charMsgEnraged != null) charMsgEnraged.setText(msgKey(msgs, de.blight.common.model.Status.ENRAGED));
|
if (charMsgEnraged != null) charMsgEnraged.setText(msgKey(msgs, de.blight.common.model.Status.ENRAGED));
|
||||||
if (charMsgEnemy != null) charMsgEnemy.setText(msgKey(msgs, de.blight.common.model.Status.ENEMY));
|
if (charMsgEnemy != null) charMsgEnemy.setText(msgKey(msgs, de.blight.common.model.Status.ENEMY));
|
||||||
|
if (charWeaponCombo != null) {
|
||||||
|
String wid = npc.getEquippedWeaponId();
|
||||||
|
charWeaponCombo.setValue(wid != null ? wid : "");
|
||||||
|
}
|
||||||
|
if (charShieldCombo != null) {
|
||||||
|
String sid = npc.getEquippedShieldId();
|
||||||
|
charShieldCombo.setValue(sid != null ? sid : "");
|
||||||
|
}
|
||||||
|
if (charMaxHpField != null) charMaxHpField.setText(String.valueOf(npc.getMaxHp() > 0 ? npc.getMaxHp() : 100));
|
||||||
|
if (charMaxManaField != null) charMaxManaField.setText(String.valueOf(npc.getMaxMana() > 0 ? npc.getMaxMana() : 100));
|
||||||
|
if (charMaxStaminaField != null) charMaxStaminaField.setText(String.valueOf(npc.getMaxStamina() > 0 ? npc.getMaxStamina() : 100));
|
||||||
if (dialogEditorView != null) {
|
if (dialogEditorView != null) {
|
||||||
dialogEditorView.setDisable(false);
|
dialogEditorView.setDisable(false);
|
||||||
dialogEditorView.loadNpc(npc);
|
dialogEditorView.loadNpc(npc);
|
||||||
@@ -325,6 +339,8 @@ public class ContentController {
|
|||||||
if (charEditorStatusLabel != null) charEditorStatusLabel.setText("Geladen: " + id);
|
if (charEditorStatusLabel != null) charEditorStatusLabel.setText("Geladen: " + id);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
if (charEditorStatusLabel != null) charEditorStatusLabel.setText("Fehler: " + e.getMessage());
|
if (charEditorStatusLabel != null) charEditorStatusLabel.setText("Fehler: " + e.getMessage());
|
||||||
|
} finally {
|
||||||
|
loadingCharacter = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -923,13 +939,25 @@ public class ContentController {
|
|||||||
dialogEditorView = new de.blight.editor.ui.DialogEditorView();
|
dialogEditorView = new de.blight.editor.ui.DialogEditorView();
|
||||||
dialogEditorView.setDisable(true);
|
dialogEditorView.setDisable(true);
|
||||||
monologueEditorView = new de.blight.editor.ui.MonologueEditorView();
|
monologueEditorView = new de.blight.editor.ui.MonologueEditorView();
|
||||||
|
routineEditorView = new de.blight.editor.ui.RoutineEditorView(ctx.input(), ctx.assetRoot().resolve("Characters"));
|
||||||
|
routineEditorView.setDisable(true);
|
||||||
|
|
||||||
|
routineJmeSlot = new StackPane();
|
||||||
|
routineJmeSlot.setMinWidth(0);
|
||||||
|
routineJmeSlot.getChildren().add(ctx.viewportSupplier().get());
|
||||||
|
SplitPane routineSplit = new SplitPane(routineEditorView, routineJmeSlot);
|
||||||
|
routineSplit.setDividerPositions(0.25);
|
||||||
|
routineSplit.widthProperty().addListener((obs, ov, nv) -> {
|
||||||
|
if (nv.doubleValue() > 0) routineSplit.setDividerPositions(0.25);
|
||||||
|
});
|
||||||
|
|
||||||
TabPane tabs = new TabPane();
|
TabPane tabs = new TabPane();
|
||||||
tabs.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE);
|
tabs.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE);
|
||||||
Tab charTab = new Tab("Charakter", buildCharacterSettingsPane());
|
Tab charTab = new Tab("Charakter", buildCharacterSettingsPane());
|
||||||
Tab dialogTab = new Tab("Dialog", dialogEditorView);
|
Tab dialogTab = new Tab("Dialog", dialogEditorView);
|
||||||
Tab monologTab = new Tab("Monologe", monologueEditorView);
|
Tab routineTab = new Tab("Tagesabläufe", routineSplit);
|
||||||
tabs.getTabs().addAll(charTab, dialogTab, monologTab);
|
Tab monologTab = new Tab("Monologe", monologueEditorView);
|
||||||
|
tabs.getTabs().addAll(charTab, dialogTab, routineTab, monologTab);
|
||||||
tabs.getSelectionModel().select(charTab);
|
tabs.getSelectionModel().select(charTab);
|
||||||
|
|
||||||
setContentCenter(tabs);
|
setContentCenter(tabs);
|
||||||
@@ -1085,9 +1113,6 @@ public class ContentController {
|
|||||||
|
|
||||||
charIdField = new TextField();
|
charIdField = new TextField();
|
||||||
charIdField.setPromptText("eindeutige ID (Dateiname)");
|
charIdField.setPromptText("eindeutige ID (Dateiname)");
|
||||||
charNameField = new TextField();
|
|
||||||
charNameField.setPromptText("Anzeigename");
|
|
||||||
charNameField.setEditable(false);
|
|
||||||
|
|
||||||
charTypeCombo = new ComboBox<>();
|
charTypeCombo = new ComboBox<>();
|
||||||
charTypeCombo.getItems().addAll("MainCharacter", "NPC");
|
charTypeCombo.getItems().addAll("MainCharacter", "NPC");
|
||||||
@@ -1113,9 +1138,8 @@ public class ContentController {
|
|||||||
charAnimSetCombo.setOnAction(e -> updateCharActionCombosFromSet());
|
charAnimSetCombo.setOnAction(e -> updateCharActionCombosFromSet());
|
||||||
|
|
||||||
charEditContainer.getChildren().addAll(
|
charEditContainer.getChildren().addAll(
|
||||||
dl.apply("ID:"), charIdField,
|
dl.apply("ID:"), charIdField,
|
||||||
dl.apply("Name:"), charNameField,
|
dl.apply("Typ:"), charTypeCombo
|
||||||
dl.apply("Typ:"), charTypeCombo
|
|
||||||
);
|
);
|
||||||
|
|
||||||
charFractionCombo = new ComboBox<>();
|
charFractionCombo = new ComboBox<>();
|
||||||
@@ -1180,6 +1204,40 @@ public class ContentController {
|
|||||||
charMsgEnemy = new TextField();
|
charMsgEnemy = new TextField();
|
||||||
charMsgEnemy.setPromptText("TextReference-Schlüssel");
|
charMsgEnemy.setPromptText("TextReference-Schlüssel");
|
||||||
|
|
||||||
|
// Weapon / shield combos – populated from items directory
|
||||||
|
charWeaponCombo = new ComboBox<>();
|
||||||
|
charWeaponCombo.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
charWeaponCombo.setPromptText("— keine Waffe —");
|
||||||
|
charWeaponCombo.getItems().add("");
|
||||||
|
|
||||||
|
charShieldCombo = new ComboBox<>();
|
||||||
|
charShieldCombo.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
charShieldCombo.setPromptText("— kein Schild —");
|
||||||
|
charShieldCombo.getItems().add("");
|
||||||
|
|
||||||
|
try {
|
||||||
|
java.util.List<de.blight.common.model.Item> allItems =
|
||||||
|
de.blight.common.model.ItemIO.loadAll(ctx.assetRoot().resolve("items"));
|
||||||
|
allItems.stream()
|
||||||
|
.filter(i -> i.getCategory() == de.blight.common.model.ItemCategory.WEAPON
|
||||||
|
&& i.getSubCategory() != de.blight.common.model.ItemSubCategory.SHIELD)
|
||||||
|
.map(de.blight.common.model.Item::getItemId)
|
||||||
|
.filter(id -> id != null && !id.isBlank())
|
||||||
|
.forEach(charWeaponCombo.getItems()::add);
|
||||||
|
allItems.stream()
|
||||||
|
.filter(i -> i.getSubCategory() == de.blight.common.model.ItemSubCategory.SHIELD)
|
||||||
|
.map(de.blight.common.model.Item::getItemId)
|
||||||
|
.filter(id -> id != null && !id.isBlank())
|
||||||
|
.forEach(charShieldCombo.getItems()::add);
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
|
||||||
|
charMaxHpField = new TextField("100");
|
||||||
|
charMaxHpField.setPromptText("100");
|
||||||
|
charMaxManaField = new TextField("100");
|
||||||
|
charMaxManaField.setPromptText("100");
|
||||||
|
charMaxStaminaField = new TextField("100");
|
||||||
|
charMaxStaminaField.setPromptText("100");
|
||||||
|
|
||||||
charNpcSection = new VBox(4,
|
charNpcSection = new VBox(4,
|
||||||
dl.apply("Status:"), charStatusCombo,
|
dl.apply("Status:"), charStatusCombo,
|
||||||
dl.apply("Handel:"), charTraderCheck,
|
dl.apply("Handel:"), charTraderCheck,
|
||||||
@@ -1188,7 +1246,14 @@ public class ContentController {
|
|||||||
dl.apply("Friendly:"), charMsgFriendly,
|
dl.apply("Friendly:"), charMsgFriendly,
|
||||||
dl.apply("Neutral:"), charMsgNeutral,
|
dl.apply("Neutral:"), charMsgNeutral,
|
||||||
dl.apply("Enraged:"), charMsgEnraged,
|
dl.apply("Enraged:"), charMsgEnraged,
|
||||||
dl.apply("Enemy:"), charMsgEnemy);
|
dl.apply("Enemy:"), charMsgEnemy,
|
||||||
|
dsl.apply("Basiswerte"),
|
||||||
|
dl.apply("Max HP:"), charMaxHpField,
|
||||||
|
dl.apply("Max Mana:"), charMaxManaField,
|
||||||
|
dl.apply("Max Ausdauer:"), charMaxStaminaField,
|
||||||
|
dsl.apply("Ausrüstung"),
|
||||||
|
dl.apply("Primärwaffe:"), charWeaponCombo,
|
||||||
|
dl.apply("Schild (nur bei Schwert):"), charShieldCombo);
|
||||||
boolean npcInitial = "NPC".equals(charTypeCombo.getValue());
|
boolean npcInitial = "NPC".equals(charTypeCombo.getValue());
|
||||||
charNpcSection.setVisible(npcInitial);
|
charNpcSection.setVisible(npcInitial);
|
||||||
charNpcSection.setManaged(npcInitial);
|
charNpcSection.setManaged(npcInitial);
|
||||||
@@ -1238,17 +1303,11 @@ public class ContentController {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Buttons
|
// Buttons
|
||||||
Button saveBtn = new Button("Speichern");
|
|
||||||
Button clearBtn = new Button("Leeren");
|
Button clearBtn = new Button("Leeren");
|
||||||
saveBtn.setMaxWidth(Double.MAX_VALUE);
|
|
||||||
clearBtn.setMaxWidth(Double.MAX_VALUE);
|
clearBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
saveBtn.setStyle("-fx-background-color:#2a5a2a;-fx-text-fill:white;");
|
|
||||||
saveBtn.setOnAction(e -> saveCharacter(charDir));
|
|
||||||
clearBtn.setOnAction(e -> clearCharacterForm());
|
clearBtn.setOnAction(e -> clearCharacterForm());
|
||||||
HBox.setHgrow(saveBtn, Priority.ALWAYS);
|
|
||||||
HBox.setHgrow(clearBtn, Priority.ALWAYS);
|
HBox.setHgrow(clearBtn, Priority.ALWAYS);
|
||||||
HBox btnBar = new HBox(6, saveBtn, clearBtn);
|
charEditContainer.getChildren().addAll(new Separator(), clearBtn);
|
||||||
charEditContainer.getChildren().addAll(new Separator(), btnBar);
|
|
||||||
|
|
||||||
// Auto-Save-Listener
|
// Auto-Save-Listener
|
||||||
charIdField.focusedProperty().addListener((obs, ov, nv) -> { if (!nv) autoSaveCharacter(charDir); });
|
charIdField.focusedProperty().addListener((obs, ov, nv) -> { if (!nv) autoSaveCharacter(charDir); });
|
||||||
@@ -1264,6 +1323,12 @@ public class ContentController {
|
|||||||
charMsgNeutral.focusedProperty().addListener((obs, ov, nv) -> { if (!nv) autoSaveCharacter(charDir); });
|
charMsgNeutral.focusedProperty().addListener((obs, ov, nv) -> { if (!nv) autoSaveCharacter(charDir); });
|
||||||
charMsgEnraged.focusedProperty().addListener((obs, ov, nv) -> { if (!nv) autoSaveCharacter(charDir); });
|
charMsgEnraged.focusedProperty().addListener((obs, ov, nv) -> { if (!nv) autoSaveCharacter(charDir); });
|
||||||
charMsgEnemy.focusedProperty().addListener((obs, ov, nv) -> { if (!nv) autoSaveCharacter(charDir); });
|
charMsgEnemy.focusedProperty().addListener((obs, ov, nv) -> { if (!nv) autoSaveCharacter(charDir); });
|
||||||
|
charWeaponCombo.valueProperty().addListener((obs, ov, nv) -> autoSaveCharacter(charDir));
|
||||||
|
charShieldCombo.valueProperty().addListener((obs, ov, nv) -> autoSaveCharacter(charDir));
|
||||||
|
charMaxHpField.focusedProperty().addListener((obs, ov, nv) -> { if (!nv) autoSaveCharacter(charDir); });
|
||||||
|
charMaxManaField.focusedProperty().addListener((obs, ov, nv) -> { if (!nv) autoSaveCharacter(charDir); });
|
||||||
|
charMaxStaminaField.focusedProperty().addListener((obs, ov, nv) -> { if (!nv) autoSaveCharacter(charDir); });
|
||||||
|
if (routineEditorView != null) routineEditorView.setOnDataChanged(() -> autoSaveCharacter(charDir));
|
||||||
|
|
||||||
inner.getChildren().add(charEditContainer);
|
inner.getChildren().add(charEditContainer);
|
||||||
|
|
||||||
@@ -1275,6 +1340,7 @@ public class ContentController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void autoSaveCharacter(Path charDir) {
|
private void autoSaveCharacter(Path charDir) {
|
||||||
|
if (loadingCharacter) return;
|
||||||
if (charIdField == null || charIdField.getText().trim().isBlank()) return;
|
if (charIdField == null || charIdField.getText().trim().isBlank()) return;
|
||||||
saveCharacter(charDir);
|
saveCharacter(charDir);
|
||||||
}
|
}
|
||||||
@@ -1333,8 +1399,7 @@ public class ContentController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void clearCharacterForm() {
|
private void clearCharacterForm() {
|
||||||
if (charIdField != null) charIdField.clear();
|
if (charIdField != null) charIdField.clear();
|
||||||
if (charNameField != null) charNameField.clear();
|
|
||||||
if (charTypeCombo != null) charTypeCombo.setValue("NPC");
|
if (charTypeCombo != null) charTypeCombo.setValue("NPC");
|
||||||
if (charModelCombo != null) charModelCombo.setValue(null);
|
if (charModelCombo != null) charModelCombo.setValue(null);
|
||||||
if (charAnimSetCombo != null) charAnimSetCombo.setValue(null);
|
if (charAnimSetCombo != null) charAnimSetCombo.setValue(null);
|
||||||
@@ -1373,8 +1438,6 @@ public class ContentController {
|
|||||||
? new de.blight.common.model.MainCharacter()
|
? new de.blight.common.model.MainCharacter()
|
||||||
: new de.blight.common.model.NPC();
|
: new de.blight.common.model.NPC();
|
||||||
c.setCharacterId(id);
|
c.setCharacterId(id);
|
||||||
String nameId = charNameField != null ? charNameField.getText().trim() : "";
|
|
||||||
c.setName(nameId.isBlank() ? null : new de.blight.common.model.TextReference(nameId));
|
|
||||||
c.setModelPath(charModelCombo != null ? charModelCombo.getValue() : null);
|
c.setModelPath(charModelCombo != null ? charModelCombo.getValue() : null);
|
||||||
c.setAnimSetPath(charAnimSetCombo != null ? charAnimSetCombo.getValue() : null);
|
c.setAnimSetPath(charAnimSetCombo != null ? charAnimSetCombo.getValue() : null);
|
||||||
if (c instanceof de.blight.common.model.NPC npc) {
|
if (c instanceof de.blight.common.model.NPC npc) {
|
||||||
@@ -1404,12 +1467,24 @@ public class ContentController {
|
|||||||
} else {
|
} else {
|
||||||
npc.setItems(null);
|
npc.setItems(null);
|
||||||
}
|
}
|
||||||
|
String wid = charWeaponCombo != null ? charWeaponCombo.getValue() : null;
|
||||||
|
npc.setEquippedWeaponId(wid != null && !wid.isBlank() ? wid : null);
|
||||||
|
String sid = charShieldCombo != null ? charShieldCombo.getValue() : null;
|
||||||
|
npc.setEquippedShieldId(sid != null && !sid.isBlank() ? sid : null);
|
||||||
|
npc.setMaxHp(parseStatField(charMaxHpField, 100));
|
||||||
|
npc.setMaxMana(parseStatField(charMaxManaField, 100));
|
||||||
|
npc.setMaxStamina(parseStatField(charMaxStaminaField, 100));
|
||||||
}
|
}
|
||||||
if (c instanceof de.blight.common.model.MainCharacter mc) {
|
if (c instanceof de.blight.common.model.MainCharacter mc) {
|
||||||
mc.setAbilities(buildAbilities());
|
mc.setAbilities(buildAbilities());
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
de.blight.common.model.CharacterIO.save(c, charDir);
|
de.blight.common.model.CharacterIO.save(c, charDir);
|
||||||
|
if (c instanceof de.blight.common.model.NPC) {
|
||||||
|
String langKey = "npc." + id + ".name";
|
||||||
|
String langName = deriveDisplayName(id);
|
||||||
|
de.blight.editor.ui.LangDefaults.writeIfAbsent(langKey, langName);
|
||||||
|
}
|
||||||
refreshContentCharacterTree();
|
refreshContentCharacterTree();
|
||||||
if (charEditorStatusLabel != null) charEditorStatusLabel.setText("Gespeichert: " + id);
|
if (charEditorStatusLabel != null) charEditorStatusLabel.setText("Gespeichert: " + id);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -1417,6 +1492,27 @@ public class ContentController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int parseStatField(TextField field, int defaultVal) {
|
||||||
|
if (field == null) { return defaultVal; }
|
||||||
|
try {
|
||||||
|
int v = Integer.parseInt(field.getText().trim());
|
||||||
|
return v > 0 ? v : defaultVal;
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return defaultVal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String deriveDisplayName(String id) {
|
||||||
|
String s = id.replaceAll("[0-9]", "").replace('_', ' ').replace('-', ' ').trim();
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (String word : s.split(" +")) {
|
||||||
|
if (word.isEmpty()) continue;
|
||||||
|
if (sb.length() > 0) sb.append(' ');
|
||||||
|
sb.append(Character.toUpperCase(word.charAt(0))).append(word.substring(1));
|
||||||
|
}
|
||||||
|
return sb.isEmpty() ? id : sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
private void refreshFractionCombo() {
|
private void refreshFractionCombo() {
|
||||||
loadedFractions.clear();
|
loadedFractions.clear();
|
||||||
loadedFractions.addAll(
|
loadedFractions.addAll(
|
||||||
|
|||||||
@@ -268,6 +268,7 @@ public class GameController {
|
|||||||
javaExe,
|
javaExe,
|
||||||
"--add-opens", "java.base/java.lang=ALL-UNNAMED",
|
"--add-opens", "java.base/java.lang=ALL-UNNAMED",
|
||||||
"--add-opens", "java.desktop/sun.awt=ALL-UNNAMED",
|
"--add-opens", "java.desktop/sun.awt=ALL-UNNAMED",
|
||||||
|
"-Xmx2g",
|
||||||
"-Djava.library.path=" + libPath,
|
"-Djava.library.path=" + libPath,
|
||||||
"-Dblight.project.root=" + projRoot,
|
"-Dblight.project.root=" + projRoot,
|
||||||
"-D" + de.blight.common.MapIO.PROP_SESSION_MAP + "=" + sessionMapPath,
|
"-D" + de.blight.common.MapIO.PROP_SESSION_MAP + "=" + sessionMapPath,
|
||||||
|
|||||||
@@ -69,7 +69,12 @@ public class RoutineEditorView extends VBox {
|
|||||||
private final List<PlacedItem> placedItems = new ArrayList<>();
|
private final List<PlacedItem> placedItems = new ArrayList<>();
|
||||||
private final List<String> npcIds = new ArrayList<>();
|
private final List<String> npcIds = new ArrayList<>();
|
||||||
|
|
||||||
private boolean updatingForm = false;
|
private boolean updatingForm = false;
|
||||||
|
private Runnable onDataChanged = null;
|
||||||
|
|
||||||
|
public void setOnDataChanged(Runnable r) { this.onDataChanged = r; }
|
||||||
|
|
||||||
|
private void fireDataChanged() { if (onDataChanged != null) onDataChanged.run(); }
|
||||||
|
|
||||||
// ── Polling-Timer ──────────────────────────────────────────────────────────
|
// ── Polling-Timer ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -358,6 +363,7 @@ public class RoutineEditorView extends VBox {
|
|||||||
activeBlock.getActivity().getWaypoints().add(wp);
|
activeBlock.getActivity().getWaypoints().add(wp);
|
||||||
refreshWaypointList();
|
refreshWaypointList();
|
||||||
updateMarkers();
|
updateMarkers();
|
||||||
|
fireDataChanged();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
delWp.setOnAction(e -> {
|
delWp.setOnAction(e -> {
|
||||||
@@ -366,6 +372,7 @@ public class RoutineEditorView extends VBox {
|
|||||||
wps.remove(wps.size() - 1);
|
wps.remove(wps.size() - 1);
|
||||||
refreshWaypointList();
|
refreshWaypointList();
|
||||||
updateMarkers();
|
updateMarkers();
|
||||||
|
fireDataChanged();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -408,6 +415,7 @@ public class RoutineEditorView extends VBox {
|
|||||||
activeBlock.setStartHour(startSpin.getValue());
|
activeBlock.setStartHour(startSpin.getValue());
|
||||||
activeBlock.setEndHour(endSpin.getValue());
|
activeBlock.setEndHour(endSpin.getValue());
|
||||||
refreshCoverage();
|
refreshCoverage();
|
||||||
|
fireDataChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applyActivityToBlock() {
|
private void applyActivityToBlock() {
|
||||||
@@ -456,6 +464,7 @@ public class RoutineEditorView extends VBox {
|
|||||||
}
|
}
|
||||||
// PATROL is managed directly in buildPatrolFields
|
// PATROL is managed directly in buildPatrolFields
|
||||||
}
|
}
|
||||||
|
fireDataChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Routine-Operationen ────────────────────────────────────────────────────
|
// ── Routine-Operationen ────────────────────────────────────────────────────
|
||||||
@@ -470,6 +479,7 @@ public class RoutineEditorView extends VBox {
|
|||||||
routines.add(r);
|
routines.add(r);
|
||||||
refreshRoutineList();
|
refreshRoutineList();
|
||||||
routineList.getSelectionModel().selectLast();
|
routineList.getSelectionModel().selectLast();
|
||||||
|
fireDataChanged();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -482,6 +492,7 @@ public class RoutineEditorView extends VBox {
|
|||||||
if (!name.isBlank()) {
|
if (!name.isBlank()) {
|
||||||
activeRoutine.setName(name.trim());
|
activeRoutine.setName(name.trim());
|
||||||
refreshRoutineList();
|
refreshRoutineList();
|
||||||
|
fireDataChanged();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -494,6 +505,7 @@ public class RoutineEditorView extends VBox {
|
|||||||
refreshRoutineList();
|
refreshRoutineList();
|
||||||
if (!routines.isEmpty())
|
if (!routines.isEmpty())
|
||||||
routineList.getSelectionModel().select(Math.min(idx, routines.size() - 1));
|
routineList.getSelectionModel().select(Math.min(idx, routines.size() - 1));
|
||||||
|
fireDataChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addBlock() {
|
private void addBlock() {
|
||||||
@@ -502,6 +514,7 @@ public class RoutineEditorView extends VBox {
|
|||||||
activeRoutine.getBlocks().add(b);
|
activeRoutine.getBlocks().add(b);
|
||||||
refreshBlockList();
|
refreshBlockList();
|
||||||
blockList.getSelectionModel().selectLast();
|
blockList.getSelectionModel().selectLast();
|
||||||
|
fireDataChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void deleteBlock() {
|
private void deleteBlock() {
|
||||||
@@ -514,6 +527,7 @@ public class RoutineEditorView extends VBox {
|
|||||||
refreshCoverage();
|
refreshCoverage();
|
||||||
if (!activeRoutine.getBlocks().isEmpty())
|
if (!activeRoutine.getBlocks().isEmpty())
|
||||||
blockList.getSelectionModel().select(Math.min(idx, activeRoutine.getBlocks().size() - 1));
|
blockList.getSelectionModel().select(Math.min(idx, activeRoutine.getBlocks().size() - 1));
|
||||||
|
fireDataChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void moveBlock(int delta) {
|
private void moveBlock(int delta) {
|
||||||
|
|||||||
@@ -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 org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
public class PlayerInputControl {
|
public class PlayerInputControl {
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(PlayerInputControl.class);
|
private static final Logger log = LoggerFactory.getLogger(PlayerInputControl.class);
|
||||||
|
|
||||||
private static final float MOVE_SPEED = 0.07f;
|
private static final float MOVE_SPEED = CharacterNavigator.RUN_SPEED;
|
||||||
private static final float SPRINT_MULT = 1.5f;
|
private static final float SPRINT_MULT = 1.5f;
|
||||||
private static final float WALK_MULT = 0.5f;
|
private static final float WALK_MULT = 0.5f;
|
||||||
private static final float ROTATE_SPEED = 10f;
|
private static final float ROTATE_SPEED = 10f;
|
||||||
@@ -153,7 +155,7 @@ public class PlayerInputControl {
|
|||||||
private float heavyLungeEndRemaining = 0f;
|
private float heavyLungeEndRemaining = 0f;
|
||||||
/** Gesamtdauer der aktuellen Kampfanim — für Kombo-Fenster-Timing (öffnet bei 50%). */
|
/** Gesamtdauer der aktuellen Kampfanim — für Kombo-Fenster-Timing (öffnet bei 50%). */
|
||||||
private float combatAnimDuration = 0f;
|
private float combatAnimDuration = 0f;
|
||||||
private MeleeAttackCallback meleeAttackCallback = null;
|
private final List<MeleeAttackCallback> meleeCallbacks = new ArrayList<>();
|
||||||
private WeaponAttachmentControl weaponAttach = null;
|
private WeaponAttachmentControl weaponAttach = null;
|
||||||
private static final float HIT_WINDOW_START = 0.20f;
|
private static final float HIT_WINDOW_START = 0.20f;
|
||||||
private static final float HIT_WINDOW_END = 0.75f;
|
private static final float HIT_WINDOW_END = 0.75f;
|
||||||
@@ -430,7 +432,22 @@ public class PlayerInputControl {
|
|||||||
public ItemSubCategory getDrawnWeaponType() { return drawnWeaponType; }
|
public ItemSubCategory getDrawnWeaponType() { return drawnWeaponType; }
|
||||||
public boolean isLastAttackCritical() { return lastAttackCritical; }
|
public boolean isLastAttackCritical() { return lastAttackCritical; }
|
||||||
public com.jme3.anim.SkinningControl getSkinningControl() { return skinningControl; }
|
public com.jme3.anim.SkinningControl getSkinningControl() { return skinningControl; }
|
||||||
public void setMeleeAttackCallback(MeleeAttackCallback cb) { this.meleeAttackCallback = cb; }
|
|
||||||
|
/**
|
||||||
|
* Geräusch-Radius des Spielers in Welteinheiten (m) – abhängig von der Bewegungsart:
|
||||||
|
* Sprint → 10 m, normales Laufen → 5 m, Gehen (walk-Taste) → 3 m,
|
||||||
|
* Schleichen / Stehen → 0 (kein Geräusch).
|
||||||
|
*/
|
||||||
|
public float getSoundRadius() {
|
||||||
|
boolean sneaking = sneakPhase != SneakPhase.NONE;
|
||||||
|
boolean moving = forward || backward || left || right;
|
||||||
|
if (!moving || sneaking) { return 0f; }
|
||||||
|
if (sprint) { return 10f; }
|
||||||
|
if (walk) { return 3f; }
|
||||||
|
return 5f;
|
||||||
|
}
|
||||||
|
public void addMeleeAttackCallback(MeleeAttackCallback cb) { if (cb != null) meleeCallbacks.add(cb); }
|
||||||
|
public void setMeleeAttackCallback(MeleeAttackCallback cb) { addMeleeAttackCallback(cb); }
|
||||||
public void setWeaponAttachmentControl(WeaponAttachmentControl w) { this.weaponAttach = w; }
|
public void setWeaponAttachmentControl(WeaponAttachmentControl w) { this.weaponAttach = w; }
|
||||||
|
|
||||||
public boolean isPaused() { return paused; }
|
public boolean isPaused() { return paused; }
|
||||||
@@ -1345,6 +1362,13 @@ public class PlayerInputControl {
|
|||||||
|| blockPhase == BlockPhase.EXITING;
|
|| blockPhase == BlockPhase.EXITING;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** True wenn der Spieler gerade läuft oder rennt (nicht steht, schleicht oder geht). */
|
||||||
|
public boolean isRunningOrSprinting() {
|
||||||
|
boolean moving = forward || backward || left || right;
|
||||||
|
boolean sneaking = sneakPhase != SneakPhase.NONE;
|
||||||
|
return moving && !sneaking && !walk;
|
||||||
|
}
|
||||||
|
|
||||||
/** Wird aufgerufen wenn ein blockierter Treffer landet. Kostet 30 Ausdauer; unter 0 → Block bricht. */
|
/** Wird aufgerufen wenn ein blockierter Treffer landet. Kostet 30 Ausdauer; unter 0 → Block bricht. */
|
||||||
public void triggerBlockImpact() {
|
public void triggerBlockImpact() {
|
||||||
if (blockPhase != BlockPhase.HOLDING || drawnWeaponType == null) return;
|
if (blockPhase != BlockPhase.HOLDING || drawnWeaponType == null) return;
|
||||||
@@ -1621,7 +1645,7 @@ public class PlayerInputControl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void fireWeaponCallback(float critMultiplier) {
|
private void fireWeaponCallback(float critMultiplier) {
|
||||||
if (meleeAttackCallback == null || weaponAttach == null) {
|
if (meleeCallbacks.isEmpty() || weaponAttach == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
com.jme3.scene.Spatial ws = weaponAttach.getWeaponSpatial();
|
com.jme3.scene.Spatial ws = weaponAttach.getWeaponSpatial();
|
||||||
@@ -1633,7 +1657,9 @@ public class PlayerInputControl {
|
|||||||
? mainCharacter.getEquippedMeleeWeapon().getDamage() : 0;
|
? mainCharacter.getEquippedMeleeWeapon().getDamage() : 0;
|
||||||
Vector3f grip = hn.getWorldTranslation().clone();
|
Vector3f grip = hn.getWorldTranslation().clone();
|
||||||
Vector3f bladeDir = ws.getWorldRotation().mult(com.jme3.math.Vector3f.UNIT_Y).normalizeLocal();
|
Vector3f bladeDir = ws.getWorldRotation().mult(com.jme3.math.Vector3f.UNIT_Y).normalizeLocal();
|
||||||
meleeAttackCallback.onMeleeAttack(currentAttackId, grip, bladeDir, WEAPON_BLADE_LENGTH, base, critMultiplier);
|
for (MeleeAttackCallback cb : meleeCallbacks) {
|
||||||
|
cb.onMeleeAttack(currentAttackId, grip, bladeDir, WEAPON_BLADE_LENGTH, base, critMultiplier);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private float computeHeavyMultiplier() {
|
private float computeHeavyMultiplier() {
|
||||||
|
|||||||
@@ -56,6 +56,10 @@ public class CharacterNavigator {
|
|||||||
public static final float WALK_SPEED = 0.035f;
|
public static final float WALK_SPEED = 0.035f;
|
||||||
/** Renn-Geschwindigkeit (m/frame bei 60fps). */
|
/** Renn-Geschwindigkeit (m/frame bei 60fps). */
|
||||||
public static final float RUN_SPEED = 0.07f;
|
public static final float RUN_SPEED = 0.07f;
|
||||||
|
/** Renn-Geschwindigkeit in m/s (für tpf-basierte Bewegung, z.B. NPCs ohne CharacterControl). */
|
||||||
|
public static final float RUN_SPEED_MPS = 4.2f;
|
||||||
|
/** Geh-Geschwindigkeit in m/s. */
|
||||||
|
public static final float WALK_SPEED_MPS = 2.1f;
|
||||||
/** Minimale Bewegung (m/s) unter der ein Charakter als steckengeblieben gilt. */
|
/** Minimale Bewegung (m/s) unter der ein Charakter als steckengeblieben gilt. */
|
||||||
private static final float STUCK_MIN_SPEED = 0.2f;
|
private static final float STUCK_MIN_SPEED = 0.2f;
|
||||||
/** Zeit (s) mit zu langsamer Bewegung bis zum Abbruch. */
|
/** Zeit (s) mit zu langsamer Bewegung bis zum Abbruch. */
|
||||||
|
|||||||
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.configureLocomotionSnap(AnimationLibrary.findAssetRoot(), setName);
|
||||||
}
|
}
|
||||||
animLib.applyAllTo(characterVisual != null ? characterVisual : character);
|
animLib.applyAllTo(characterVisual != null ? characterVisual : character);
|
||||||
|
WorldNpcsState npcsState = app.getStateManager().getState(WorldNpcsState.class);
|
||||||
|
if (npcsState != null) npcsState.reapplyAnimations();
|
||||||
log.info("[AnimCtx] MainCharacter: {} animSetPath: {} clipCount: {} clips: {}",
|
log.info("[AnimCtx] MainCharacter: {} animSetPath: {} clipCount: {} clips: {}",
|
||||||
mc != null ? mc.getCharacterId() : "null",
|
mc != null ? mc.getCharacterId() : "null",
|
||||||
setName,
|
setName,
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ public class InteractionHudState extends BaseAppState {
|
|||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(InteractionHudState.class);
|
private static final Logger log = LoggerFactory.getLogger(InteractionHudState.class);
|
||||||
|
|
||||||
private static final float Y_OFFSET = 0.6f;
|
private static final float Y_OFFSET = 0.6f;
|
||||||
|
private static final ColorRGBA COL_DEFAULT = new ColorRGBA(1f, 0.95f, 0.6f, 1f);
|
||||||
|
|
||||||
private Camera cam;
|
private Camera cam;
|
||||||
private Node guiNode;
|
private Node guiNode;
|
||||||
@@ -52,7 +53,7 @@ public class InteractionHudState extends BaseAppState {
|
|||||||
labelText = new BitmapText(font);
|
labelText = new BitmapText(font);
|
||||||
labelText.setSize(font.getCharSet().getRenderedSize() * 1.2f * 2f);
|
labelText.setSize(font.getCharSet().getRenderedSize() * 1.2f * 2f);
|
||||||
labelText.setLocalScale(0.5f, 0.5f, 1f);
|
labelText.setLocalScale(0.5f, 0.5f, 1f);
|
||||||
labelText.setColor(new ColorRGBA(1f, 0.95f, 0.6f, 1f));
|
labelText.setColor(COL_DEFAULT.clone());
|
||||||
labelText.setCullHint(Spatial.CullHint.Always);
|
labelText.setCullHint(Spatial.CullHint.Always);
|
||||||
guiNode.attachChild(labelText);
|
guiNode.attachChild(labelText);
|
||||||
}
|
}
|
||||||
@@ -90,15 +91,18 @@ public class InteractionHudState extends BaseAppState {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String labelKey = null;
|
String labelKey = null;
|
||||||
Vector3f worldPos = null;
|
Vector3f worldPos = null;
|
||||||
|
com.jme3.math.ColorRGBA labelColor = COL_DEFAULT;
|
||||||
|
|
||||||
// NPC hat höchste Priorität, dann Interactable, dann Item
|
// NPC hat höchste Priorität, dann Interactable, dann Item
|
||||||
if (worldNpcs != null) {
|
if (worldNpcs != null) {
|
||||||
String key = worldNpcs.getHoveredLabelKey();
|
String key = worldNpcs.getHoveredLabelKey();
|
||||||
if (key != null && !key.isBlank()) {
|
if (key != null && !key.isBlank()) {
|
||||||
labelKey = key;
|
labelKey = key;
|
||||||
worldPos = worldNpcs.getHoveredWorldPos();
|
worldPos = worldNpcs.getHoveredWorldPos();
|
||||||
|
com.jme3.math.ColorRGBA c = worldNpcs.getHoveredLabelColor();
|
||||||
|
if (c != null) labelColor = c;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,9 +147,10 @@ public class InteractionHudState extends BaseAppState {
|
|||||||
if (keyBindings != null) {
|
if (keyBindings != null) {
|
||||||
text = text.replace("{interact}", KeyNames.of(keyBindings.interact));
|
text = text.replace("{interact}", KeyNames.of(keyBindings.interact));
|
||||||
}
|
}
|
||||||
|
labelText.setColor(labelColor);
|
||||||
labelText.setText(text);
|
labelText.setText(text);
|
||||||
float textW = labelText.getLineWidth();
|
float textW = labelText.getLineWidth() * 0.5f;
|
||||||
labelText.setLocalTranslation(screenV.x - textW * 0.5f, screenV.y, 1f);
|
labelText.setLocalTranslation(screenV.x - textW, screenV.y, 1f);
|
||||||
labelText.setCullHint(Spatial.CullHint.Inherit);
|
labelText.setCullHint(Spatial.CullHint.Inherit);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ public class WorldDummyState extends BaseAppState implements MeleeAttackCallback
|
|||||||
|
|
||||||
loadDummies();
|
loadDummies();
|
||||||
buildEnemyHud();
|
buildEnemyHud();
|
||||||
playerInput.setMeleeAttackCallback(this);
|
playerInput.addMeleeAttackCallback(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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
|
rostiges_kurzschwert.name=Rostiges Kurzschwert
|
||||||
blutagave.name=Blutagave
|
blutagave.name=Blutagave
|
||||||
erzmoss.name=Erzmoss
|
erzmoss.name=Erzmoss
|
||||||
|
|
||||||
|
# NPC Aggro – erste Warnung (ENRAGED, erster Sichtkontakt)
|
||||||
|
npc.aggro.warn1.1=Verschwinde – das ist deine letzte freundliche Warnung!
|
||||||
|
npc.aggro.warn1.2=Noch einen Schritt und du bereust es!
|
||||||
|
npc.aggro.warn1.3=Geh weg, solange du noch kannst!
|
||||||
|
npc.aggro.warn1.4=Ich warne dich: Komm mir nicht zu nah!
|
||||||
|
npc.aggro.warn1.5=Geh jetzt, oder du wirst es bereuen!
|
||||||
|
|
||||||
|
# NPC Aggro – zweite Warnung (ENRAGED, nach 10 s)
|
||||||
|
npc.aggro.warn2.1=Ich meine es ernst! Das ist deine letzte Chance!
|
||||||
|
npc.aggro.warn2.2=Immer noch da? Das war deine letzte Warnung!
|
||||||
|
npc.aggro.warn2.3=Du spielst mit dem Feuer – verschwinde jetzt!
|
||||||
|
npc.aggro.warn2.4=Noch einmal: Geh weg! Das nächste Mal greife ich an!
|
||||||
|
npc.aggro.warn2.5=Du testest meine Geduld. Geh – jetzt sofort!
|
||||||
|
|
||||||
|
# NPC Aggro – Angriff (Übergang zu ATTACKING)
|
||||||
|
npc.aggro.attack.1=Genug geredet! Jetzt stirbst du!
|
||||||
|
npc.aggro.attack.2=Du hättest besser gehen sollen!
|
||||||
|
npc.aggro.attack.3=Das hast du dir selbst eingebrockt!
|
||||||
|
npc.aggro.attack.4=Auf dich habe ich lange genug gewartet!
|
||||||
|
npc.aggro.attack.5=Dann eben auf die harte Tour!
|
||||||
|
npc.aggro.break.1=Irgendwann krieg ich dich noch!
|
||||||
|
npc.aggro.break.2=Das war dein Glückstag. Aber ich vergesse das nicht!
|
||||||
|
npc.aggro.break.3=Lauf nur! Du kannst nicht für immer fliehen!
|
||||||
|
npc.aggro.break.4=Beim nächsten Mal kommst du nicht so leicht davon!
|
||||||
|
npc.aggro.break.5=Wir sehen uns wieder – darauf kannst du dich verlassen!
|
||||||
|
|
||||||
|
npc.bandit1.name=Bandit
|
||||||
|
npc.bjoernson.name=Bjoernson
|
||||||
|
npc.silas.name=Silas
|
||||||
|
|||||||
@@ -104,3 +104,33 @@ einfacher_schild.name=Simple Shield
|
|||||||
rostiges_kurzschwert.name=Rusty Short Sword
|
rostiges_kurzschwert.name=Rusty Short Sword
|
||||||
blutagave.name=Blood Agave
|
blutagave.name=Blood Agave
|
||||||
erzmoss.name=Ore Moss
|
erzmoss.name=Ore Moss
|
||||||
|
|
||||||
|
# NPC Aggro – first warning
|
||||||
|
npc.aggro.warn1.1=Leave – that is your first and last friendly warning!
|
||||||
|
npc.aggro.warn1.2=Take one more step and you'll regret it!
|
||||||
|
npc.aggro.warn1.3=Get out while you still can!
|
||||||
|
npc.aggro.warn1.4=I'm warning you: don't get too close to me!
|
||||||
|
npc.aggro.warn1.5=You'd better leave now, or you'll be sorry!
|
||||||
|
|
||||||
|
# NPC Aggro – second warning
|
||||||
|
npc.aggro.warn2.1=I mean it! This is your last chance!
|
||||||
|
npc.aggro.warn2.2=Still here? That was your final warning!
|
||||||
|
npc.aggro.warn2.3=You're playing with fire – get out now!
|
||||||
|
npc.aggro.warn2.4=Once more: leave! Next time I will attack!
|
||||||
|
npc.aggro.warn2.5=You are testing my patience. Go – right now!
|
||||||
|
|
||||||
|
# NPC Aggro – attack start
|
||||||
|
npc.aggro.attack.1=Enough talk! Now you die!
|
||||||
|
npc.aggro.attack.2=You should have left when you had the chance!
|
||||||
|
npc.aggro.attack.3=You brought this on yourself!
|
||||||
|
npc.aggro.attack.4=I have been patient long enough!
|
||||||
|
npc.aggro.attack.5=Then we'll do it the hard way!
|
||||||
|
npc.aggro.break.1=I'll get you eventually!
|
||||||
|
npc.aggro.break.2=Lucky day for you. But I won't forget this!
|
||||||
|
npc.aggro.break.3=Run! You can't hide forever!
|
||||||
|
npc.aggro.break.4=Next time you won't get off so easily!
|
||||||
|
npc.aggro.break.5=We'll meet again – count on it!
|
||||||
|
|
||||||
|
npc.bandit1.name=Bandit
|
||||||
|
npc.bjoernson.name=Bjoernson
|
||||||
|
npc.silas.name=Silas
|
||||||
|
|||||||
BIN
doc/Banditen Plot.odt
Normal file
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