1 // ============================================================================
2 // File Name: on_player_chat
3 // Generative Agent "Sense" Phase - Placed on Module OnPlayerChat
4 // ============================================================================
6 #include "nwnx_redis_lib"
10 object oSpeaker = GetPCChatSpeaker();
11 string sMessage = GetPCChatMessage();
12 int nVolume = GetPCChatVolume();
14 if (nVolume != TALKVOLUME_TALK) return;
16 // ==========================================================
17 // 1. THE PHYSICAL EMOTE SYSTEM (/commands)
18 // ==========================================================
19 string sPhysicalEmote = "";
20 string sLowerMsg = GetStringLowerCase(sMessage);
22 if (GetStringLeft(sLowerMsg, 1) == "/") {
23 if (sLowerMsg == "/bow") {
24 AssignCommand(oSpeaker, ActionPlayAnimation(ANIMATION_FIREFORGET_BOW));
25 sPhysicalEmote = "[The player physically bows respectfully to you.]";
27 else if (sLowerMsg == "/laugh") {
28 AssignCommand(oSpeaker, ActionPlayAnimation(ANIMATION_LOOPING_TALK_LAUGHING, 1.0, 3.0));
29 sPhysicalEmote = "[The player is physically laughing out loud.]";
31 else if (sLowerMsg == "/taunt" || sLowerMsg == "/threaten") {
32 AssignCommand(oSpeaker, ActionPlayAnimation(ANIMATION_FIREFORGET_TAUNT));
33 sPhysicalEmote = "[The player steps forward and physically taunts you.]";
35 else if (sLowerMsg == "/wave" || sLowerMsg == "/greet") {
36 AssignCommand(oSpeaker, ActionPlayAnimation(ANIMATION_FIREFORGET_GREETING));
37 sPhysicalEmote = "[The player physically waves hello to you.]";
39 else if (sLowerMsg == "/cheer") {
40 AssignCommand(oSpeaker, ActionPlayAnimation(ANIMATION_FIREFORGET_VICTORY1));
41 sPhysicalEmote = "[The player is cheering and celebrating.]";
43 else if (sLowerMsg == "/drunk") {
44 AssignCommand(oSpeaker, ActionPlayAnimation(ANIMATION_LOOPING_PAUSE_DRUNK, 1.0, 6.0));
45 sPhysicalEmote = "[The player is swaying physically, acting heavily intoxicated.]";
51 // ==========================================================
52 // 2. EXTRACT PLAYER BODY LANGUAGE & STATE
53 // ==========================================================
54 string sPlayerState = "Relaxed and unarmed.";
55 object oRightHand = GetItemInSlot(INVENTORY_SLOT_RIGHTHAND, oSpeaker);
56 object oLeftHand = GetItemInSlot(INVENTORY_SLOT_LEFTHAND, oSpeaker);
59 if (GetIsObjectValid(oRightHand)) {
60 int nType = GetBaseItemType(oRightHand);
61 if (nType != BASE_ITEM_TORCH && nType != BASE_ITEM_BLANK_POTION && nType != BASE_ITEM_SPELLSCROLL) bArmed = TRUE;
63 if (GetIsObjectValid(oLeftHand) && GetBaseItemType(oLeftHand) != BASE_ITEM_TORCH) bArmed = TRUE;
65 if (bArmed) sPlayerState += " They have a weapon drawn in their hands!";
66 if (sPhysicalEmote != "") sPlayerState += " " + sPhysicalEmote;
68 string sWorldState = GetLocalString(GetModule(), "LLM_WORLD_STATE");
70 // ==========================================================
71 // 3. THE RADIUS SCANNER (Group Conversations)
72 // ==========================================================
74 object oTarget = GetNearestObject(OBJECT_TYPE_CREATURE | OBJECT_TYPE_PLACEABLE, oSpeaker, nNth);
76 while (GetIsObjectValid(oTarget) && GetDistanceBetween(oSpeaker, oTarget) <= 5.0f)
78 string sPersona = GetLocalString(oTarget, "LLM_PERSONA");
79 if (sPersona == "") sPersona = GetLocalString(oTarget, "LLM_PROMPT");
83 // --- STRATEGY OVERRIDE: Prevent Maestros from reacting to player chat ---
84 int nStrategy = GetLocalInt(oTarget, "LLM_STRATEGY");
85 if (nStrategy == 0) nStrategy = 1;
89 oTarget = GetNearestObject(OBJECT_TYPE_CREATURE | OBJECT_TYPE_PLACEABLE, oSpeaker, nNth);
93 // --- COOLDOWN CHECK ---
94 if (GetLocalInt(oTarget, "LLM_CHAT_COOLDOWN") == TRUE) {
96 oTarget = GetNearestObject(OBJECT_TYPE_CREATURE | OBJECT_TYPE_PLACEABLE, oSpeaker, nNth);
100 SetLocalInt(oTarget, "LLM_CHAT_COOLDOWN", TRUE);
101 DelayCommand(6.0f, SetLocalInt(oTarget, "LLM_CHAT_COOLDOWN", FALSE));
103 ApplyEffectToObject(DURATION_TYPE_INSTANT, EffectVisualEffect(VFX_IMP_HEAD_MIND), oTarget);
104 SendMessageToPC(oSpeaker, "[" + GetName(oTarget) + " is listening...]");
106 string sLawChaos = "Neutral";
107 int nLawChaos = GetAlignmentLawChaos(oTarget);
108 if (nLawChaos == ALIGNMENT_LAWFUL) sLawChaos = "Lawful";
109 else if (nLawChaos == ALIGNMENT_CHAOTIC) sLawChaos = "Chaotic";
111 string sGoodEvil = "Neutral";
112 int nGoodEvil = GetAlignmentGoodEvil(oTarget);
113 if (nGoodEvil == ALIGNMENT_GOOD) sGoodEvil = "Good";
114 else if (nGoodEvil == ALIGNMENT_EVIL) sGoodEvil = "Evil";
116 string sAlignment = sLawChaos + " " + sGoodEvil;
117 if (sAlignment == "Neutral Neutral") sAlignment = "True Neutral";
119 string sGender = "Unknown";
120 int nGender = GetGender(oTarget);
121 if (nGender == GENDER_MALE) sGender = "Male";
122 else if (nGender == GENDER_FEMALE) sGender = "Female";
124 string sRace = "Creature";
125 int nRace = GetRacialType(oTarget);
126 if (nRace == RACIAL_TYPE_HUMAN) sRace = "Human";
127 else if (nRace == RACIAL_TYPE_ELF) sRace = "Elf";
128 else if (nRace == RACIAL_TYPE_DWARF) sRace = "Dwarf";
129 else if (nRace == RACIAL_TYPE_HALFLING) sRace = "Halfling";
130 else if (nRace == RACIAL_TYPE_GNOME) sRace = "Gnome";
131 else if (nRace == RACIAL_TYPE_HALFELF) sRace = "Half-Elf";
132 else if (nRace == RACIAL_TYPE_HALFORC) sRace = "Half-Orc";
134 int nMyMaxHP = GetMaxHitPoints(oTarget);
135 int nMyCurHP = GetCurrentHitPoints(oTarget);
136 string sMyHealth = "Healthy and uninjured.";
137 if (nMyCurHP < nMyMaxHP) {
138 if (nMyCurHP <= (nMyMaxHP / 4)) sMyHealth = "Critically wounded, bleeding heavily, and near death! I need to REST immediately.";
139 else if (nMyCurHP <= (nMyMaxHP / 2)) sMyHealth = "Injured, bruised, and exhausted.";
140 else sMyHealth = "Slightly hurt, with a few cuts and scratches.";
143 string sRelationship = "Neutral or Friendly.";
144 if (GetIsEnemy(oSpeaker, oTarget)) {
145 sRelationship = "CRITICAL: THIS PLAYER IS YOUR ENEMY! They have attacked you. You are FURIOUS, AGGRESSIVE, and HOSTILE. Do NOT be polite or friendly!";
148 string sLocationLore = "";
149 object oArea = GetArea(oTarget);
150 string sAreaLore = GetLocalString(oArea, "LLM_LOCATION_CONTEXT");
151 if (sAreaLore != "") sLocationLore = sAreaLore;
153 object oWaypoint = GetNearestObjectByTag("WP_LLM_LORE", oTarget);
154 if (GetIsObjectValid(oWaypoint) && GetDistanceBetween(oTarget, oWaypoint) <= 15.0f) {
155 string sWPLore = GetLocalString(oWaypoint, "LLM_LOCATION_CONTEXT");
156 if (sWPLore != "") sLocationLore += " SPECIFIC SURROUNDINGS: " + sWPLore;
159 string sNearbyNPCs = "";
161 object oRadarNPC = GetNearestCreature(CREATURE_TYPE_PLAYER_CHAR, PLAYER_CHAR_NOT_PC, oTarget, nNPCScan);
162 while (GetIsObjectValid(oRadarNPC) && GetDistanceBetween(oTarget, oRadarNPC) <= 15.0f) {
163 if (oRadarNPC != oTarget) sNearbyNPCs += GetName(oRadarNPC) + ", ";
165 oRadarNPC = GetNearestCreature(CREATURE_TYPE_PLAYER_CHAR, PLAYER_CHAR_NOT_PC, oTarget, nNPCScan);
168 json jData = JsonObject();
169 jData = JsonObjectSet(jData, "npc_tag", JsonString(GetTag(oTarget)));
170 jData = JsonObjectSet(jData, "target_player", JsonString(GetName(oSpeaker)));
171 jData = JsonObjectSet(jData, "message", JsonString(sMessage));
172 jData = JsonObjectSet(jData, "persona", JsonString(sPersona));
173 jData = JsonObjectSet(jData, "profession", JsonString(GetLocalString(oTarget, "LLM_PROFESSION")));
174 jData = JsonObjectSet(jData, "mood", JsonString(GetLocalString(oTarget, "LLM_MOOD")));
175 jData = JsonObjectSet(jData, "secret", JsonString(GetLocalString(oTarget, "LLM_SECRET")));
176 jData = JsonObjectSet(jData, "npc_routine", JsonString(GetLocalString(oTarget, "LLM_ROUTINE")));
177 jData = JsonObjectSet(jData, "npc_alignment", JsonString(sAlignment));
178 jData = JsonObjectSet(jData, "npc_gender", JsonString(sGender));
179 jData = JsonObjectSet(jData, "npc_race", JsonString(sRace));
180 jData = JsonObjectSet(jData, "player_state", JsonString(sPlayerState));
181 jData = JsonObjectSet(jData, "npc_health", JsonString(sMyHealth));
182 jData = JsonObjectSet(jData, "relationship", JsonString(sRelationship));
183 jData = JsonObjectSet(jData, "llm_strategy", JsonInt(nStrategy));
185 string sQuests = GetLocalString(oTarget, "LLM_QUESTS");
186 if (sQuests != "") jData = JsonObjectSet(jData, "available_quests", JsonString(sQuests));
188 string sProps = GetLocalString(oTarget, "LLM_PROPS");
189 if (sProps != "") jData = JsonObjectSet(jData, "available_props", JsonString(sProps));
191 if (sLocationLore != "") jData = JsonObjectSet(jData, "location_context", JsonString(sLocationLore));
192 if (sWorldState != "") jData = JsonObjectSet(jData, "world_state", JsonString(sWorldState));
193 if (sNearbyNPCs != "") jData = JsonObjectSet(jData, "nearby_npcs", JsonString(sNearbyNPCs));
195 NWNX_Redis_RPush("nwn_to_llm", JsonDump(jData));
198 oTarget = GetNearestObject(OBJECT_TYPE_CREATURE | OBJECT_TYPE_PLACEABLE, oSpeaker, nNth);