diff --git a/docs/monster-data.md b/docs/monster-data.md
index 7bc5c41747..6189913184 100644
--- a/docs/monster-data.md
+++ b/docs/monster-data.md
@@ -245,7 +245,12 @@ any character.
| `moveSpeed` | Its walk speed, 0 to 65535 |
| `noBloodPool` | Leaves no blood pool when it dies |
| `hooks` | Code of the maps it hands over to when it appears, with the values that code fixed (Q-MN36d): `[{"hook": "EmpireGuardianRageReset", "boss": "Raymond"}]`. The hooks: `RaklionBossDrop` (Selupan drops down from where he waits), `Kanturu2ndEntrance` (Kanturu's entrance window takes the Gateway Machine, whose animations and the way in it runs), `EmpireGuardianRageReset` with `boss` `Raymond`, `Ercanne`, `Daesuler`, `Gallia`, `Bermont`, `Kato`, `Gaion` or `Jerint` (a new boss starts without the rage before it), `IllusionTempleStorageReset` with `storage` `allied` or `illusion` (a new storage shows no point effect its team had). A hook or value the maps do not know is an Error and does nothing |
-| `stateVariants` | Fields that are other while a state of the game is on, each replacing the monster's: `[{"state": "castleSiegeNotRunning", "heightOffset": -100}]`. The states: `castleSiegeRunning`, `castleSiegeNotRunning`, `raklionBossWaiting` (Selupan above the map, from standby to ready), `playerTransformed` (a player transformed into the monster comes into view, packet 0x45: the Giant is smaller then), `crywolfReady` (Crywolf's battle is about to start: its monsters come in at once, without the thunder). A state variant changes fields and clears none, and has no state variants of its own. A map variant's `stateVariants` replace the monster's on its maps |
+| `stateVariants` | Fields that are other while a state of the game is on, each replacing the monster's: `[{"state": "castleSiegeNotRunning", "heightOffset": -100}]`. The states: `castleSiegeRunning`, `castleSiegeNotRunning`, `raklionBossWaiting` (Selupan above the map, from standby to ready), `playerTransformed` (a player transformed into the monster comes into view, packet 0x45: the Giant is smaller then), `crywolfReady` (Crywolf's battle is about to start: its monsters come in at once, without the thunder), `crywolfElfTime` (on Crywolf only elves may make the altar contracts: the Werewolf Guardsman 407 only talks then). A state variant changes fields and clears none, and has no state variants of its own. A map variant's `stateVariants` replace the monster's on its maps |
+| `talks` | Whether the NPC talks when the hero clicks it (MN5, D66): `false` for the Wolf Status 204; every other NPC talks |
+| `talkLevel` | The level the hero needs to talk to it, and the message below it, with `%d` for the level: `{"level": 10, "message": {"en": "Only level above %d can do the Chaos Combination."}}` (the Chaos Goblin 238). Below it the hero gets the message and keeps the NPC as the target |
+| `repairShop` | Its shop shows the repair buttons: the five repair shops 243, 246, 251, 416 and 578 |
+| `onTalk` | What talking to it does, in order ([Talking to NPCs](#talking-to-npcs)); without it the NPC sends the talk request |
+| `crywolfContract` | It takes part in Crywolf's hero contract: a buff the server sends again while it shows it (0x07) toggles the contract and takes the other Crywolf states off (the altars 205-209 and the NPCs 226, 230, 240, 248, 250, 251, 256, 478) |
A value of the wrong kind is fatal; a joint, object, buff or standing the code
does not have is an Error, and the character appears without it.
@@ -261,6 +266,61 @@ map its entry names, with each state its state variants name on and off,
in a new slot, a slot another character left and the slot of a living
character with its key, through the packets and the cutscenes.
+### Talking to NPCs
+
+When the hero talks to an NPC, the client checks `talks` and `talkLevel`,
+sets the shop's repair buttons by `repairShop`, closes the quest window and
+asks the legacy quest state, then runs the NPC's `onTalk` steps in order
+(MN5, D66, Q-MN46). The server answers the talk request with the NPC's window.
+
+```json
+"onTalk": [
+ {"dialog": {"title": {"en": "Werewolf Guardsman"}, "lines": [{"en": "..."}],
+ "ok": {"request": "enterRefuge", "if": {"quest": "thirdChangeUp2", "is": ["ongoing", "done"]},
+ "zen": 3000000, "failure": {"en": "You are short of Zen."}}}},
+ {"send": "talk"}
+]
+```
+
+| Step | What it does |
+|---|---|
+| `{"send": "talk"}` | Sends the talk request, as the map lets it: on Kanturu 2nd not while the gate machine animates, in the Illusion Temple only to the event's NPCs (their code stays) |
+| `{"send": "petInfo"}` | Asks the server for the equipped Dark Horse's and Dark Raven's information (the Pet Trainer 226) |
+| `{"dialog": ...}` | Shows a dialog: its bold `title`, its `lines`, and an `ok` that sends a `request` (`enterRefuge`, `enterBarracks`) when the `quest` (`changeUp1` ... `thirdChangeUp3`) is in one of the states of `is` (`none`, `ongoing`, `done`, `no`, `ready`, `error`) and the hero has the `zen`, else its `failure` message |
+| `{"message": ...}` | Logs a system message (the red and blue Little Santas 470 and 471) |
+| `{"questWindow": {"noQuestTitleFor": [...]}}` | The quest window shows the NPC's own name and no quest title for these classes (Marlon 229 for the Dark Lord, Magic Gladiator and Rage Fighter, whose quest is another NPC's) |
+| `{"crywolfAltarContract": "crywolfAltar"}` | A Crywolf altar's contract (205-209), which the map's code makes while only elves may, with the texts of the shared set it names ([Shared talk texts](#shared-talk-texts)). On Crywolf otherwise it does nothing; elsewhere it sends the talk request |
+
+The Werewolf Guardsman 407 shows his dialog and talks, the Gatekeeper 408
+talks and shows his. The texts are translatable like the names, copied from
+the game's texts in the 11 languages that have them (the game's zh-CN texts
+have none of them yet); Data Checks warns about a text missing one of the
+game's languages (it shows the English one there), about an OK whose quest
+has no state in `is` (it never sends) and about a `crywolfAltarContract`
+step on a model that is no Crywolf altar (it only talks). While only elves
+may make the Crywolf altar contracts (the state `crywolfElfTime`), an altar
+runs its `crywolfAltarContract` step and the Werewolf Guardsman 407 only
+talks, by his state variant; every other NPC talks as always.
+
+#### Shared talk texts
+
+Talk texts several NPCs share are defined once, by name, in `talkTexts` of
+`Monsters.json`, and a step names its set. The five altars' contract step
+names `crywolfAltar`, whose texts are `canContract` (`%d` for the contracts
+left), `proceed`, `onMount`, `needGuardian`, `destroyed` and `ongoing`:
+
+```json
+"talkTexts": {
+ "crywolfAltar": {"canContract": {"en": "Contract can be made for %d times."}, "proceed": {"en": "..."}, ...}
+}
+```
+
+A step naming a set the list does not have, and a set missing one of its
+texts, are fatal when the list is read; in the editor Data Checks reports a
+step naming a set that is not there as an error (the altar shows empty
+texts, Save refuses it). The sets' texts are translatable like the others
+and Data Checks warns about their missing languages.
+
### Unknown monsters
A monster the game cannot show as itself gets a bug look instead of the Bull
@@ -833,6 +893,11 @@ item tools.
monster's row; see
[translation-system.md → In the monster editor](translation-system.md#in-the-monster-editor)
and [Import and export of NpcName txt](#import-and-export-of-npcname-txt).
+ The tab **Talk texts** has a row per text of the shared talk texts
+ ("talkTexts.crywolfAltar.canContract"), then per text of an NPC's talk, its
+ map and state variants' included ("408 onTalk[1].dialog.lines[0]",
+ "205 mapVariants[0].onTalk[0].message"), with a column per language and the
+ same missing-language filter; an edit is an undo step.
- **Monster Model**: the model of the selected monster, from the model list:
its slot and range (hover over the range for what the code does with it),
the monsters with the model (a click selects one), and
@@ -1013,7 +1078,18 @@ item tools.
are edited under their label once set. The equipment shows its count
and **Open Monster Equipment**. Below them, always shown, the section
**Unknown monsters** edits the bug look of every monster the game cannot
- show as itself ([Unknown monsters](#unknown-monsters)).
+ show as itself ([Unknown monsters](#unknown-monsters)), and **Shared talk
+ texts** the sets of [Shared talk texts](#shared-talk-texts): each set's
+ texts in English with **x**, and **Add a set** with its name. The section **NPC
+ rules** has `talks`, the minimum level to talk with its message,
+ `repairShop`, `crywolfContract` and **On talk, in order**: a row per step,
+ opened to its kind and values (a dialog's title, lines with **Add a line**,
+ and **OK sends a request** with its request, quest and states, zen and the
+ text without the zen, and a red hint while a quest has no state ticked; a
+ message; the classes of a quest window; the shared set an altar contract
+ shows),
+ with **Move up**, **Move down**, **x** and **Add a step**. Texts are typed
+ in English there; the other languages are in Monster Translations.
- **Monster Equipment**: what the selected monster holds and wears, laid
out as the game's character window (helm and wings on top; weapon 1,
armor, weapon 2 and helper; gloves, pants and boots). A click on a slot
diff --git a/docs/superpowers/specs/2026-10-23-data-driven-monsters-design.md b/docs/superpowers/specs/2026-10-23-data-driven-monsters-design.md
index e0037daf35..7902573ca8 100644
--- a/docs/superpowers/specs/2026-10-23-data-driven-monsters-design.md
+++ b/docs/superpowers/specs/2026-10-23-data-driven-monsters-design.md
@@ -926,7 +926,7 @@ model entries.
| MNG | One monster, several numbers | MN2b | D61: the grouped format (merge and difference), the one-time conversion (36 monsters with 160 numbers) with its comparison, `maps` per number from OpenMU's Season 6 placements (430 numbers, with the event spawns of Kanturu, Raklion, Crywolf, the Imperial Guardian and the Castle Siege), the Browser tree and its number actions, shared and own values in every monster tool. The 13 numbers of P26 keep the generic pattern as their own `attacks`. |
| MN3 | Models | MN1, MN.P | `Data/Monsters/Models.json` with what `OpenMonsterModel`, `OpenNpc`, the creators' model constants and the slot-dependent code in `CreateCharacterPointer` (with the Hellas and Battle Castle hooks) set; part models by FX1 name; sounds with ids. `model` is filled for every monster for the tools; the creators still choose the model until MN4, and the recorder checks both agree. Monster recorder, model part. Editor: Monster Model, Monster Preview. P1, P9. |
| MN4 | Setup at spawn | MN3, MN.P, MNG | The values of `CreateMonster` and the creators into the monster entries (D62, D63); the creators keep only logic, as hooks (D49); the post-creation changes of the packet handlers as Q-MN38 decided; `CDirection::SummonCreateMonster` takes the model from the monster table. Three parts in PR 3b: **MN4a** the core switch, **MN4b** the map creators, **MN4c** player-shaped monsters and NPCs and the merchants' part models, with the item tools learning monster references (D60). Monster recorder, creation part. Then the removal commits and one fix commit each: the bug look instead of the Bull Fighter (D64), P2, P13, P14, P27, Gayion's swords as parts, Maya without the Bull Fighter model, Molt once per map (Q-MN45). Editor: Monster Setup, Monster Equipment, the Preview additions. P2, P13–P15, P17, P24, P27. |
-| MN5 | NPC rules | MN4 | P23 fixed first; the talk rules by number into the monster entries as `talks`, `talkLevel`, `repairShop` and talk blocks (D66), replacing the code of Marlon, the Pet Trainer, the Werewolf Guardsman and the barracks Gatekeeper; the Crywolf altar contract stays a hook; the models that 0x07 runs the Crywolf hero contract rule for (`ReceiveSkillStatus` for a buff shown already: the altars, `MODEL_SMITH`, `MODEL_NPC_SERBIS`, `MODEL_MERCHANT_MAN`, `MODEL_STORAGE`, `MODEL_NPC_BREEDER`; its always-true condition fixed in sven-n/MuMain#1081) become a field or that hook of their entries, so an entry's model change no longer switches the rule; texts as translatable strings. Monster recorder, talk part. Editor: NPC rules in Monster Setup, texts in Monster Translations. P23. |
+| MN5 | NPC rules | MN4 | P23 fixed first; the talk rules by number into the monster entries as `talks`, `talkLevel`, `repairShop` and talk blocks (D66), replacing the code of Marlon, the Pet Trainer, the Werewolf Guardsman and the barracks Gatekeeper; the Crywolf altar contract stays a hook; the models that 0x07 runs the Crywolf hero contract rule for (`ReceiveSkillStatus` for a buff shown already: the altars, `MODEL_SMITH`, `MODEL_NPC_SERBIS`, `MODEL_MERCHANT_MAN`, `MODEL_STORAGE`, `MODEL_NPC_BREEDER`; its always-true condition fixed in sven-n/MuMain#1081) become their entries' field `crywolfContract`, so an entry's model change no longer switches the rule (done in 3c, with the altars' contract as their `crywolfAltarContract` talk step); texts as translatable strings. Monster recorder, talk part. Editor: NPC rules in Monster Setup, texts in Monster Translations. P23. |
| MN6 | OpenMU sync, client side | MN1, MN2, items 7 | Exchange of number and names (D53); the read-only kind, window and skill data for the checks; the skill check against `attacks`. |
| MN-B | OpenMU sync, server side | MN6 (file format) | OpenMU: monsters in the exchange pages of items phase B, with the event monster skills collected from settings and code. |
| MN7 | Sounds per animation | MN3 | The global and the 7 map `PlayMonsterSound` hooks (with the 500-unit cut-off), the trap sounds of `SetPlayerAttack` and the per-map sound loads become sound events of the models. Monster recorder, sound part. |
diff --git a/src/Localization/Editor.de.resx b/src/Localization/Editor.de.resx
index 73c7e0d5af..cd343e486a 100644
--- a/src/Localization/Editor.de.resx
+++ b/src/Localization/Editor.de.resx
@@ -5263,4 +5263,124 @@
Augen brauchen 2, 4 oder 6 Knochen (rechts, links und bis zu zwei weitere Paare), jeweils 0 oder mehr; bis dahin speichert Speichern die Monster nicht.
+
+ NPC-Regeln
+
+
+ Spricht
+
+
+ Mindeststufe zum Sprechen
+
+
+ Reparaturladen
+
+
+ Beim Sprechen, der Reihe nach
+
+
+ Dialog
+
+
+ Gesprächsanfrage senden
+
+
+ Haustier-Informationen erfragen
+
+
+ Questfenster ohne Questtitel, für
+
+
+ Anfrage
+
+
+ Wenn die Quest
+
+
+ ist in
+
+
+ Kosten in Zen
+
+
+ Ohne das Zen
+
+
+ Titel
+
+
+ Zeile hinzufügen
+
+
+ OK sendet eine Anfrage
+
+
+ Einen Zustand ankreuzen: ohne ihn sendet OK nichts.
+
+
+ Schritt hinzufügen
+
+
+ Die Texte hier sind Englisch; die anderen Sprachen stehen in Monster Translations.
+
+
+ Namen
+
+
+ Gesprächstexte
+
+
+ Alle Gesprächstexte
+
+
+ Gesprächstext {1} von {0} in {2}
+
+
+ Der Gesprächstext wurde nicht gespeichert.
+
+
+ Crywolf-Altarvertrag
+
+
+ Verbleibende Verträge (%d)
+
+
+ Die Frage
+
+
+ Auf einem Reittier
+
+
+ Bei Abbrechen
+
+
+ Altar zerstört
+
+
+ Vertrag läuft
+
+
+ Im Crywolf-Heldenvertrag
+
+
+ Geteilte Gesprächstexte
+
+
+ Gesprächstexte, die mehrere NPCs teilen, mit Namen: der Altarvertrags-Schritt von Crywolf nennt seinen Satz. Hier auf Englisch; die anderen Sprachen sind in Monster Translations.
+
+
+ Satz hinzufügen
+
+
+ Name des Satzes
+
+
+ Texte des Satzes
+
+
+ Die Texte werden in Geteilte Gesprächstexte bearbeitet.
+
+
+ Ein Gesprächsschritt nennt diesen Satz: er bleibt.
+
diff --git a/src/Localization/Editor.en.resx b/src/Localization/Editor.en.resx
index 104ec00709..078f7cae36 100644
--- a/src/Localization/Editor.en.resx
+++ b/src/Localization/Editor.en.resx
@@ -5479,4 +5479,124 @@
Eyes need 2, 4 or 6 bones (right, left and up to two more pairs), 0 or more each; until then Save refuses the monsters.
+
+ NPC rules
+
+
+ Talks
+
+
+ Minimum level to talk
+
+
+ Repair shop
+
+
+ On talk, in order
+
+
+ Dialog
+
+
+ Send the talk request
+
+
+ Ask for the pet information
+
+
+ Quest window without its title, for
+
+
+ Request
+
+
+ If the quest
+
+
+ is in
+
+
+ Zen it costs
+
+
+ Without the zen
+
+
+ Title
+
+
+ Add a line
+
+
+ OK sends a request
+
+
+ Tick a state: without one the OK sends nothing.
+
+
+ Add a step
+
+
+ The texts here are English; the other languages are in Monster Translations.
+
+
+ Names
+
+
+ Talk texts
+
+
+ All talk texts
+
+
+ talk text {1} of {0} in {2}
+
+
+ The talk text was not stored.
+
+
+ Crywolf altar contract
+
+
+ Contracts left (%d)
+
+
+ The question
+
+
+ On a mount
+
+
+ On Cancel
+
+
+ Altar destroyed
+
+
+ Contract ongoing
+
+
+ In the Crywolf hero contract
+
+
+ Shared talk texts
+
+
+ Talk texts several NPCs share, by name: a Crywolf altar contract step names its set. Typed in English here; the other languages are in Monster Translations.
+
+
+ Add a set
+
+
+ Set name
+
+
+ Texts of the set
+
+
+ The texts are edited in Shared talk texts.
+
+
+ A talk step names this set: it stays.
+
diff --git a/src/MuEditor/UI/MonsterTools/MonsterChecks.cpp b/src/MuEditor/UI/MonsterTools/MonsterChecks.cpp
index 4d37c535a4..e5e8f9ad6a 100644
--- a/src/MuEditor/UI/MonsterTools/MonsterChecks.cpp
+++ b/src/MuEditor/UI/MonsterTools/MonsterChecks.cpp
@@ -8,6 +8,7 @@
#include "MonsterGroupChecks.h"
#include "Data/DataHandler/MonsterData/MonsterDataHandler.h"
+#include "I18N/All.h"
#include "Data/DataHandler/MonsterData/MonsterJsonStorage.h"
#include "Data/GameData/ItemData/ItemDatabase.h"
#include "Data/GameData/MonsterData/MonsterDataValidation.h"
@@ -48,6 +49,11 @@ void RunMonsters(std::vector& issues)
GroupChecks::Validate(issues);
ValidateItems(issues);
Data::Monsters::ValidateMonsterSetups(AllMonsters(), Data::Monsters::GetMonstersFile().generic_string(), issues);
+ Data::Monsters::ValidateMonsterTalkSteps(AllMonsters(), g_MonsterDatabase.GetTalkTexts(),
+ Data::Monsters::GetMonstersFile().generic_string(), issues);
+ Data::Monsters::ValidateMonsterTalkTexts(AllMonsters(), g_MonsterDatabase.GetTalkTexts(),
+ I18N::GetAvailableLocales(),
+ Data::Monsters::GetMonstersFile().generic_string(), issues);
}
void RunModels(std::vector& issues)
diff --git a/src/MuEditor/UI/MonsterTools/MonsterEditHistory.cpp b/src/MuEditor/UI/MonsterTools/MonsterEditHistory.cpp
index d1f9b6b574..fb653c94c0 100644
--- a/src/MuEditor/UI/MonsterTools/MonsterEditHistory.cpp
+++ b/src/MuEditor/UI/MonsterTools/MonsterEditHistory.cpp
@@ -20,7 +20,7 @@ MonsterEditHistory::MonsterEditHistory(Hooks hooks) : m_hooks(std::move(hooks))
void MonsterEditHistory::Record(MonsterEditEntry entry)
{
if (entry.steps.empty() && entry.models.empty() && entry.soundSets.empty() && entry.groups.empty() &&
- !entry.unknownAfter)
+ !entry.unknownAfter && !entry.talkTextsAfter)
{
return;
}
@@ -138,7 +138,9 @@ bool MonsterEditHistory::IsAsLeft(const MonsterEditEntry& entry, bool undoing) c
{ return m_hooks.readSoundSet(step.name) == (undoing ? step.after : step.before); });
const bool unknownAsLeft = !entry.unknownAfter || !m_hooks.readUnknownLook ||
m_hooks.readUnknownLook() == *(undoing ? entry.unknownAfter : entry.unknownBefore);
- return monstersAsLeft && modelsAsLeft && setsAsLeft && unknownAsLeft &&
+ const bool talkTextsAsLeft = !entry.talkTextsAfter || !m_hooks.readTalkTexts ||
+ m_hooks.readTalkTexts() == *(undoing ? entry.talkTextsAfter : entry.talkTextsBefore);
+ return monstersAsLeft && modelsAsLeft && setsAsLeft && unknownAsLeft && talkTextsAsLeft &&
std::ranges::all_of(entry.groups, [&](const MonsterGroupEditStep& step)
{ return m_hooks.readGroup(step.id) == (undoing ? step.after : step.before); });
}
@@ -150,6 +152,10 @@ void MonsterEditHistory::Apply(const MonsterEditEntry& entry, bool undoing)
{
m_hooks.restoreUnknownLook(*(undoing ? entry.unknownBefore : entry.unknownAfter));
}
+ if (entry.talkTextsAfter && m_hooks.restoreTalkTexts)
+ {
+ m_hooks.restoreTalkTexts(*(undoing ? entry.talkTextsBefore : entry.talkTextsAfter));
+ }
if (!entry.steps.empty())
{
std::vector snapshots;
diff --git a/src/MuEditor/UI/MonsterTools/MonsterEditHistory.h b/src/MuEditor/UI/MonsterTools/MonsterEditHistory.h
index 19cd146e7a..ae5676c1ac 100644
--- a/src/MuEditor/UI/MonsterTools/MonsterEditHistory.h
+++ b/src/MuEditor/UI/MonsterTools/MonsterEditHistory.h
@@ -76,6 +76,10 @@ struct MonsterEditEntry
// entry changed it.
std::optional unknownBefore;
std::optional unknownAfter;
+ // The shared talk texts before and after; both set when the entry
+ // changed them.
+ std::optional talkTextsBefore;
+ std::optional talkTextsAfter;
// Made on a monster with several numbers as a whole (its shared values):
// its undo selects the monster as a whole again.
bool wholeMonster = false;
@@ -110,6 +114,9 @@ class MonsterEditHistory
// that change it.
std::function readUnknownLook;
std::function restoreUnknownLook;
+ // The same for the shared talk texts.
+ std::function readTalkTexts;
+ std::function restoreTalkTexts;
};
// Why the last Undo or Redo did nothing.
diff --git a/src/MuEditor/UI/MonsterTools/MonsterEdits.cpp b/src/MuEditor/UI/MonsterTools/MonsterEdits.cpp
index 9318c524f5..0851c3cff1 100644
--- a/src/MuEditor/UI/MonsterTools/MonsterEdits.cpp
+++ b/src/MuEditor/UI/MonsterTools/MonsterEdits.cpp
@@ -151,13 +151,30 @@ MonsterEdits::MonsterEdits()
[](std::span snapshots) { g_MonsterDataHandler.RestoreModels(snapshots); },
[](const std::string& name) { return g_MonsterDataHandler.ReadSoundSet(name); },
[](std::span snapshots)
- { g_MonsterDataHandler.RestoreSoundSets(snapshots); }, [](int id)
- { return g_MonsterDataHandler.ReadGroup(id); }, [](std::span snapshots)
- { g_MonsterDataHandler.RestoreGroups(snapshots); }, [] { return g_MonsterDatabase.GetUnknownLook(); },
- [](const Data::Monsters::UnknownMonsterLook& look) { g_MonsterDatabase.SetUnknownLook(look); }})
+ { g_MonsterDataHandler.RestoreSoundSets(snapshots); },
+ [](int id) { return g_MonsterDataHandler.ReadGroup(id); },
+ [](std::span snapshots) { g_MonsterDataHandler.RestoreGroups(snapshots); },
+ [] { return g_MonsterDatabase.GetUnknownLook(); }, [](const Data::Monsters::UnknownMonsterLook& look)
+ { g_MonsterDatabase.SetUnknownLook(look); }, [] { return g_MonsterDatabase.GetTalkTexts(); },
+ [](const Data::Monsters::MonsterTalkTexts& texts) { g_MonsterDatabase.SetTalkTexts(texts); }})
{
}
+bool MonsterEdits::SetTalkTexts(std::string label, const Data::Monsters::MonsterTalkTexts& texts)
+{
+ const Data::Monsters::MonsterTalkTexts before = g_MonsterDatabase.GetTalkTexts();
+ if (before == texts)
+ {
+ return false;
+ }
+ g_MonsterDatabase.SetTalkTexts(texts);
+ MonsterEditEntry entry{std::move(label), {}, {}, {}, {}};
+ entry.talkTextsBefore = before;
+ entry.talkTextsAfter = texts;
+ m_history.Record(std::move(entry));
+ return true;
+}
+
bool MonsterEdits::SetUnknownLook(std::string label, const Data::Monsters::UnknownMonsterLook& look)
{
const Data::Monsters::UnknownMonsterLook before = g_MonsterDatabase.GetUnknownLook();
diff --git a/src/MuEditor/UI/MonsterTools/MonsterEdits.h b/src/MuEditor/UI/MonsterTools/MonsterEdits.h
index 38dd24fecf..aae370d723 100644
--- a/src/MuEditor/UI/MonsterTools/MonsterEdits.h
+++ b/src/MuEditor/UI/MonsterTools/MonsterEdits.h
@@ -34,6 +34,9 @@ class MonsterEdits
// when it is so already. Living monsters with the look show its glow,
// marks and message at once, its body at their next spawn.
bool SetUnknownLook(std::string label, const Data::Monsters::UnknownMonsterLook& look);
+ // The talk texts several NPCs share (the list's talkTexts), saved with
+ // the monster list; false when they are so already.
+ bool SetTalkTexts(std::string label, const Data::Monsters::MonsterTalkTexts& texts);
// Sets the monster's name in `locale`; an empty text removes the
// translation. False, with nothing changed, when the number has no entry,
// the locale is not a language code, the text has the separator
diff --git a/src/MuEditor/UI/MonsterTools/MonsterSetupFields.cpp b/src/MuEditor/UI/MonsterTools/MonsterSetupFields.cpp
index a8af742846..0ab20f7a7c 100644
--- a/src/MuEditor/UI/MonsterTools/MonsterSetupFields.cpp
+++ b/src/MuEditor/UI/MonsterTools/MonsterSetupFields.cpp
@@ -64,6 +64,12 @@ constexpr std::array Fields{
Field{"gate", &I18N::Editor::CastleGate, GateAndHooks, NextSpawn},
Field{"hooks", &I18N::Editor::Hooks, GateAndHooks, NextSpawn},
Field{"stateVariants", &I18N::Editor::StateVariants, GateAndHooks, AtOnce},
+ // The talk rules are read when the hero talks to the NPC.
+ Field{"talks", &I18N::Editor::Talks, NpcRules, AtOnce},
+ Field{"talkLevel", &I18N::Editor::TalkLevel, NpcRules, AtOnce},
+ Field{"repairShop", &I18N::Editor::RepairShop, NpcRules, AtOnce},
+ Field{"onTalk", &I18N::Editor::OnTalk, NpcRules, AtOnce},
+ Field{"crywolfContract", &I18N::Editor::CrywolfHeroContract, NpcRules, AtOnce},
};
} // namespace
diff --git a/src/MuEditor/UI/MonsterTools/MonsterSetupFields.h b/src/MuEditor/UI/MonsterTools/MonsterSetupFields.h
index 3240594d6d..54c3abc3ae 100644
--- a/src/MuEditor/UI/MonsterTools/MonsterSetupFields.h
+++ b/src/MuEditor/UI/MonsterTools/MonsterSetupFields.h
@@ -21,6 +21,7 @@ enum class Section
Appear,
Behaviour,
GateAndHooks,
+ NpcRules,
};
// When a living monster of the number takes a change (D55): at once (its
diff --git a/src/MuEditor/UI/MonsterTools/MonsterSetupLists.cpp b/src/MuEditor/UI/MonsterTools/MonsterSetupLists.cpp
index 470ac868c6..744b0175b9 100644
--- a/src/MuEditor/UI/MonsterTools/MonsterSetupLists.cpp
+++ b/src/MuEditor/UI/MonsterTools/MonsterSetupLists.cpp
@@ -4,10 +4,14 @@
#include "MonsterSetupLists.h"
+#include "TalkStepStates.h"
+
#include "../MuEditor/UI/ItemTools/ItemFieldWidgets.h"
#include "../MuEditor/UI/ItemTools/ItemTextInput.h"
#include "Data/GameData/EffectData/EffectTypeSymbols.h"
+#include "Data/GameData/ItemData/ItemJsonFormat.h"
#include "Data/GameData/MonsterData/MonsterAnimations.h"
+#include "Data/GameData/MonsterData/MonsterDatabase.h"
#include "Data/GameData/MonsterData/MonsterSoundIds.h"
#include "Data/GameData/MonsterData/MonsterModelDatabase.h"
#include "I18N/All.h"
@@ -16,8 +20,10 @@
#include
#include
#include
+#include
#include
#include
+#include
#include
namespace MuEditor::Monsters::SetupLists
@@ -669,6 +675,319 @@ bool FacingByTile(std::vector& tiles, const A
}
return changed;
}
+namespace
+{
+using Data::Monsters::MonsterTalkBlock;
+using TalkKind = MonsterTalkBlock::Kind;
+
+struct TalkKindEntry
+{
+ TalkKind kind;
+ const char* const* name;
+};
+
+const std::array TalkKindNames{{
+ {TalkKind::Dialog, &I18N::Editor::TalkDialog},
+ {TalkKind::SendTalk, &I18N::Editor::SendTheTalkRequest},
+ {TalkKind::SendPetInfo, &I18N::Editor::AskForPetInformation},
+ {TalkKind::Message, &I18N::Editor::Message},
+ {TalkKind::QuestWindow, &I18N::Editor::QuestWindowWithoutItsTitle},
+ {TalkKind::CrywolfAltarContract, &I18N::Editor::CrywolfAltarContract},
+}};
+
+const char* TalkKindName(TalkKind kind)
+{
+ const auto found = std::ranges::find(TalkKindNames, kind, &TalkKindEntry::kind);
+ return found != TalkKindNames.end() ? *found->name : "";
+}
+
+// The English text; the other languages are edited in Monster Translations.
+void EnglishText(const char* label, Data::LocalizedString& text, float numbers, const AfterField& afterField)
+{
+ std::string english = text.GetNeutral();
+ ImGui::SetNextItemWidth(Width(numbers));
+ if (Items::InputString(label, english))
+ text.Set(Data::LocalizedString::NeutralLocale, english);
+ afterField();
+}
+
+// A combo of the names; true when another was picked.
+bool NameCombo(const char* label, std::string& value, std::span names, bool allowNone)
+{
+ bool changed = false;
+ ImGui::SetNextItemWidth(Width(1.6f));
+ if (ImGui::BeginCombo(label, value.empty() ? I18N::Editor::None : value.c_str()))
+ {
+ if (allowNone && ImGui::Selectable(I18N::Editor::None, value.empty()) && !value.empty())
+ {
+ value.clear();
+ changed = true;
+ }
+ for (const std::string_view name : names)
+ {
+ if (ImGui::Selectable(std::string(name).c_str(), name == value) && name != value)
+ {
+ value = std::string(name);
+ changed = true;
+ }
+ }
+ ImGui::EndCombo();
+ }
+ return changed;
+}
+
+bool TalkOk(Data::Monsters::MonsterTalkOk& ok, const AfterField& afterField)
+{
+ bool changed = NameCombo(I18N::Editor::Request, ok.request, Data::Monsters::GetTalkRequests(), false);
+ if (NameCombo(I18N::Editor::IfTheQuest, ok.quest, Data::Monsters::GetTalkQuests(), true))
+ {
+ // Without a quest there are no states to keep.
+ if (ok.quest.empty())
+ ok.questStates.clear();
+ changed = true;
+ }
+ if (!ok.quest.empty())
+ {
+ ImGui::TextUnformatted(I18N::Editor::IsIn);
+ for (const std::string_view state : Data::Monsters::GetTalkQuestStates())
+ {
+ ImGui::SameLine();
+ const auto found = std::ranges::find(ok.questStates, state);
+ bool ticked = found != ok.questStates.end();
+ if (ImGui::Checkbox(std::string(state).c_str(), &ticked))
+ {
+ if (ticked)
+ ok.questStates.emplace_back(state);
+ else
+ ok.questStates.erase(found);
+ changed = true;
+ }
+ }
+ if (ok.questStates.empty())
+ ImGui::TextColored(WarningColor, "%s", I18N::Editor::TickAStateWithoutOneTheOKSendsNothing);
+ }
+ int zen = static_cast(ok.zen);
+ ImGui::SetNextItemWidth(Width(1.2f));
+ if (ImGui::InputInt(I18N::Editor::ZenItCosts, &zen, 0))
+ ok.zen = static_cast(std::max(zen, 0));
+ afterField();
+ EnglishText(I18N::Editor::WithoutTheZen, ok.failure, 4.0f, afterField);
+ return changed;
+}
+
+bool TalkDialog(Data::Monsters::MonsterTalkDialog& dialog, const AfterField& afterField)
+{
+ bool changed = false;
+ EnglishText(I18N::Editor::DialogTitle, dialog.title, 3.0f, afterField);
+ for (size_t index = 0; index < dialog.lines.size(); ++index)
+ {
+ ImGui::PushID(static_cast(index));
+ EnglishText("##Line", dialog.lines[index], 5.0f, afterField);
+ ImGui::SameLine();
+ const bool remove = ImGui::SmallButton("x");
+ ImGui::PopID();
+ if (remove)
+ {
+ dialog.lines.erase(dialog.lines.begin() + static_cast(index));
+ return true;
+ }
+ }
+ if (ImGui::SmallButton(I18N::Editor::AddALine))
+ {
+ dialog.lines.emplace_back();
+ changed = true;
+ }
+ bool hasOk = dialog.ok.has_value();
+ if (ImGui::Checkbox(I18N::Editor::OKSendsARequest, &hasOk))
+ {
+ dialog.ok = hasOk ? std::optional(
+ Data::Monsters::MonsterTalkOk{std::string(Data::Monsters::GetTalkRequests().front())})
+ : std::nullopt;
+ changed = true;
+ }
+ if (dialog.ok)
+ {
+ ImGui::Indent();
+ changed |= TalkOk(*dialog.ok, afterField);
+ ImGui::Unindent();
+ }
+ return changed;
+}
+
+bool TalkBlockFields(MonsterTalkBlock& block, const AfterField& afterField)
+{
+ bool changed = false;
+ ImGui::SetNextItemWidth(Width(2.5f));
+ if (ImGui::BeginCombo(I18N::Editor::Kind, TalkKindName(block.kind)))
+ {
+ for (const TalkKindEntry& kind : TalkKindNames)
+ {
+ if (ImGui::Selectable(*kind.name, kind.kind == block.kind) && kind.kind != block.kind)
+ {
+ // A step of the new kind, with none of the old one's values;
+ // an altar contract shows the first shared set.
+ block = MonsterTalkBlock{};
+ block.kind = kind.kind;
+ if (block.kind == TalkKind::CrywolfAltarContract && !g_MonsterDatabase.GetTalkTexts().empty())
+ block.altarTexts = g_MonsterDatabase.GetTalkTexts().begin()->first;
+ changed = true;
+ }
+ }
+ ImGui::EndCombo();
+ }
+ switch (block.kind)
+ {
+ case TalkKind::Dialog:
+ changed |= TalkDialog(block.dialog, afterField);
+ break;
+ case TalkKind::Message:
+ EnglishText("##Message", block.message, 5.0f, afterField);
+ break;
+ case TalkKind::QuestWindow:
+ {
+ const std::span classes = Data::Items::GetClassKeys();
+ for (size_t index = 0; index < classes.size(); ++index)
+ {
+ const int classIndex = static_cast(index);
+ const auto found = std::ranges::find(block.noQuestTitleFor, classIndex);
+ bool ticked = found != block.noQuestTitleFor.end();
+ if (index > 0)
+ ImGui::SameLine();
+ if (ImGui::Checkbox(classes[index], &ticked))
+ {
+ if (ticked)
+ block.noQuestTitleFor.push_back(classIndex);
+ else
+ block.noQuestTitleFor.erase(found);
+ changed = true;
+ }
+ }
+ break;
+ }
+ case TalkKind::CrywolfAltarContract:
+ {
+ // The map's code makes the contract; its texts are a shared set.
+ std::vector names;
+ for (const auto& [name, contract] : g_MonsterDatabase.GetTalkTexts())
+ names.emplace_back(name);
+ changed |= NameCombo(I18N::Editor::TextsOfTheSet, block.altarTexts, names, false);
+ ImGui::TextDisabled("%s", I18N::Editor::TheTextsAreEditedInSharedTalkTexts);
+ break;
+ }
+ case TalkKind::SendTalk:
+ case TalkKind::SendPetInfo:
+ break;
+ }
+ return changed;
+}
+// What tells two steps apart: the kind, the English texts and the values.
+// Two equal steps may share it; they are interchangeable.
+unsigned int StepSignature(const MonsterTalkBlock& block)
+{
+ std::string text = std::to_string(static_cast(block.kind));
+ const auto add = [&](std::string_view part)
+ {
+ text += '\x1f';
+ text += part;
+ };
+ add(block.message.GetNeutral());
+ add(block.dialog.title.GetNeutral());
+ for (const Data::LocalizedString& line : block.dialog.lines)
+ add(line.GetNeutral());
+ if (block.dialog.ok)
+ {
+ add(block.dialog.ok->request);
+ add(block.dialog.ok->quest);
+ for (const std::string& state : block.dialog.ok->questStates)
+ add(state);
+ add(std::to_string(block.dialog.ok->zen));
+ add(block.dialog.ok->failure.GetNeutral());
+ }
+ for (const int classIndex : block.noQuestTitleFor)
+ add(std::to_string(classIndex));
+ add(block.altarTexts);
+ return static_cast(std::hash{}(text));
+}
+
+} // namespace
+
+bool TalkBlocks(std::vector& blocks, const AfterField& afterField)
+{
+ ImGui::TextDisabled("%s", I18N::Editor::TalkTextsNote);
+ // Each step's tree node is keyed by an id of its own, which follows it
+ // when steps move, go or come here, and is found again by its signature
+ // after a change made elsewhere (undo, redo).
+ TalkStepStates states(blocks.size());
+ std::vector signatures;
+ for (const MonsterTalkBlock& block : blocks)
+ signatures.push_back(StepSignature(block));
+ const std::vector ids = MatchStepIds(states.Before(), signatures, [&] { return states.NewId(); });
+ for (size_t row = 0; row < ids.size(); ++row)
+ states.SetId(row, ids[row]);
+ const auto done = [&](bool changed)
+ {
+ if (changed)
+ {
+ signatures.clear();
+ for (const MonsterTalkBlock& block : blocks)
+ signatures.push_back(StepSignature(block));
+ }
+ states.Leave(signatures);
+ return changed;
+ };
+
+ for (size_t index = 0; index < blocks.size(); ++index)
+ {
+ ImGui::PushID(states.Id(index));
+ const std::string title = std::to_string(index + 1) + ". " + TalkKindName(blocks[index].kind);
+ const bool open = ImGui::TreeNode("##Step", "%s", title.c_str());
+ ImGui::SameLine();
+ std::optional moveTo;
+ if (index > 0 && ImGui::SmallButton(I18N::Editor::MoveUp))
+ moveTo = index - 1;
+ ImGui::SameLine();
+ if (index + 1 < blocks.size() && ImGui::SmallButton(I18N::Editor::MoveDown))
+ moveTo = index + 1;
+ ImGui::SameLine();
+ const bool remove = ImGui::SmallButton("x");
+ bool changed = false;
+ if (open)
+ {
+ if (!moveTo && !remove)
+ changed = TalkBlockFields(blocks[index], afterField);
+ ImGui::TreePop();
+ }
+ ImGui::PopID();
+ if (moveTo)
+ {
+ // The two steps change rows with their ids.
+ std::swap(blocks[index], blocks[*moveTo]);
+ const int idHere = states.Id(index);
+ states.SetId(index, states.Id(*moveTo));
+ states.SetId(*moveTo, idHere);
+ return done(true);
+ }
+ if (remove)
+ {
+ // The later steps move up a row with their ids.
+ for (size_t row = index; row + 1 < blocks.size(); ++row)
+ states.SetId(row, states.Id(row + 1));
+ blocks.erase(blocks.begin() + static_cast(index));
+ return done(true);
+ }
+ if (changed)
+ return done(true);
+ }
+ if (ImGui::SmallButton(I18N::Editor::AddAStep))
+ {
+ // A new step has a new id, so it starts closed.
+ states.SetId(blocks.size(), states.NewId());
+ blocks.emplace_back();
+ return done(true);
+ }
+ return done(false);
+}
+
} // namespace MuEditor::Monsters::SetupLists
#endif // _EDITOR
diff --git a/src/MuEditor/UI/MonsterTools/MonsterSetupLists.h b/src/MuEditor/UI/MonsterTools/MonsterSetupLists.h
index adb8c81f74..ebed126add 100644
--- a/src/MuEditor/UI/MonsterTools/MonsterSetupLists.h
+++ b/src/MuEditor/UI/MonsterTools/MonsterSetupLists.h
@@ -9,6 +9,7 @@
#include
#include
+#include
#include
#include
#include
@@ -103,6 +104,11 @@ int& TileOnAxis(Data::Monsters::MonsterTileFacing& tile);
// How it comes in (Q-MN38c): the animation, blend, start frame, fade in,
// appear time, sound and the effects where it comes in.
bool Appear(Data::Monsters::MonsterAppear& appear, const AfterField& afterField);
+// An NPC's talk steps in order (D66): a row per step, opened to its values
+// (a dialog's title, lines and OK, a message, the classes of a quest window),
+// with up, down and remove; Add a step. Texts are edited in English.
+bool TalkBlocks(std::vector& blocks, const AfterField& afterField);
+
// A castle gate's animations closed, opening and open, and what an open gate
// coming into view plays.
bool Gate(Data::Monsters::MonsterGate& gate);
diff --git a/src/MuEditor/UI/MonsterTools/MonsterTalkTextRows.cpp b/src/MuEditor/UI/MonsterTools/MonsterTalkTextRows.cpp
new file mode 100644
index 0000000000..0765b6c6e8
--- /dev/null
+++ b/src/MuEditor/UI/MonsterTools/MonsterTalkTextRows.cpp
@@ -0,0 +1,271 @@
+#include "stdafx.h"
+
+#ifdef _EDITOR
+
+#include "MonsterTalkTextRows.h"
+
+#include "MonsterEdits.h"
+#include "MonsterOwnValues.h"
+#include "MonsterSelection.h"
+#include "MonsterShownValues.h"
+
+#include "../MuEditor/Core/MuEditorCore.h"
+#include "../MuEditor/UI/Common/MuEditorWindowIds.h"
+#include "../MuEditor/UI/Console/MuEditorConsoleUI.h"
+#include "Data/DataHandler/MonsterData/MonsterDataHandler.h"
+#include "Data/GameData/MonsterData/MonsterDatabase.h"
+#include "Data/GameData/MonsterData/MonsterSetup.h"
+#include "I18N/All.h"
+#include "imgui.h"
+
+#include
+#include
+#include
+#include
+
+namespace MuEditor::Monsters
+{
+namespace
+{
+using ShownValues::WholeMonsterKey;
+
+int NumberOf(int key)
+{
+ return key >= WholeMonsterKey ? key - WholeMonsterKey : key;
+}
+
+std::string Lower(std::string_view text)
+{
+ std::string lower(text);
+ std::ranges::transform(lower, lower.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); });
+ return lower;
+}
+
+using TalkTexts = std::vector>;
+
+// The texts of a key's talk with their fields, its map and state variants'
+// included.
+TalkTexts TextsOf(int key)
+{
+ TalkTexts texts;
+ const std::optional shown = ShownValues::Load(key);
+ if (!shown)
+ return texts;
+ Data::Monsters::ForEachMonsterTalkText(shown->monster,
+ [&](const Data::LocalizedString& text, const std::string& field)
+ { texts.emplace_back(field, text); });
+ return texts;
+}
+} // namespace
+
+std::vector MonsterTalkTextRows::CollectRows() const
+{
+ std::vector rows;
+ const auto add = [&](int key, TalkTexts texts)
+ {
+ for (auto& [field, text] : texts)
+ rows.push_back({static_cast(rows.size()), key, std::move(field), std::move(text)});
+ };
+ TalkTexts shared;
+ Data::Monsters::ForEachSharedTalkText(g_MonsterDatabase.GetTalkTexts(),
+ [&](const Data::LocalizedString& text, const std::string& field)
+ { shared.emplace_back(field, text); });
+ add(SharedKey, std::move(shared));
+ for (const std::vector& numbers : g_MonsterEdits.ReadMonsters())
+ {
+ if (numbers.size() < 2)
+ {
+ add(numbers.front(), TextsOf(numbers.front()));
+ continue;
+ }
+ // The monster's texts, then those of a number whose talk is its own.
+ const int whole = numbers.front() + WholeMonsterKey;
+ const TalkTexts shared = TextsOf(whole);
+ add(whole, shared);
+ for (const int member : numbers)
+ {
+ TalkTexts own = TextsOf(member);
+ if (own != shared)
+ add(member, std::move(own));
+ }
+ }
+ return rows;
+}
+
+void MonsterTalkTextRows::Build() const
+{
+ if (m_builtVersion == GetVersion())
+ return;
+ m_all = CollectRows();
+ m_builtVersion = GetVersion();
+}
+
+const std::vector& MonsterTalkTextRows::List(std::string_view search, std::string_view missingLocale)
+{
+ Build();
+ if (m_listed && m_listedVersion == m_builtVersion && search == m_search && missingLocale == m_missingLocale)
+ return m_rows;
+ m_search.assign(search);
+ m_missingLocale.assign(missingLocale);
+ m_rows.clear();
+ const std::string needle = Lower(search);
+ for (const Row& row : m_all)
+ {
+ if (!m_missingLocale.empty() && row.text.HasOwn(m_missingLocale))
+ continue;
+ char id[96];
+ FormatId(row.id, id, sizeof(id));
+ if (!needle.empty() && Lower(id).find(needle) == std::string::npos &&
+ Lower(row.text.GetNeutral()).find(needle) == std::string::npos &&
+ (row.key == SharedKey ||
+ Lower(ShownValues::MonsterName(NumberOf(row.key))).find(needle) == std::string::npos))
+ continue;
+ m_rows.push_back(row.id);
+ }
+ m_listed = true;
+ m_listedVersion = m_builtVersion;
+ return m_rows;
+}
+
+void MonsterTalkTextRows::Refresh()
+{
+ m_listed = false;
+}
+
+int MonsterTalkTextRows::GetVersion() const
+{
+ return g_MonsterDatabase.GetVersion() + g_MonsterDataHandler.GetGroupsVersion() + OwnValues::GetUnlockVersion();
+}
+
+std::vector MonsterTalkTextRows::CountMissing(std::span locales) const
+{
+ Build();
+ std::vector missing(locales.size(), 0);
+ for (const Row& row : m_all)
+ {
+ for (size_t index = 0; index < locales.size(); ++index)
+ missing[index] += row.text.HasOwn(locales[index]) ? 0 : 1;
+ }
+ return missing;
+}
+
+const Data::LocalizedString* MonsterTalkTextRows::FindNames(int id) const
+{
+ Build();
+ return id >= 0 && id < static_cast(m_all.size()) ? &m_all[static_cast(id)].text : nullptr;
+}
+
+void MonsterTalkTextRows::FormatId(int id, char* buffer, std::size_t size) const
+{
+ Build();
+ if (id < 0 || id >= static_cast(m_all.size()))
+ {
+ std::snprintf(buffer, size, "?");
+ return;
+ }
+ // "408 onTalk[1].dialog.lines[0]"; a monster with several numbers by its
+ // first one and how many it has.
+ const Row& row = m_all[static_cast(id)];
+ if (row.key == SharedKey)
+ std::snprintf(buffer, size, "%s", row.field.c_str());
+ else if (row.key >= WholeMonsterKey)
+ std::snprintf(buffer, size, "%d (%d) %s", NumberOf(row.key),
+ g_MonsterEdits.CountMonsterNumbers(NumberOf(row.key)), row.field.c_str());
+ else
+ std::snprintf(buffer, size, "%d %s", row.key, row.field.c_str());
+}
+
+void MonsterTalkTextRows::CommitName(int id, const char* locale, const char* text)
+{
+ Build();
+ if (id < 0 || id >= static_cast(m_all.size()))
+ return;
+ const Row row = m_all[static_cast(id)];
+ if (row.key == SharedKey)
+ {
+ Data::Monsters::MonsterTalkTexts texts = g_MonsterDatabase.GetTalkTexts();
+ bool changed = false;
+ Data::Monsters::ForEachSharedTalkText(texts,
+ [&](Data::LocalizedString& talkText, const std::string& field)
+ {
+ if (field == row.field && !changed)
+ {
+ talkText.Set(locale, text);
+ changed = true;
+ }
+ });
+ const std::string label = I18N::Format(I18N::Editor::StepTalkText, {I18N::Editor::SharedTalkTexts, row.field,
+ I18N::GetLanguageDisplayName(locale)});
+ if (changed)
+ g_MonsterEdits.SetTalkTexts(label, texts);
+ return;
+ }
+ std::optional shown = ShownValues::Load(row.key);
+ if (!shown)
+ return;
+ bool found = false;
+ Data::Monsters::ForEachMonsterTalkText(shown->monster,
+ [&](Data::LocalizedString& talkText, const std::string& field)
+ {
+ if (field == row.field && !found)
+ {
+ talkText.Set(locale, text);
+ found = true;
+ }
+ });
+ if (!found)
+ return;
+ const std::string label = I18N::Format(
+ I18N::Editor::StepTalkText, {ShownValues::LogName(*shown), row.field, I18N::GetLanguageDisplayName(locale)});
+ if (!ShownValues::Store(label, *shown))
+ g_MuEditorConsoleUI.LogEditor(I18N::Editor::TheTalkTextWasNotStored);
+}
+
+int MonsterTalkTextRows::GetSelected() const
+{
+ Build();
+ // The first text of the selected monster's key.
+ const int key = ShownValues::GetSelectedKey();
+ if (key < 0)
+ return -1;
+ const auto found = std::ranges::find(m_all, key, &Row::key);
+ return found != m_all.end() ? found->id : -1;
+}
+
+int MonsterTalkTextRows::GetSelectionSerial() const
+{
+ return g_MonsterSelection.GetSerial();
+}
+
+void MonsterTalkTextRows::Select(int id)
+{
+ Build();
+ if (id < 0 || id >= static_cast(m_all.size()))
+ return;
+ const int key = m_all[static_cast(id)].key;
+ if (key == SharedKey)
+ return;
+ if (key >= WholeMonsterKey)
+ g_MonsterSelection.SelectMonster(NumberOf(key));
+ else
+ g_MonsterSelection.Select(key);
+}
+
+void MonsterTalkTextRows::ShowBrowser()
+{
+ g_MuEditorCore.GetToolWindows().monsterBrowser = true;
+ ImGui::SetWindowFocus(WindowIds::MonsterBrowser);
+}
+
+const char* MonsterTalkTextRows::GetSelectTooltip() const
+{
+ return I18N::Editor::SelectTheMonsterAndShowTheMonsterBrowser;
+}
+
+const char* MonsterTalkTextRows::GetAllRowsLabel() const
+{
+ return I18N::Editor::AllTalkTexts;
+}
+} // namespace MuEditor::Monsters
+
+#endif // _EDITOR
diff --git a/src/MuEditor/UI/MonsterTools/MonsterTalkTextRows.h b/src/MuEditor/UI/MonsterTools/MonsterTalkTextRows.h
new file mode 100644
index 0000000000..291a0e2ab9
--- /dev/null
+++ b/src/MuEditor/UI/MonsterTools/MonsterTalkTextRows.h
@@ -0,0 +1,62 @@
+#pragma once
+
+#ifdef _EDITOR
+
+#include "../MuEditor/UI/Common/NamesGridRows.h"
+#include "Data/GameData/Common/LocalizedString.h"
+
+#include