+// ============================================================================
+// File Name: asl_npc_ai_receiver
+// Generative Agent "Act" Phase placed on Module OnHeartbeat event.
+// ============================================================================
+#include "nwnx_redis"
+#include "nwnx_redis_lib"
+#include "nw_i0_generic"
+
+void ExecuteFallbackJump(object oNPC, object oHome) {
+ if (GetArea(oNPC) != GetArea(oHome)) {
+ ApplyEffectToObject(DURATION_TYPE_INSTANT, EffectVisualEffect(VFX_IMP_UNSUMMON), oNPC);
+ AssignCommand(oNPC, ActionJumpToObject(oHome));
+ }
+}
+
+void main()
+{
+ int nResultId = NWNX_Redis_LPOP("llm_to_nwn");
+ string sReplyData = NWNX_Redis_GetResultAsString(nResultId);
+ int nSafety = 0;
+
+ while (sReplyData != "" && sReplyData != "null" && sReplyData != "(nil)" && nSafety < 10)
+ {
+ nSafety++;
+ if (GetStringLength(sReplyData) < 10) break;
+
+ json jResponse = JsonParse(sReplyData);
+ string sNpcTag = JsonGetString(JsonObjectGet(jResponse, "npc_tag"));
+ string sTargetPlayer = JsonGetString(JsonObjectGet(jResponse, "target_player"));
+ string sReplyText = JsonGetString(JsonObjectGet(jResponse, "reply"));
+
+ object oNPC = GetObjectByTag(sNpcTag);
+ object oDebugPC = GetFirstPC();
+
+ if (!GetIsObjectValid(oNPC)) {
+ SendMessageToPC(oDebugPC, "[DEBUG ERROR] Could not find NPC with Tag: " + sNpcTag);
+ nResultId = NWNX_Redis_LPOP("llm_to_nwn");
+ sReplyData = NWNX_Redis_GetResultAsString(nResultId);
+ continue;
+ }
+
+ json jAgentBrain = JsonParse(sReplyText);
+
+ string sSpeech = "";
+ if (JsonGetType(JsonObjectGet(jAgentBrain, "speech")) == JSON_TYPE_STRING) sSpeech = JsonGetString(JsonObjectGet(jAgentBrain, "speech"));
+
+ string sEmotion = "NEUTRAL";
+ if (JsonGetType(JsonObjectGet(jAgentBrain, "emotion")) == JSON_TYPE_STRING) sEmotion = GetStringUpperCase(JsonGetString(JsonObjectGet(jAgentBrain, "emotion")));
+
+ string sAction = "";
+ if (JsonGetType(JsonObjectGet(jAgentBrain, "action")) == JSON_TYPE_STRING) sAction = GetStringUpperCase(JsonGetString(JsonObjectGet(jAgentBrain, "action")));
+
+ string sTarget = "";
+ if (JsonGetType(JsonObjectGet(jAgentBrain, "action_target")) == JSON_TYPE_STRING) sTarget = JsonGetString(JsonObjectGet(jAgentBrain, "action_target"));
+
+ string sTargetSpeech = "";
+ if (JsonGetType(JsonObjectGet(jAgentBrain, "target_speech")) == JSON_TYPE_STRING) sTargetSpeech = JsonGetString(JsonObjectGet(jAgentBrain, "target_speech"));
+
+ if (GetObjectType(oNPC) == OBJECT_TYPE_CREATURE) {
+ if (GetIsInCombat(oNPC) || GetIsObjectValid(GetAttackTarget(oNPC))) {
+ if (sAction != "PEACE" && sAction != "ATTACK") {
+ if (sSpeech != "" && sSpeech != "*grunts quietly*") {
+ AssignCommand(oNPC, SpeakString(sSpeech));
+ }
+ nResultId = NWNX_Redis_LPOP("llm_to_nwn");
+ sReplyData = NWNX_Redis_GetResultAsString(nResultId);
+ continue;
+ }
+ }
+ }
+
+ if (GetObjectType(oNPC) == OBJECT_TYPE_PLACEABLE) {
+ if (sSpeech != "") {
+ if (sSpeech == "*grunts quietly*") sSpeech = "*The shrine glows faintly but remains silent.*";
+ AssignCommand(oNPC, SpeakString(sSpeech));
+ ApplyEffectToObject(DURATION_TYPE_INSTANT, EffectVisualEffect(VFX_IMP_SUPER_HEROISM), oNPC);
+ }
+ nResultId = NWNX_Redis_LPOP("llm_to_nwn");
+ sReplyData = NWNX_Redis_GetResultAsString(nResultId);
+ continue;
+ }
+
+ SendMessageToPC(oDebugPC, "[DEBUG] " + GetName(oNPC) + " executing: [" + sAction + "] target: [" + sTarget + "]");
+ AssignCommand(oNPC, ClearAllActions(TRUE));
+
+ if (sSpeech != "") {
+ object oTalkTarget = GetFirstPC();
+ while (GetIsObjectValid(oTalkTarget)) {
+ if (GetName(oTalkTarget) == sTargetPlayer) {
+ AssignCommand(oNPC, SetFacingPoint(GetPosition(oTalkTarget)));
+ break;
+ }
+ oTalkTarget = GetNextPC();
+ }
+
+ DelayCommand(0.1, AssignCommand(oNPC, SpeakString(sSpeech)));
+
+ if (sEmotion == "LAUGHING") DelayCommand(0.2, AssignCommand(oNPC, ActionPlayAnimation(ANIMATION_LOOPING_TALK_LAUGHING, 1.0, 4.0)));
+ else if (sEmotion == "ANGRY") DelayCommand(0.2, AssignCommand(oNPC, ActionPlayAnimation(ANIMATION_LOOPING_TALK_FORCEFUL, 1.0, 4.0)));
+ else if (sEmotion == "PLEADING") DelayCommand(0.2, AssignCommand(oNPC, ActionPlayAnimation(ANIMATION_LOOPING_TALK_PLEADING, 1.0, 4.0)));
+ else if (sEmotion == "BOW") DelayCommand(0.2, AssignCommand(oNPC, ActionPlayAnimation(ANIMATION_FIREFORGET_BOW)));
+ else if (sEmotion == "TAUNT") DelayCommand(0.2, AssignCommand(oNPC, ActionPlayAnimation(ANIMATION_FIREFORGET_TAUNT)));
+ else if (sEmotion == "CHEER") {
+ if (Random(2) == 0) DelayCommand(0.2, AssignCommand(oNPC, ActionPlayAnimation(ANIMATION_FIREFORGET_VICTORY1)));
+ else DelayCommand(0.2, AssignCommand(oNPC, ActionPlayAnimation(ANIMATION_FIREFORGET_GREETING)));
+ }
+ else DelayCommand(0.2, AssignCommand(oNPC, ActionPlayAnimation(ANIMATION_LOOPING_TALK_NORMAL, 1.0, 3.0)));
+ }
+
+ if (sAction == "ATTACK") {
+ object oEnemy = OBJECT_INVALID;
+ if (sTarget != "") {
+ int i = 1;
+ object oSearch = GetNearestCreature(CREATURE_TYPE_IS_ALIVE, TRUE, oNPC, i);
+ while (GetIsObjectValid(oSearch) && GetDistanceBetween(oNPC, oSearch) < 20.0f) {
+ if (FindSubString(GetStringLowerCase(GetName(oSearch)), GetStringLowerCase(sTarget)) >= 0) {
+ oEnemy = oSearch;
+ break;
+ }
+ i++;
+ oSearch = GetNearestCreature(CREATURE_TYPE_IS_ALIVE, TRUE, oNPC, i);
+ }
+ }
+ if (!GetIsObjectValid(oEnemy)) oEnemy = GetNearestCreature(CREATURE_TYPE_REPUTATION, REPUTATION_TYPE_ENEMY, oNPC, 1, CREATURE_TYPE_IS_ALIVE, TRUE);
+ if (GetIsObjectValid(oEnemy)) {
+ if (GetIsPC(oEnemy)) SetIsTemporaryEnemy(oEnemy, oNPC, TRUE);
+ DelayCommand(0.3, AssignCommand(oNPC, ActionAttack(oEnemy)));
+ DelayCommand(0.4, AssignCommand(oNPC, DetermineCombatRound(oEnemy)));
+ }
+ }
+
+ else if (sAction == "PEACE") {
+ object oMyArea = GetArea(oNPC);
+ object oForgive = GetFirstPC();
+ while (GetIsObjectValid(oForgive)) {
+ if (GetArea(oForgive) == oMyArea) ClearPersonalReputation(oForgive, oNPC);
+ oForgive = GetNextPC();
+ }
+ AssignCommand(oNPC, ClearAllActions(TRUE));
+ AssignCommand(oNPC, SurrenderToEnemies());
+ DelayCommand(0.2, AssignCommand(oNPC, ActionPlayAnimation(ANIMATION_FIREFORGET_PAUSE_SCRATCH_HEAD)));
+ }
+
+ else if (sAction == "COMMAND") {
+ DelayCommand(0.2, AssignCommand(oNPC, ActionPlayAnimation(ANIMATION_LOOPING_TALK_FORCEFUL, 1.0, 3.0)));
+ object oVictim = OBJECT_INVALID;
+ if (sTarget != "RETREAT" && sTarget != "DEFEND_ME") {
+ int nScan = 1;
+ object oSearch = GetNearestCreature(CREATURE_TYPE_PLAYER_CHAR, PLAYER_CHAR_IS_PC, oNPC, nScan);
+ while (GetIsObjectValid(oSearch) && GetDistanceBetween(oNPC, oSearch) < 30.0f) {
+ if (FindSubString(GetStringLowerCase(GetName(oSearch)), GetStringLowerCase(sTarget)) >= 0) {
+ oVictim = oSearch;
+ break;
+ }
+ nScan++;
+ oSearch = GetNearestCreature(CREATURE_TYPE_PLAYER_CHAR, PLAYER_CHAR_IS_PC, oNPC, nScan);
+ }
+ }
+ int i = 1;
+ object oMinion = GetNearestCreature(CREATURE_TYPE_REPUTATION, REPUTATION_TYPE_FRIEND, oNPC, i);
+ while (GetIsObjectValid(oMinion) && GetDistanceBetween(oNPC, oMinion) < 30.0f) {
+ if (!GetIsPC(oMinion) && GetFactionEqual(oMinion, oNPC)) {
+ AssignCommand(oMinion, ClearAllActions(TRUE));
+ if (GetIsObjectValid(oVictim)) AssignCommand(oMinion, ActionAttack(oVictim));
+ else if (sTarget == "RETREAT") {
+ object oNearestPC = GetNearestCreature(CREATURE_TYPE_PLAYER_CHAR, PLAYER_CHAR_IS_PC, oMinion);
+ AssignCommand(oMinion, ActionMoveAwayFromObject(oNearestPC, TRUE, 40.0f));
+ }
+ else if (sTarget == "DEFEND_ME") AssignCommand(oMinion, ActionForceFollowObject(oNPC, 1.5f));
+ }
+ i++;
+ oMinion = GetNearestCreature(CREATURE_TYPE_REPUTATION, REPUTATION_TYPE_FRIEND, oNPC, i);
+ }
+ }
+
+ else if (sAction == "WANDER") DelayCommand(0.2, AssignCommand(oNPC, ActionRandomWalk()));
+ else if (sAction == "PATROL") DelayCommand(0.2, AssignCommand(oNPC, WalkWayPoints()));
+ else if (sAction == "FOLLOW" || sAction == "FOLLOW_NEAREST") {
+ object oTargetPC = GetFirstPC();
+ int bFound = FALSE;
+ while (GetIsObjectValid(oTargetPC)) {
+ if (FindSubString(GetStringLowerCase(GetName(oTargetPC)), GetStringLowerCase(sTarget)) >= 0 || GetName(oTargetPC) == sTargetPlayer) {
+ DelayCommand(0.2, AssignCommand(oNPC, ActionForceFollowObject(oTargetPC, 2.0f)));
+ bFound = TRUE;
+ break;
+ }
+ oTargetPC = GetNextPC();
+ }
+ if (!bFound) {
+ object oNearest = GetNearestCreature(CREATURE_TYPE_PLAYER_CHAR, PLAYER_CHAR_IS_PC, oNPC);
+ if (GetIsObjectValid(oNearest)) DelayCommand(0.2, AssignCommand(oNPC, ActionForceFollowObject(oNearest, 2.0f)));
+ }
+ }
+
+ else if (sAction == "REST") {
+ DelayCommand(0.2, ApplyEffectToObject(DURATION_TYPE_INSTANT, EffectVisualEffect(VFX_IMP_HEALING_M), oNPC));
+ DelayCommand(0.3, AssignCommand(oNPC, ActionPlayAnimation(ANIMATION_LOOPING_SIT_CROSS, 1.0, 12.0)));
+ DelayCommand(0.5, ForceRest(oNPC));
+ }
+ else if (sAction == "STEALTH") {
+ DelayCommand(0.2, AssignCommand(oNPC, SetActionMode(oNPC, ACTION_MODE_STEALTH, TRUE)));
+ DelayCommand(0.5, AssignCommand(oNPC, ActionPlayAnimation(ANIMATION_FIREFORGET_SPASM)));
+ }
+ else if (sAction == "SEARCH") {
+ DelayCommand(0.2, AssignCommand(oNPC, SetActionMode(oNPC, ACTION_MODE_DETECT, TRUE)));
+ }
+ else if (sAction == "UNSTEALTH") {
+ DelayCommand(0.2, AssignCommand(oNPC, SetActionMode(oNPC, ACTION_MODE_STEALTH, FALSE)));
+ DelayCommand(0.2, AssignCommand(oNPC, SetActionMode(oNPC, ACTION_MODE_DETECT, FALSE)));
+ }
+ else if (sAction == "GUARD") {
+ DelayCommand(0.2, AssignCommand(oNPC, ActionPlayAnimation(ANIMATION_FIREFORGET_HEAD_TURN_LEFT)));
+ DelayCommand(3.2, AssignCommand(oNPC, ActionPlayAnimation(ANIMATION_FIREFORGET_HEAD_TURN_RIGHT)));
+ }
+ else if (sAction == "GO_TO" && sTarget != "") {
+ object oDest = GetObjectByTag(sTarget);
+ if (GetIsObjectValid(oDest)) DelayCommand(0.2, AssignCommand(oNPC, ActionForceMoveToObject(oDest, FALSE, 1.0f)));
+ else DelayCommand(0.2, AssignCommand(oNPC, ActionRandomWalk()));
+ }
+
+ else if (sAction == "INTERACT" || sAction == "USE_OBJECT") {
+ int bFoundTarget = FALSE;
+ string sTargetLower = GetStringLowerCase(sTarget);
+
+ if (sTargetLower == "chair" || sTargetLower == "seat") sTargetLower = "stool";
+ else if (sTargetLower == "drink" || sTargetLower == "mug" || sTargetLower == "ale") sTargetLower = "cup";
+ else if (sTargetLower == "food" || sTargetLower == "meal") sTargetLower = "plate";
+ else if (sTargetLower == "fire" || sTargetLower == "hearth") sTargetLower = "fireplace";
+
+ object oTargetObj = GetNearestObject(OBJECT_TYPE_PLACEABLE, oNPC, 1);
+ int nNth = 1;
+
+ while (GetIsObjectValid(oTargetObj) && GetDistanceBetween(oNPC, oTargetObj) < 20.0) {
+ string sObjName = GetStringLowerCase(GetName(oTargetObj));
+
+ if ((sTargetLower != "" && FindSubString(sObjName, sTargetLower) >= 0) ||
+ (sTargetLower == "" && GetLocalInt(oTargetObj, "NW_INTERACTIVE") == TRUE)) {
+
+ bFoundTarget = TRUE;
+ AssignCommand(oNPC, ActionMoveToObject(oTargetObj, FALSE, 0.5f));
+ AssignCommand(oNPC, ActionDoCommand(SetFacingPoint(GetPosition(oTargetObj))));
+
+ if (FindSubString(sObjName, "cup") >= 0 || FindSubString(sObjName, "wine") >= 0 || FindSubString(sObjName, "keg") >= 0) {
+ AssignCommand(oNPC, ActionDoCommand(ActionPlayAnimation(ANIMATION_FIREFORGET_DRINK)));
+ } else if (FindSubString(sObjName, "stool") >= 0 || FindSubString(sObjName, "bench") >= 0 || FindSubString(sObjName, "chair") >= 0) {
+ AssignCommand(oNPC, ActionDoCommand(ActionPlayAnimation(ANIMATION_LOOPING_SIT_CHAIR, 1.0, 9999.0)));
+ } else if (FindSubString(sObjName, "book") >= 0 || FindSubString(sObjName, "shelf") >= 0) {
+ AssignCommand(oNPC, ActionDoCommand(ActionPlayAnimation(ANIMATION_FIREFORGET_READ)));
+ } else if (FindSubString(sObjName, "cook") >= 0 || FindSubString(sObjName, "oven") >= 0 || FindSubString(sObjName, "pot") >= 0) {
+ AssignCommand(oNPC, ActionDoCommand(ActionPlayAnimation(ANIMATION_LOOPING_GET_MID, 1.0, 9999.0)));
+ } else {
+ AssignCommand(oNPC, ActionDoCommand(ActionPlayAnimation(ANIMATION_LOOPING_GET_MID, 1.0, 3.0)));
+ }
+ break;
+ }
+ nNth++;
+ oTargetObj = GetNearestObject(OBJECT_TYPE_PLACEABLE, oNPC, nNth);
+ }
+
+ if (!bFoundTarget && sAction == "INTERACT" && sTargetLower != "") {
+ int i = 1;
+ object oCreature = GetNearestCreature(CREATURE_TYPE_IS_ALIVE, TRUE, oNPC, i);
+ while (GetIsObjectValid(oCreature) && GetDistanceBetween(oNPC, oCreature) < 20.0) {
+ if (FindSubString(GetStringLowerCase(GetName(oCreature)), sTargetLower) >= 0) {
+ AssignCommand(oNPC, ActionMoveToObject(oCreature, FALSE, 1.5f));
+ AssignCommand(oNPC, ActionDoCommand(SetFacingPoint(GetPosition(oCreature))));
+ AssignCommand(oNPC, ActionDoCommand(ActionPlayAnimation(ANIMATION_FIREFORGET_GREETING)));
+ bFoundTarget = TRUE;
+ break;
+ }
+ i++;
+ oCreature = GetNearestCreature(CREATURE_TYPE_IS_ALIVE, TRUE, oNPC, i);
+ }
+ }
+ if (!bFoundTarget) AssignCommand(oNPC, ActionPlayAnimation(ANIMATION_FIREFORGET_PAUSE_SCRATCH_HEAD));
+ }
+
+ else if (sAction == "CONVERSE") {
+ int nScan = 1;
+ object oPuppet = GetNearestCreature(CREATURE_TYPE_IS_ALIVE, TRUE, oNPC, nScan);
+ object oFoundPuppet = OBJECT_INVALID;
+
+ while (GetIsObjectValid(oPuppet) && GetDistanceBetween(oNPC, oPuppet) < 20.0f) {
+ if (!GetIsPC(oPuppet)) {
+ if (FindSubString(GetStringLowerCase(GetName(oPuppet)), GetStringLowerCase(sTarget)) >= 0) {
+ oFoundPuppet = oPuppet;
+ break;
+ }
+ }
+ nScan++;
+ oPuppet = GetNearestCreature(CREATURE_TYPE_IS_ALIVE, TRUE, oNPC, nScan);
+ }
+
+ if (GetIsObjectValid(oFoundPuppet)) {
+ AssignCommand(oNPC, ActionDoCommand(SetFacingPoint(GetPosition(oFoundPuppet))));
+ AssignCommand(oFoundPuppet, ClearAllActions(TRUE));
+ AssignCommand(oFoundPuppet, SetFacingPoint(GetPosition(oNPC)));
+
+ if (sTargetSpeech != "") {
+ DelayCommand(3.5f, AssignCommand(oFoundPuppet, ActionPlayAnimation(ANIMATION_FIREFORGET_GREETING)));
+ DelayCommand(4.0f, AssignCommand(oFoundPuppet, SpeakString(sTargetSpeech)));
+ } else {
+ DelayCommand(3.5f, AssignCommand(oFoundPuppet, ActionPlayAnimation(ANIMATION_FIREFORGET_PAUSE_SCRATCH_HEAD)));
+ }
+ }
+ }
+
+ else if (sAction == "OPEN_STORE") {
+ string sShopTag = GetLocalString(oNPC, "LLM_SHOPTAG");
+ if (sShopTag == "") sShopTag = "STORE_" + GetTag(oNPC);
+
+ object oStore = GetNearestObjectByTag(sShopTag, oNPC);
+ object oBuyer = OBJECT_INVALID;
+ object oTestPC = GetFirstPC();
+ while(GetIsObjectValid(oTestPC)) {
+ if(GetName(oTestPC) == sTargetPlayer) {
+ oBuyer = oTestPC;
+ break;
+ }
+ oTestPC = GetNextPC();
+ }
+
+ if (!GetIsObjectValid(oBuyer)) oBuyer = GetNearestCreature(CREATURE_TYPE_PLAYER_CHAR, PLAYER_CHAR_IS_PC, oNPC);
+
+ if (GetIsObjectValid(oStore) && GetIsObjectValid(oBuyer)) {
+ DelayCommand(1.0f, AssignCommand(oNPC, ActionPlayAnimation(ANIMATION_FIREFORGET_GREETING)));
+ DelayCommand(1.5f, OpenStore(oStore, oBuyer));
+ } else {
+ AssignCommand(oNPC, ActionSpeakString("I seem to have lost my wares..."));
+ }
+ }
+
+ else if (sAction == "GIVE_QUEST") {
+ object oPlayer = OBJECT_INVALID;
+ object oTestPC = GetFirstPC();
+ while(GetIsObjectValid(oTestPC)) {
+ if(GetName(oTestPC) == sTargetPlayer) {
+ oPlayer = oTestPC;
+ break;
+ }
+ oTestPC = GetNextPC();
+ }
+
+ if (!GetIsObjectValid(oPlayer)) oPlayer = GetNearestCreature(CREATURE_TYPE_PLAYER_CHAR, PLAYER_CHAR_IS_PC, oNPC);
+
+ if (GetIsObjectValid(oPlayer)) {
+ DelayCommand(1.0f, ApplyEffectToObject(DURATION_TYPE_INSTANT, EffectVisualEffect(VFX_IMP_MAGIC_PROTECTION), oNPC));
+ SendMessageToPC(oPlayer, "[SERVER] " + GetName(oNPC) + " has granted you a new quest objective!");
+ }
+ }
+
+ else if (sAction == "RETURN_TO_POST") {
+ object oHome = GetWaypointByTag("WP_" + GetTag(oNPC) + "_HOME");
+ if (GetIsObjectValid(oHome)) {
+ DelayCommand(0.2, AssignCommand(oNPC, ActionForceMoveToObject(oHome, FALSE, 1.0f)));
+ object oBed = GetNearestObjectByTag("Bed", oNPC);
+ if (GetIsObjectValid(oBed) && GetDistanceBetween(oHome, oBed) < 5.0) {
+ AssignCommand(oNPC, ActionDoCommand(ActionPlayAnimation(ANIMATION_LOOPING_DEAD_BACK, 1.0, 9999.0)));
+ } else {
+ AssignCommand(oNPC, ActionDoCommand(SetFacing(GetFacing(oHome))));
+ }
+ DelayCommand(20.0, ExecuteFallbackJump(oNPC, oHome));
+ }
+ }
+
+ nResultId = NWNX_Redis_LPOP("llm_to_nwn");
+ sReplyData = NWNX_Redis_GetResultAsString(nResultId);
+ }
+}
\ No newline at end of file