设为首页收藏本站同能贴吧 切换语言 繁体中文
开启辅助访问 切换到窄版
扫描二维码关注官方公众号
返回列表
+ 发新帖
查看: 72|回复: 0

[转载发布] AI Dialogue

[复制链接]
累计送礼:
0 个
累计收礼:
1 个
  • TA的每日心情
    开心
    2026-7-12 04:10
  • 签到天数: 209 天

    连续签到: 2 天

    [LV.7]常住居民III

    5778

    主题

    864

    回帖

    3万

    积分

    管理员

    Rank: 9Rank: 9Rank: 9

    VIP
    7
    卡币
    25563
    OK点
    16
    推广点
    0
    同能卷
    50
    积分
    32235

    灌水之王

    发表于 昨天 07:58 | 显示全部楼层 |阅读模式
    I was programming in Python as usual and using GROQ's API, and I thought, why not create something for RPG Maker MZ too? Since I just finished developing my FOW Minimap, I sat down and wrote some quick code.

    I'm giving it away here for free, I'm not developing it further. Take it and do whatever you want – MIT License. I made it as a hobby project, it's not a plugin I'll maintain. Here's what this version offers:










    GroqNPC.js – AI-Powered Dynamic NPC Engine for RPG Maker MZ​

    Version: 2.2.0
    License: MIT – do whatever you want with it
    Requirements: RPG Maker MZ, Groq API key (free tier available)


    What's Inside​

    This plugin connects your RPG Maker MZ game to Groq's fast LLM API (Llama 3.1) to deliver four integrated systems:

    Layer - Feature - What It Does

    1 - AI Dialogue - Streaming conversations with memory, 10s timeout
    2 - Quest Machine - Define quests; all NPCs share state automatically
    3 - NPC Knowledge - NPCs know quest status, companions, world state
    4 - Enemy Behavior - AI decides tactics, sets MZ switches before battle



    Quick Start – Dragon Hunt Example​

    1. Define the quest (in an autorun event)​


                    Code:       
    1. Plugin Command: Define Quest
    2.   Quest ID: dragon_hunt
    3.   Title: The Dragon of Ashvale
    4.   Objectives: Talk to Mark|Find the lair|Defeat the dragon
    5.   NPC Members: Mark,Sera,Old Tom
    6.   Quest Switch: 10
    7.   Quest Variable: 11
    复制代码



    2. Train Mark – the quest giver (in his event)​


                    Code:       
    1. Plugin Command: Train NPC Persona
    2.   Name: Mark
    3.   Personality: You are Mark, a grizzled dragon hunter. You lost your left eye to the Ashvale Dragon 20 years ago. You speak in a gruff, tired voice and constantly warn about danger.
    4.   Role: questgiver
    5.   Quest ID: dragon_hunt
    6.   Guardrails: Never tell the player the dragon is easy to beat
    7.   Fallback Message: I ain't lyin' to you about that beast.
    8. Plugin Command: Start AI Dialogue.
    复制代码


    3. Train a companion (Sera's event)​

                    Code:       
    1. Plugin Command: Train NPC Persona
    2.   Name: Sera
    3.   Personality: You are Sera, a young eager mage who wants to prove herself. You admire Mark but think he's too cautious. You speak quickly and enthusiastically.
    4.   Role: companion
    5.   Quest ID: dragon_hunt
    6.   Max Tokens: 150
    7. Plugin Command: Start AI Dialogue
    复制代码

    4. Enemy behavior – before dragon battle​

                    Code:       
    1. Plugin Command: Evaluate Enemy Behavior
    2.   Enemy Name: Ashvale Dragon
    3.   Enemy Description: Ancient red dragon, wounded but enraged. Scales are cracked on the left side (Mark's old wound). Breathes fire every 3 turns.
    4.   Quest ID: dragon_hunt
    5.   Behavior Switch Base: 20
    6.   Player HP % Variable: 1
    复制代码


    Then check switches in your battle event:

    • Switch 20 ON → Aggressive (full attack)
    • Switch 21 ON → Defensive (guard/buff)
    • Switch 22 ON → Flee (escape attempt)
    • Switch 23 ON → Reinforce (call allies)
    • Switch 24 ON → Enraged (berserk mode)
    5. Advance the quest anywhere​

                    Code:       
    1. Plugin Command: Advance Quest Objective
    2.   Quest ID: dragon_hunt
    复制代码


    All NPCs linked to dragon_hunt will automatically know the new objective.


    Text Formatting (AI → MZ Color Codes)​

    The AI wraps key words with semantic tags. The plugin converts them to MZ colors:

    Tag - Purpose - MZ Color
                    Code:       
    1. [important]text[/important]
    复制代码

    - Dangers, warnings, critical info - Red (2)
                    Code:       
    1. [highlight]text[/highlight]
    复制代码

    - Names, places, items, proper nouns - Yellow (14)
                    Code:       
    1. [secret]text[/secret]
    复制代码

    - Secrets, rare knowledge, confidential  - Green (3)
                    Code:       
    1. [weak]text[/weak]
    复制代码

    - Rumors, uncertain info, vague hints - Gray (8)



    Example AI response:

                    Code:       
    1. DIALOGUE: The [highlight]Dragon of Ashvale[/highlight] sleeps near the
    2. [highlight]eastern caves[/highlight]. It is [important]extremely dangerous[/important]
    3. – its fire can melt steel in seconds.
    4. PROMPT: Ask about the weak spot?
    复制代码


    What the player sees:Colored text automatically (yellow for locations, red for warnings, etc.)



    Plugin Parameters (Configure in Plugin Manager)​


    Parameter - Default - Description
                    Code:       
    1. Groq API Key
    复制代码

    - (Empty) - Get from console.groq.com/keys
                    Code:       
    1. AI Model
    复制代码

    - llama-3.1-8b-instant - Visit HEREfor more
                    Code:       
    1. Stream Timeout
    复制代码

    - 10 seconds - Hard cutoff. Partial text shown if triggered
                    Code:       
    1. Chat Memory
    复制代码

    - 4 exchanges - How many back-and-forth messages per NPC to remember
                    Code:       
    1. Default Input Prompt
    复制代码

    - What do you say? - Fallback when AI doesn't generate a prompt
                    Code:       
    1. Max Player Input Length
    复制代码

    - 100 characters - Limits what player can type
                    Code:       
    1. Global System Prompt Suffix
    复制代码

    - (Empty) - Appended to ALL NPC prompts. Example:
    "Keep responses under 3 sentences. Never break character."
                    Code:       
    1. Timeout Fallback Message
    复制代码

    - Hmm... I need a moment to think - Shown when API times out
                    Code:       
    1. Debug Mode
    复制代码

    - false - Logs full API payloads to
    F8 console


    Complete Plugin Command Reference​

    Quest Management​

    Command - Arguments - Description
                    Code:       
    1. defineQuest
    复制代码

    - questId, title, description, objectives (pipe-separated), npcMembers (comma), questSwitch, questVariable - reate/update a quest
                    Code:       
    1. advanceObjective
    复制代码

    - questId - Mark current objective done, move to next
                    Code:       
    1. completeQuest
    复制代码

    - questId - Mark quest as fully completed
                    Code:       
    1. failQuest
    复制代码

    - questId - Mark quest as failed

    NPC Persona​

    Command - Arguments - Description
                    Code:       
    1. trainPersona
    复制代码

    - name, personality, role (none/questgiver/companion/merchant/informant), questId, guardrails, fallbackMessage, maxTokens - Define NPC personality. Call BEFORE startDialogue
                    Code:       
    1. startDialogue
    复制代码

    - (none) - Begin AI conversation with this NPC
                    Code:       
    1. clearMemory
    复制代码

    - (none) - Wipe this NPC's conversation history
                    Code:       
    1. clearAllMemory
    复制代码

    - (none) - Wipe ALL NPC conversation histories
    Enemy Behavior​

    Command - Arguments - Description
                    Code:       
    1. evaluateEnemyBehavior
    复制代码

    - enemyName, enemyDescription, questId, behaviorSwitchBase, playerHpPercent - AI decides behavior, sets MZ switches

    Utilities​

    Command - Arguments - Description
                    Code:       
    1. testApiKey
    复制代码

    - (none) - Verify API key works. Shows result in message window
                    Code:       
    1. setContextVar
    复制代码

    - label, variableId or switchId - Expose game state to ALL AI calls globally


    ️ Notetags (Alternative to Train Persona)​

    Place these in the event's Note field:

                    Code:       
    1. <AIPersona: You are a paranoid old hermit who hoards mushrooms and mistrusts strangers.>
    2. <AIRole: informant>
    3. <AIQuestID: dragon_hunt>
    4. <AIGuardrails: Never talk about the royal family. Never reveal the dragon's location directly.>
    5. <AIFallback: I... I cannot speak of such things... The walls have ears.>
    6. <AIMaxTokens: 150>
    复制代码

    Note: Train Persona plugin commands override notetags.


    NPC Memory & Knowledge System​

    What NPCs Know Automatically​


    • Quest status – all quests linked to them via questId
    • Current objective – which step the player is on
    • Quest members – names of all NPCs in the same quest
    • World state – any variables/switches exposed via setContextVar
    • Conversation history – last N exchanges (configurable)
    Example: Exposing World State​


                    Code:       
    1. Plugin Command: Set Context Variable
    2. Label: Player defeated the dragon
    3. Switch ID: 25
    复制代码

    Now EVERY NPC sees this in their system prompt:

                    Code:       
    1. [WORLD STATE]
    2. Player defeated the dragon: true
    复制代码

    They will react accordingly. A merchant might say: "I heard you slew the dragon! Take this discount."

    Memory Management​


    • Memory is saved in the save file
    • Each NPC has independent memory (by map ID + event ID)
    • Use Clear NPC Memory to reset an NPC
    • Use Clear All NPC Memory to reset everyone

    Enemy Behavior Deep Dive​

    Behavior Switch Mapping​

    When you call evaluateEnemyBehavior with Behavior Switch Base: 20:
    Switch - Behavior - AI Decides This When

    20 - Aggressive - Player HP is low, enemy has advantage, quest near start
    21 - Defensive - Enemy is wounded, protecting something, buying time
    22 - Flee - Enemy is outmatched, mission objective complete, low HP
    23 - Reinforce - Enemy is alone vs party, quest climax approaching
    24 - Enraged - Quest-specific trigger (e.g., dragon at 30% HP, companion died)

    Only ONE switch is ON at a time. Use conditional branches to check them.

    Enemy Evaluation Example​

    The AI receives this system prompt internally:

                    Code:       
    1. You are an AI Game Master deciding enemy combat behavior.
    2. Enemy: Ashvale Dragon
    3. Description: Ancient red dragon, wounded but enraged.
    4. [QUEST: The Dragon of Ashvale]
    5. Status: ACTIVE
    6. Objectives:
    7. DONE: Talk to Mark
    8. CURRENT: Find the lair
    9. PENDING: Defeat the dragon
    10. Player current HP: 45%
    11. [WORLD STATE]
    12. Player has the Dragon Slayer sword: true
    复制代码



    Choose ONE behavior: aggressive / defensive / flee / reinforce / enraged
    The AI responds with a single word, and the plugin sets the corresponding switch.


    Save File Compatibility​

    The plugin saves the following data automatically:

    • _npcMemory – conversation histories
    • _trainedPersonas – all trained NPC personalities
    • _quests – quest states (status, current objective)
    • _ctxVars – exposed context variables
    No special handling needed. Save and load normally.



    Limitations & Warnings​


    Issue - Detail
    API key visibility - Client-side calls mean the API key is visible in player's browser. Obfuscation possible but not implemented.
    Internet required - Game won't work offline for AI features. Regular MZ gameplay unaffected.
    API costs - You pay for usage. Groq is very cheap (~$0.0002 per dialogue)
    but not free.
    Steam policy - Must declare AI-generated content in your store listing.

    No ongoing development - I made this for fun. It works as-is. I won't fix bugs or add features.





    Troubleshooting​

    "API key is empty" error​


    • Check Plugin Manager → GroqNPC → Groq API Key
    • Key should start with gsk_
    • No spaces before or after
    Timeout errors​


    • Increase Stream Timeout parameter (10-15 seconds recommended)
    • Switch to llama-3.1-8b-instant model (faster)
    No response or weird formatting​


    • Enable Debug Mode and check F8 console
    • Verify your Groq account has credits
    • Test with testApiKey plugin command
    NPC doesn't remember quest​


    • Ensure Quest ID matches exactly (case-sensitive)
    • Call trainPersona BEFORE startDialogue
    • Check that the quest was defined (run defineQuest first)

    License & Credits​

    MIT License – do literally anything you want.


    • No credit required (but appreciated)
    • No warranty
    • No support
    Built with:


    Final Words​

    That's it. Take it, break it, fix it, improve it, sell games with it.

    If you make something cool, I'd love to see it. If you don't, that's fine too.

    Enjoy!
    ↓FULL CODE ↓

    Spoiler                JavaScript:       
    1. //=============================================================================
    2. //=============================================================================
    3. // GroqNPC.js - Groq-Powered Dynamic NPC Engine for RPG Maker MZ
    4. // Version: 2.2.0
    5. // Changelog:
    6. //   2.0.0 - Quest State Machine, NPC Knowledge Sharing, Enemy Behavior AI,
    7. //            dynamic input prompt, structured AI response (DIALOGUE/PROMPT)
    8. //   1.2.0 - Hybrid text formatting (semantic tags -> MZ color codes)
    9. //   1.1.0 - Streaming API, 10s timeout, Train NPC Persona command
    10. //   1.0.0 - Initial release
    11. //=============================================================================
    12. // LEGAL DISCLAIMER:
    13. // Client-side calls to Groq API. You accept Groq ToS (https://groq.com/terms).
    14. // Declare AI-generated content in your store listing (Steam policy).
    15. // Author not liable for API costs or AI-generated content.
    16. //=============================================================================
    17. /*:
    18. * @target MZ
    19. * @plugindesc v2.2 | Groq AI Game Master - Dynamic NPCs, Quest Machine, Enemy Behavior
    20. * @author Rpx
    21. * @url https://gdschool.education/
    22. *
    23. * @help
    24. * ============================================================================
    25. * GROQ NPC ENGINE v2.0 - OVERVIEW
    26. * ============================================================================
    27. *
    28. * Four systems work together:
    29. *   LAYER 1 - AI Dialogue       Streaming NPC conversation, max 10s response
    30. *   LAYER 2 - Quest Machine     Define quests; all tagged NPCs share state
    31. *   LAYER 3 - NPC Knowledge     NPCs know quest status, companions, world state
    32. *   LAYER 4 - Enemy Behavior    AI decides enemy behavior, sets MZ Switches
    33. *
    34. * ============================================================================
    35. * QUICK START - DRAGON HUNTER QUEST EXAMPLE
    36. * ============================================================================
    37. *
    38. * 1. In a map autorun or quest-giver event:
    39. *    [Plugin Command] Define Quest
    40. *      ID: dragon_hunt | Title: The Dragon of Ashvale
    41. *      Objectives: Talk to Mark|Find the lair|Defeat the dragon
    42. *      Members: Mark,Sera,Old Tom | Quest Switch: 10
    43. *
    44. * 2. Mark's event (quest giver):
    45. *    [Plugin Command] Train NPC Persona
    46. *      Name: Mark | Role: questgiver | Quest ID: dragon_hunt
    47. *      Personality: You are Mark, veteran dragon hunter...
    48. *    [Plugin Command] Start AI Dialogue
    49. *
    50. * 3. Companion events (Sera, Old Tom):
    51. *    [Plugin Command] Train NPC Persona
    52. *      Role: companion | Quest ID: dragon_hunt
    53. *    [Plugin Command] Start AI Dialogue
    54. *
    55. * 4. Dragon enemy event (before battle):
    56. *    [Plugin Command] Evaluate Enemy Behavior
    57. *      Enemy: Ashvale Dragon | Quest ID: dragon_hunt
    58. *      Behavior Switch Base: 20
    59. *    [Conditional Branch] Switch 20 ON -> aggressive pattern
    60. *    [Conditional Branch] Switch 21 ON -> defensive pattern
    61. *    [Conditional Branch] Switch 22 ON -> flee
    62. *    [Conditional Branch] Switch 23 ON -> call reinforcements
    63. *    [Conditional Branch] Switch 24 ON -> enraged
    64. *
    65. * 5. Advance quest anywhere:
    66. *    [Plugin Command] Advance Quest Objective | Quest ID: dragon_hunt
    67. *
    68. * ============================================================================
    69. * NOTETAGS (Event Note field - overridden by Train NPC Persona command)
    70. * ============================================================================
    71. *
    72. * <AIPersona: [description]>
    73. * <AIGuardrails: [rules]>
    74. * <AIFallback: [message]>
    75. * <AIMaxTokens: [number]>
    76. * <AIQuestID: [questId]>
    77. * <AIRole: questgiver|companion|merchant|informant>
    78. *
    79. * ============================================================================
    80. * TEXT FORMATTING TAGS
    81. * ============================================================================
    82. *
    83. * AI wraps key words - plugin converts to MZ color codes:
    84. *   [important]...[/important]  -> Red    (dangers, warnings)
    85. *   [highlight]...[/highlight]  -> Yellow (names, places, items)
    86. *   [secret]...[/secret]        -> Green  (secrets, rare info)
    87. *   [weak]...[/weak]            -> Gray   (rumors, uncertain)
    88. *
    89. * ============================================================================
    90. * ENEMY BEHAVIOR SWITCHES (Base ID set in Evaluate Enemy Behavior command)
    91. * ============================================================================
    92. *
    93. *   Base+0  -> Aggressive  (full attack)
    94. *   Base+1  -> Defensive   (guard/buff)
    95. *   Base+2  -> Flee        (escape attempt)
    96. *   Base+3  -> Reinforce   (call allies)
    97. *   Base+4  -> Enraged     (quest-triggered berserk)
    98. *
    99. * Only ONE switch is ON at a time. Use Conditional Branch on each.
    100. *
    101. * ============================================================================
    102. *
    103. * @param apiKey
    104. * @text Groq API Key
    105. * @type string
    106. * @desc Get one at https://console.groq.com/keys
    107. * @default
    108. *
    109. * @param model
    110. * @text AI Model
    111. * @type combo
    112. * @option llama-3.1-8b-instant
    113. * @option llama-3.3-70b-versatile
    114. * @desc llama-3.1-8b-instant = fastest + cheapest. Recommended.
    115. * @default llama-3.1-8b-instant
    116. *
    117. * @param streamTimeout
    118. * @text Stream Timeout (seconds)
    119. * @type number
    120. * @min 3
    121. * @max 30
    122. * @desc Hard cutoff. Partial text shown if triggered.
    123. * @default 10
    124. *
    125. * @param maxMemory
    126. * @text Chat Memory (exchanges per NPC)
    127. * @type number
    128. * @min 1
    129. * @max 20
    130. * @default 4
    131. *
    132. * @param defaultInputPrompt
    133. * @text Default Input Prompt
    134. * @type string
    135. * @desc Fallback when AI does not generate a contextual prompt.
    136. * @default What do you say?
    137. *
    138. * @param maxInputLength
    139. * @text Max Player Input Length
    140. * @type number
    141. * @min 10
    142. * @max 200
    143. * @default 100
    144. *
    145. * @param globalSystemSuffix
    146. * @text Global System Prompt Suffix
    147. * @type multiline_string
    148. * @desc Appended to ALL NPC system prompts.
    149. * @default Keep responses under 3 sentences. Stay in character. Never mention being an AI.
    150. *
    151. * @param timeoutMessage
    152. * @text Timeout Fallback Message
    153. * @type string
    154. * @default Hmm... I need a moment to think.
    155. *
    156. * @param enableFormatting
    157. * @text Enable AI Text Formatting
    158. * @type boolean
    159. * @default true
    160. *
    161. * @param colorImportant
    162. * @text Color: [important]
    163. * @type number
    164. * @min -1
    165. * @max 31
    166. * @desc MZ color index. 2=red. -1=disabled.
    167. * @default 2
    168. *
    169. * @param colorHighlight
    170. * @text Color: [highlight]
    171. * @type number
    172. * @min -1
    173. * @max 31
    174. * @desc MZ color index. 14=yellow. -1=disabled.
    175. * @default 14
    176. *
    177. * @param colorSecret
    178. * @text Color: [secret]
    179. * @type number
    180. * @min -1
    181. * @max 31
    182. * @desc MZ color index. 3=green. -1=disabled.
    183. * @default 3
    184. *
    185. * @param colorWeak
    186. * @text Color: [weak]
    187. * @type number
    188. * @min -1
    189. * @max 31
    190. * @desc MZ color index. 8=gray. -1=disabled.
    191. * @default 8
    192. *
    193. * @param debugMode
    194. * @text Debug Mode
    195. * @type boolean
    196. * @desc Log full payloads to F8 console.
    197. * @default false
    198. *
    199. * @command testApiKey
    200. * @text Test API Connection
    201. * @desc Run this in any event to verify your API key works. Shows result in a message window.
    202. *
    203. * @command defineQuest
    204. * @text Define Quest
    205. * @desc Create/update a quest. All linked NPCs share this quest state.
    206. *
    207. * @arg questId
    208. * @text Quest ID
    209. * @type string
    210. * @desc Unique ID, no spaces. e.g. "dragon_hunt"
    211. * @default my_quest
    212. *
    213. * @arg title
    214. * @text Quest Title
    215. * @type string
    216. * @default The Quest
    217. *
    218. * @arg description
    219. * @text Quest Description
    220. * @type multiline_string
    221. * @default A quest to be completed.
    222. *
    223. * @arg objectives
    224. * @text Objectives (pipe-separated)
    225. * @type string
    226. * @desc e.g. "Find Mark|Reach the cave|Defeat the dragon"
    227. * @default Objective 1|Objective 2|Objective 3
    228. *
    229. * @arg npcMembers
    230. * @text NPC Members (comma-separated)
    231. * @type string
    232. * @desc Names of all NPCs involved. They all share quest knowledge.
    233. * @default NPC1,NPC2
    234. *
    235. * @arg questSwitch
    236. * @text Quest Active Switch ID
    237. * @type switch
    238. * @desc Set ON when quest is active. 0 = none.
    239. * @default 0
    240. *
    241. * @arg questVariable
    242. * @text Quest Progress Variable ID
    243. * @type variable
    244. * @desc Stores current objective index (0-based). 0 = none.
    245. * @default 0
    246. *
    247. * @command advanceObjective
    248. * @text Advance Quest Objective
    249. * @desc Mark current objective done, move to next.
    250. *
    251. * @arg questId
    252. * @text Quest ID
    253. * @type string
    254. * @default my_quest
    255. *
    256. * @command completeQuest
    257. * @text Complete Quest
    258. * @desc Mark quest as fully completed.
    259. *
    260. * @arg questId
    261. * @text Quest ID
    262. * @type string
    263. * @default my_quest
    264. *
    265. * @command failQuest
    266. * @text Fail Quest
    267. * @desc Mark quest as failed.
    268. *
    269. * @arg questId
    270. * @text Quest ID
    271. * @type string
    272. * @default my_quest
    273. *
    274. * @command trainPersona
    275. * @text Train NPC Persona
    276. * @desc Define NPC personality and quest link. Call BEFORE Start AI Dialogue.
    277. *
    278. * @arg name
    279. * @text NPC Name
    280. * @type string
    281. * @default Villager
    282. *
    283. * @arg personality
    284. * @text Personality & Background
    285. * @type multiline_string
    286. * @desc Role, traits, knowledge, speech style.
    287. * @default You are a friendly villager in a medieval fantasy town.
    288. *
    289. * @arg role
    290. * @text Quest Role
    291. * @type select
    292. * @option none
    293. * @option questgiver
    294. * @option companion
    295. * @option merchant
    296. * @option informant
    297. * @default none
    298. *
    299. * @arg questId
    300. * @text Quest ID (optional)
    301. * @type string
    302. * @desc Link this NPC to a quest. Leave empty for standalone.
    303. * @default
    304. *
    305. * @arg guardrails
    306. * @text Forbidden Topics
    307. * @type multiline_string
    308. * @default
    309. *
    310. * @arg fallbackMessage
    311. * @text Guardrail Fallback
    312. * @type string
    313. * @default I cannot speak of such things.
    314. *
    315. * @arg maxTokens
    316. * @text Max Response Tokens
    317. * @type number
    318. * @min 30
    319. * @max 400
    320. * @default 120
    321. *
    322. * @command startDialogue
    323. * @text Start AI Dialogue
    324. * @desc Begin AI conversation with this NPC.
    325. *
    326. * @command evaluateEnemyBehavior
    327. * @text Evaluate Enemy Behavior
    328. * @desc AI decides enemy behavior and sets MZ Switches.
    329. *
    330. * @arg enemyName
    331. * @text Enemy Name
    332. * @type string
    333. * @default Enemy
    334. *
    335. * @arg enemyDescription
    336. * @text Enemy Description
    337. * @type multiline_string
    338. * @desc Personality, lore, combat context.
    339. * @default A fierce monster guarding its territory.
    340. *
    341. * @arg questId
    342. * @text Quest ID (optional)
    343. * @type string
    344. * @default
    345. *
    346. * @arg behaviorSwitchBase
    347. * @text Behavior Switch Base ID
    348. * @type switch
    349. * @desc Base+0=Aggressive, Base+1=Defensive, Base+2=Flee, Base+3=Reinforce, Base+4=Enraged
    350. * @default 1
    351. *
    352. * @arg playerHpPercent
    353. * @text Player HP % Variable
    354. * @type variable
    355. * @desc Variable with player HP 0-100. 0 = not used.
    356. * @default 0
    357. *
    358. * @command setContextVar
    359. * @text Set Context Variable
    360. * @desc Expose a game variable or switch to all AI calls globally.
    361. *
    362. * @arg label
    363. * @text Label
    364. * @type string
    365. * @desc Human-readable name. e.g. "Player defeated the dragon"
    366. *
    367. * @arg variableId
    368. * @text Variable ID
    369. * @type variable
    370. *
    371. * @arg switchId
    372. * @text Switch ID
    373. * @type switch
    374. *
    375. * @command clearMemory
    376. * @text Clear NPC Memory
    377. * @desc Wipe this NPC's conversation history.
    378. *
    379. * @command clearAllMemory
    380. * @text Clear All NPC Memory
    381. * @desc Wipe ALL NPC conversation histories.
    382. */
    383. (() => {
    384.     "use strict";
    385.     const PLUGIN_NAME = "GroqNPC";
    386.     const LOG_PREFIX  = "[GroqNPC]";
    387.     const API_URL     = "https://api.groq.com/openai/v1/chat/completions";
    388.     //=========================================================================
    389.     // Config
    390.     //=========================================================================
    391.     const params = PluginManager.parameters(PLUGIN_NAME);
    392.     // Parsing robusto: trim + gestione undefined/null
    393.     const _rawKey = params["apiKey"] !== undefined ? String(params["apiKey"]).trim() : "";
    394.     const Config = {
    395.         apiKey        : _rawKey,
    396.         model         : String(params.model              || "llama-3.1-8b-instant"),
    397.         streamTimeout : Number(params.streamTimeout      || 10) * 1000,
    398.         maxMemory     : Number(params.maxMemory          || 4),
    399.         defaultPrompt : String(params.defaultInputPrompt || "What do you say?"),
    400.         maxInputLen   : Number(params.maxInputLength     || 100),
    401.         globalSuffix  : String(params.globalSystemSuffix || ""),
    402.         timeoutMsg    : String(params.timeoutMessage     || "Hmm... I need a moment to think."),
    403.         enableFmt     : params.enableFormatting          !== "false",
    404.         colorImportant: Number(params.colorImportant     ?? 2),
    405.         colorHighlight: Number(params.colorHighlight     ?? 14),
    406.         colorSecret   : Number(params.colorSecret        ?? 3),
    407.         colorWeak     : Number(params.colorWeak          ?? 8),
    408.         debugMode     : params.debugMode                 === "true",
    409.     };
    410.     const log  = (...a) => { if (Config.debugMode) console.log(LOG_PREFIX, ...a); };
    411.     const warn = (...a) => console.warn(LOG_PREFIX,  ...a);
    412.     const lerr = (...a) => console.error(LOG_PREFIX, ...a);
    413.     //=========================================================================
    414.     // LAYER 2 - Quest State Machine
    415.     //=========================================================================
    416.     const _quests = {};
    417.     function getQuest(id) { return _quests[id] || null; }
    418.     function buildQuestContext(questId) {
    419.         const q = getQuest(questId);
    420.         if (!q) return "";
    421.         const objectives = q.objectives.map((obj, i) => {
    422.             const done = i < q.currentObjective;
    423.             const cur  = i === q.currentObjective && q.status === "active";
    424.             return `  ${done ? "DONE" : cur ? "CURRENT" : "PENDING"}: ${obj}`;
    425.         }).join("\n");
    426.         return `\n[QUEST: ${q.title}]
    427. Status: ${q.status.toUpperCase()}
    428. Description: ${q.description}
    429. Objectives:\n${objectives}
    430. Active objective: ${q.objectives[q.currentObjective] || "All complete"}
    431. Quest members: ${q.npcMembers.join(", ")}`;
    432.     }
    433.     function advanceQuestObjective(questId) {
    434.         const q = getQuest(questId);
    435.         if (!q || q.status !== "active") return;
    436.         q.currentObjective = Math.min(q.currentObjective + 1, q.objectives.length - 1);
    437.         if (q.questVariable > 0) $gameVariables.setValue(q.questVariable, q.currentObjective);
    438.         log("Quest advanced:", questId, "-> objective", q.currentObjective);
    439.     }
    440.     //=========================================================================
    441.     // LAYER 3 - NPC Memory & Knowledge
    442.     //=========================================================================
    443.     const _npcMemory       = {};
    444.     const _trainedPersonas = {};
    445.     const memKey   = (m, e) => `${m}_${e}`;
    446.     const getMem   = (m, e) => { const k = memKey(m,e); if (!_npcMemory[k]) _npcMemory[k]=[]; return _npcMemory[k]; };
    447.     const clearMem = (m, e) => { _npcMemory[memKey(m,e)] = []; };
    448.     function pushMem(mapId, evId, role, content) {
    449.         const mem = getMem(mapId, evId);
    450.         mem.push({ role, content: stripTags(content) });
    451.         const max = Config.maxMemory * 2;
    452.         while (mem.length > max) mem.shift();
    453.     }
    454.     //=========================================================================
    455.     // Context Variables
    456.     //=========================================================================
    457.     const _ctxVars = [];
    458.     function buildCtxString() {
    459.         const lines = _ctxVars.map(cv => {
    460.             if (cv.variableId > 0) return `${cv.label}: ${$gameVariables.value(cv.variableId)}`;
    461.             if (cv.switchId   > 0) return `${cv.label}: ${$gameSwitches.value(cv.switchId) ? "true" : "false"}`;
    462.             return null;
    463.         }).filter(Boolean);
    464.         return lines.length ? "\n[WORLD STATE]\n" + lines.join("\n") : "";
    465.     }
    466.     //=========================================================================
    467.     // Notetag Parser
    468.     //=========================================================================
    469.     function parseNotetags(event) {
    470.         const note = event.event().note || "";
    471.         const get  = (tag, def) => { const m = note.match(new RegExp(`<${tag}:\\s*([^>]+)>`, "i")); return m ? m[1].trim() : def; };
    472.         return {
    473.             name      : get("AIName",      "NPC"),
    474.             persona   : get("AIPersona",   "You are a villager in a fantasy RPG."),
    475.             guardrails: get("AIGuardrails",""),
    476.             fallback  : get("AIFallback",  "I cannot speak of such things."),
    477.             maxTokens : Number(get("AIMaxTokens", "120")),
    478.             questId   : get("AIQuestID",   ""),
    479.             role      : get("AIRole",      "none"),
    480.         };
    481.     }
    482.     //=========================================================================
    483.     // System Prompt Builder
    484.     //=========================================================================
    485.     const ROLE_DESC = {
    486.         questgiver: "You are the quest giver. Explain objectives, urgency, and reward clearly.",
    487.         companion : "You are a companion in this quest. Share tips, morale, and quest details.",
    488.         merchant  : "You are a merchant. Sell useful items and share quest-related rumors.",
    489.         informant : "You are an informant. Share world information carefully and selectively.",
    490.     };
    491.     const FORMAT_INSTRUCTIONS = `
    492. [TEXT FORMATTING - USE SPARINGLY, MAX 4 WORDS PER TAG]
    493. [important]...[/important] = dangers, warnings, critical info
    494. [highlight]...[/highlight] = item names, locations, proper nouns
    495. [secret]...[/secret]       = secrets, rare knowledge, confidential
    496. [weak]...[/weak]           = rumors, uncertain info, vague hints`;
    497.     const RESPONSE_FORMAT = `
    498. [MANDATORY RESPONSE FORMAT]
    499. Reply using EXACTLY this structure, no exceptions:
    500. DIALOGUE: <your in-character reply, 1-3 sentences>
    501. PROMPT: <short question hint for player input, max 6 words>
    502. Example:
    503. DIALOGUE: The [highlight]Dragon of Ashvale[/highlight] sleeps near the [highlight]eastern caves[/highlight]. It is [important]extremely dangerous[/important].
    504. PROMPT: Ask about the reward?`;
    505.     function buildSysPrompt(tags) {
    506.         let p = `You are ${tags.name}. ${tags.persona}`;
    507.         if (ROLE_DESC[tags.role]) p += `\nRole: ${ROLE_DESC[tags.role]}`;
    508.         if (tags.questId) {
    509.             const qCtx = buildQuestContext(tags.questId);
    510.             if (qCtx) p += qCtx;
    511.         }
    512.         if (tags.guardrails) {
    513.             p += `\n[STRICT RULES]\n${tags.guardrails}`;
    514.             p += `\nIf forbidden topic raised, say ONLY: "${tags.fallback}"`;
    515.         }
    516.         const ctx = buildCtxString();
    517.         if (ctx) p += ctx;
    518.         if (Config.enableFmt) p += FORMAT_INSTRUCTIONS;
    519.         p += RESPONSE_FORMAT;
    520.         if (Config.globalSuffix) p += "\n" + Config.globalSuffix;
    521.         log("System prompt built for:", tags.name, "| quest:", tags.questId);
    522.         log("Full prompt:", p);
    523.         return p;
    524.     }
    525.     //=========================================================================
    526.     // LAYER 4 - Enemy Behavior Evaluator
    527.     //=========================================================================
    528.     const ENEMY_BEHAVIORS = ["aggressive", "defensive", "flee", "reinforce", "enraged"];
    529.     async function evaluateEnemyBehavior(enemyName, enemyDesc, questId, playerHpPct) {
    530.         if (!Config.apiKey) {
    531.             lerr("API key empty in evaluateEnemyBehavior. Raw:", params["apiKey"]);
    532.             throw new Error("Groq API key not set. Check Plugin Manager.");
    533.         }
    534.         let sys = `You are an AI Game Master deciding enemy combat behavior.
    535. Enemy: ${enemyName}
    536. ${enemyDesc ? "Description: " + enemyDesc : ""}`;
    537.         if (questId) {
    538.             const qCtx = buildQuestContext(questId);
    539.             if (qCtx) sys += qCtx;
    540.         }
    541.         if (playerHpPct > 0) sys += `\nPlayer current HP: ${playerHpPct}%`;
    542.         sys += buildCtxString();
    543.         sys += `
    544. [YOUR TASK]
    545. Based on all context, choose ONE behavior for this enemy:
    546. - aggressive: full attack, no mercy, player is manageable
    547. - defensive:  protect self, use guards/buffs, buying time
    548. - flee:       escape, enemy is outmatched or mission complete
    549. - reinforce:  call for allies, enemy needs backup now
    550. - enraged:    quest-triggered power surge, goes berserk (use when quest reaches climax)
    551. Respond with ONLY one word. No punctuation, no explanation.`;
    552.         const payload = {
    553.             model     : Config.model,
    554.             max_tokens: 5,
    555.             stream    : false,
    556.             messages  : [
    557.                 { role: "system", content: sys },
    558.                 { role: "user",   content: "Choose behavior." }
    559.             ],
    560.         };
    561.         log("Enemy eval payload:", JSON.stringify(payload));
    562.         const res = await fetch(API_URL, {
    563.             method : "POST",
    564.             headers: { "Content-Type": "application/json", "Authorization": `Bearer ${Config.apiKey}` },
    565.             body   : JSON.stringify(payload),
    566.         });
    567.         if (!res.ok) throw new Error(`Groq API ${res.status}`);
    568.         const data     = await res.json();
    569.         const behavior = (data.choices[0].message.content || "").trim().toLowerCase().split(/[\s\n]/)[0];
    570.         const idx      = ENEMY_BEHAVIORS.indexOf(behavior);
    571.         log(`Enemy "${enemyName}" behavior: "${behavior}" -> index ${idx >= 0 ? idx : 0}`);
    572.         return idx >= 0 ? idx : 0;
    573.     }
    574.     //=========================================================================
    575.     // Text Formatting
    576.     //=========================================================================
    577.     function stripTags(text) {
    578.         return text
    579.             .replace(/\[(important|highlight|secret|weak)\]([\s\S]*?)\[\/\1\]/gi, "$2")
    580.             .replace(/\[(\/?)(?:important|highlight|secret|weak)\]/gi, "");
    581.     }
    582.     function convertTags(text) {
    583.         if (!Config.enableFmt) return stripTags(text);
    584.         const map = {
    585.             important: Config.colorImportant,
    586.             highlight: Config.colorHighlight,
    587.             secret   : Config.colorSecret,
    588.             weak     : Config.colorWeak,
    589.         };
    590.         let r = text;
    591.         for (const [tag, idx] of Object.entries(map)) {
    592.             if (idx < 0) {
    593.                 r = r.replace(new RegExp(`\\[${tag}\\]([\\s\\S]*?)\\[\\/${tag}\\]`, "gi"), "$1");
    594.             } else {
    595.                 r = r.replace(
    596.                     new RegExp(`\\[${tag}\\]([\\s\\S]*?)\\[\\/${tag}\\]`, "gi"),
    597.                     `\\C[${idx}]$1\\C[0]`
    598.                 );
    599.             }
    600.         }
    601.         return r.replace(/\[(\/?)(?:important|highlight|secret|weak)\]/gi, "");
    602.     }
    603.     //=========================================================================
    604.     // Structured Response Parser - DIALOGUE: / PROMPT:
    605.     //=========================================================================
    606.     function parseResponse(raw) {
    607.         const dm = raw.match(/DIALOGUE:\s*([\s\S]*?)(?=\nPROMPT:|$)/i);
    608.         const pm = raw.match(/PROMPT:\s*(.+)/i);
    609.         return {
    610.             dialogue: (dm ? dm[1] : raw).trim(),
    611.             prompt  : (pm ? pm[1] : Config.defaultPrompt).trim(),
    612.         };
    613.     }
    614.     //=========================================================================
    615.     // Groq Streaming
    616.     //=========================================================================
    617.     async function callGroqStream(sysPrompt, messages, maxTokens) {
    618.         if (!Config.apiKey) {
    619.             const msg = `Groq API key is empty. Check Plugin Manager > GroqNPC > Groq API Key. Raw value: "${params["apiKey"]}"`;
    620.             lerr(msg);
    621.             throw new Error(msg);
    622.         }
    623.         const controller = new AbortController();
    624.         const timeoutId  = setTimeout(() => controller.abort(), Config.streamTimeout);
    625.         const payload = {
    626.             model     : Config.model,
    627.             max_tokens: maxTokens,
    628.             stream    : true,
    629.             messages  : [{ role: "system", content: sysPrompt }, ...messages],
    630.         };
    631.         let accumulated = "";
    632.         try {
    633.             const res = await fetch(API_URL, {
    634.                 method : "POST",
    635.                 headers: { "Content-Type": "application/json", "Authorization": `Bearer ${Config.apiKey}` },
    636.                 body   : JSON.stringify(payload),
    637.                 signal : controller.signal,
    638.             });
    639.             if (!res.ok) {
    640.                 const ed = await res.json().catch(() => ({}));
    641.                 throw new Error(`Groq API ${res.status}: ${ed?.error?.message || res.statusText}`);
    642.             }
    643.             const reader  = res.body.getReader();
    644.             const decoder = new TextDecoder("utf-8");
    645.             outer: while (true) {
    646.                 let chunk;
    647.                 try   { chunk = await reader.read(); }
    648.                 catch (_) { log("Stream cut by timeout. Got:", accumulated); break outer; }
    649.                 if (chunk.done) break;
    650.                 for (const line of decoder.decode(chunk.value, { stream: true }).split("\n")) {
    651.                     if (!line.startsWith("data: ")) continue;
    652.                     const data = line.slice(6).trim();
    653.                     if (data === "[DONE]") break outer;
    654.                     try {
    655.                         const delta = JSON.parse(data)?.choices?.[0]?.delta?.content;
    656.                         if (delta) accumulated += delta;
    657.                     } catch (_) {}
    658.                 }
    659.             }
    660.         } catch (e) {
    661.             if (e.name !== "AbortError") { clearTimeout(timeoutId); throw e; }
    662.             log("Fetch aborted (timeout).");
    663.         }
    664.         clearTimeout(timeoutId);
    665.         return accumulated.trim() || Config.timeoutMsg;
    666.     }
    667.     //=========================================================================
    668.     // Window_NpcInput - dynamic prompt text
    669.     //=========================================================================
    670.     class Window_NpcInput extends Window_Base {
    671.         initialize(rect) {
    672.             super.initialize(rect);
    673.             this._text        = "";
    674.             this._active      = false;
    675.             this._prompt      = Config.defaultPrompt;
    676.             this._cursorTimer = 0;
    677.             this._cursorVis   = true;
    678.             this._onConfirm   = null;
    679.             this._onCancel    = null;
    680.             this._boundKD     = this._onKeyDown.bind(this);
    681.             this.openness     = 0;
    682.         }
    683.         activate(prompt, onConfirm, onCancel) {
    684.             this._text      = "";
    685.             this._prompt    = prompt || Config.defaultPrompt;
    686.             this._active    = true;
    687.             this._onConfirm = onConfirm;
    688.             this._onCancel  = onCancel;
    689.             this.open();
    690.             this.refresh();
    691.             document.addEventListener("keydown", this._boundKD, true);
    692.         }
    693.         deactivate() {
    694.             this._active = false;
    695.             document.removeEventListener("keydown", this._boundKD, true);
    696.             this.close();
    697.         }
    698.         _onKeyDown(e) {
    699.             if (!this._active) return;
    700.             if (e.key === "Enter") {
    701.                 e.preventDefault(); e.stopPropagation();
    702.                 const t = this._text.trim();
    703.                 if (t) { this.deactivate(); this._onConfirm(t); }
    704.                 return;
    705.             }
    706.             if (e.key === "Escape") {
    707.                 e.preventDefault(); e.stopPropagation();
    708.                 this.deactivate(); this._onCancel();
    709.                 return;
    710.             }
    711.             if (e.key === "Backspace") {
    712.                 e.preventDefault();
    713.                 this._text = this._text.slice(0, -1);
    714.                 this.refresh();
    715.                 return;
    716.             }
    717.             if (e.key.length === 1 && !e.ctrlKey && !e.metaKey) {
    718.                 if (this._text.length < Config.maxInputLen) { this._text += e.key; this.refresh(); }
    719.             }
    720.         }
    721.         update() {
    722.             super.update();
    723.             if (this._active && ++this._cursorTimer >= 30) {
    724.                 this._cursorTimer = 0;
    725.                 this._cursorVis   = !this._cursorVis;
    726.                 this.refresh();
    727.             }
    728.         }
    729.         refresh() {
    730.             this.contents.clear();
    731.             const lh = this.lineHeight();
    732.             this.changeTextColor(ColorManager.systemColor());
    733.             this.drawText(this._prompt, 0, 0, this.innerWidth, "left");
    734.             this.resetTextColor();
    735.             const cursor = this._cursorVis && this._active ? "|" : " ";
    736.             this.drawText(this._text + cursor, 0, lh, this.innerWidth - 90, "left");
    737.             this.changeTextColor(ColorManager.textColor(8));
    738.             this.drawText(`${this._text.length}/${Config.maxInputLen}`, 0, lh, this.innerWidth, "right");
    739.             this.resetTextColor();
    740.             this.changeTextColor(ColorManager.textColor(7));
    741.             this.drawText("[Enter] Send   [Esc] Leave", 0, lh * 2, this.innerWidth, "right");
    742.             this.resetTextColor();
    743.         }
    744.     }
    745.     //=========================================================================
    746.     // Scene_Map - inject input window
    747.     //=========================================================================
    748.     const _origCreate = Scene_Map.prototype.createAllWindows;
    749.     Scene_Map.prototype.createAllWindows = function () {
    750.         _origCreate.call(this);
    751.         const ww   = Graphics.boxWidth * 0.82;
    752.         const wh   = this.calcWindowHeight(3, false);
    753.         const rect = new Rectangle((Graphics.boxWidth - ww) / 2, Graphics.boxHeight - wh - 8, ww, wh);
    754.         this._npcInputWindow = new Window_NpcInput(rect);
    755.         this.addWindow(this._npcInputWindow);
    756.     };
    757.     const getInputWin = () => SceneManager._scene?._npcInputWindow || null;
    758.     //=========================================================================
    759.     // Plugin Commands
    760.     //=========================================================================
    761.     // --- Test API Connection (debug) ---
    762.     PluginManager.registerCommand(PLUGIN_NAME, "testApiKey", function () {
    763.         const interp = this;
    764.         interp._waitCount = 9999;
    765.         (async () => {
    766.             const key = Config.apiKey;
    767.             if (!key) {
    768.                 $gameMessage.clear();
    769.                 $gameMessage.add("[GroqNPC] API Key is EMPTY.");
    770.                 $gameMessage.add(`Raw param value: "${params["apiKey"]}"`);
    771.                 interp._waitCount = 0;
    772.                 return;
    773.             }
    774.             try {
    775.                 const res = await fetch(API_URL, {
    776.                     method : "POST",
    777.                     headers: { "Content-Type": "application/json", "Authorization": `Bearer ${key}` },
    778.                     body   : JSON.stringify({
    779.                         model: Config.model, max_tokens: 5, stream: false,
    780.                         messages: [{ role: "user", content: "Say OK" }]
    781.                     }),
    782.                 });
    783.                 const data = await res.json();
    784.                 $gameMessage.clear();
    785.                 if (res.ok) {
    786.                     $gameMessage.add("[GroqNPC] API OK! Key works.");
    787.                     $gameMessage.add(`Model: ${Config.model}`);
    788.                 } else {
    789.                     $gameMessage.add(`[GroqNPC] API ERROR ${res.status}`);
    790.                     $gameMessage.add(data?.error?.message || "Unknown error");
    791.                 }
    792.             } catch (e) {
    793.                 $gameMessage.clear();
    794.                 $gameMessage.add("[GroqNPC] Network error:");
    795.                 $gameMessage.add(e.message);
    796.             }
    797.             interp._waitCount = 0;
    798.         })();
    799.     });
    800.     PluginManager.registerCommand(PLUGIN_NAME, "defineQuest", function (args) {
    801.         const id = String(args.questId || "").trim();
    802.         if (!id) { warn("defineQuest: questId is empty."); return; }
    803.         const switchId = Number(args.questSwitch   || 0);
    804.         const varId    = Number(args.questVariable || 0);
    805.         _quests[id] = {
    806.             id,
    807.             title           : String(args.title       || id),
    808.             description     : String(args.description || ""),
    809.             objectives      : String(args.objectives  || "").split("|").map(s => s.trim()).filter(Boolean),
    810.             currentObjective: 0,
    811.             status          : "active",
    812.             npcMembers      : String(args.npcMembers  || "").split(",").map(s => s.trim()).filter(Boolean),
    813.             questSwitch     : switchId,
    814.             questVariable   : varId,
    815.         };
    816.         if (switchId > 0) $gameSwitches.setValue(switchId, true);
    817.         if (varId    > 0) $gameVariables.setValue(varId, 0);
    818.         log("Quest defined:", id, _quests[id]);
    819.     });
    820.     PluginManager.registerCommand(PLUGIN_NAME, "advanceObjective", function (args) {
    821.         advanceQuestObjective(String(args.questId || "").trim());
    822.     });
    823.     PluginManager.registerCommand(PLUGIN_NAME, "completeQuest", function (args) {
    824.         const q = getQuest(String(args.questId || "").trim());
    825.         if (!q) { warn("completeQuest: quest not found"); return; }
    826.         q.status = "completed";
    827.         q.currentObjective = q.objectives.length - 1;
    828.         if (q.questSwitch > 0) $gameSwitches.setValue(q.questSwitch, false);
    829.         log("Quest completed:", q.id);
    830.     });
    831.     PluginManager.registerCommand(PLUGIN_NAME, "failQuest", function (args) {
    832.         const q = getQuest(String(args.questId || "").trim());
    833.         if (!q) { warn("failQuest: quest not found"); return; }
    834.         q.status = "failed";
    835.         if (q.questSwitch > 0) $gameSwitches.setValue(q.questSwitch, false);
    836.         log("Quest failed:", q.id);
    837.     });
    838.     PluginManager.registerCommand(PLUGIN_NAME, "trainPersona", function (args) {
    839.         const k = memKey($gameMap.mapId(), this._eventId);
    840.         _trainedPersonas[k] = {
    841.             name      : String(args.name           || "NPC"),
    842.             persona   : String(args.personality    || "You are a villager."),
    843.             role      : String(args.role           || "none"),
    844.             questId   : String(args.questId        || ""),
    845.             guardrails: String(args.guardrails      || ""),
    846.             fallback  : String(args.fallbackMessage || "I cannot speak of such things."),
    847.             maxTokens : Number(args.maxTokens       || 120),
    848.         };
    849.         log("Persona trained:", _trainedPersonas[k].name, "| role:", _trainedPersonas[k].role, "| quest:", _trainedPersonas[k].questId);
    850.     });
    851.     PluginManager.registerCommand(PLUGIN_NAME, "startDialogue", function () {
    852.         const interp  = this;
    853.         const event   = $gameMap.event(interp._eventId);
    854.         const mapId   = $gameMap.mapId();
    855.         const eventId = interp._eventId;
    856.         if (!event) { warn("startDialogue: event not found. ID:", eventId); return; }
    857.         const k    = memKey(mapId, eventId);
    858.         const tags = _trainedPersonas[k] || parseNotetags(event);
    859.         const sys  = buildSysPrompt(tags);
    860.         interp._waitCount = 999999;
    861.         (async () => {
    862.             let active        = true;
    863.             let currentPrompt = Config.defaultPrompt;
    864.             while (active) {
    865.                 const input = await getPlayerInput(currentPrompt);
    866.                 if (input === null) break;
    867.                 pushMem(mapId, eventId, "user", input);
    868.                 let raw = "";
    869.                 try {
    870.                     // +40 tokens headroom for DIALOGUE:/PROMPT: structure overhead
    871.                     raw = await callGroqStream(sys, getMem(mapId, eventId), tags.maxTokens + 40);
    872.                 } catch (e) {
    873.                     lerr("API error:", e.message);
    874.                     raw = `DIALOGUE: [Connection error. Try again.]\nPROMPT: ${Config.defaultPrompt}`;
    875.                 }
    876.                 const { dialogue, prompt } = parseResponse(raw);
    877.                 currentPrompt = prompt;
    878.                 pushMem(mapId, eventId, "assistant", dialogue);
    879.                 await showMsg(convertTags(dialogue));
    880.                 active = await promptContinue();
    881.             }
    882.             interp._waitCount = 0;
    883.         })();
    884.     });
    885.     PluginManager.registerCommand(PLUGIN_NAME, "evaluateEnemyBehavior", function (args) {
    886.         const interp     = this;
    887.         const enemyName  = String(args.enemyName           || "Enemy");
    888.         const enemyDesc  = String(args.enemyDescription    || "");
    889.         const questId    = String(args.questId             || "");
    890.         const baseSwitch = Number(args.behaviorSwitchBase  || 1);
    891.         const hpVarId    = Number(args.playerHpPercent     || 0);
    892.         const playerHp   = hpVarId > 0 ? $gameVariables.value(hpVarId) : 100;
    893.         interp._waitCount = 9999;
    894.         (async () => {
    895.             let idx = 0;
    896.             try   { idx = await evaluateEnemyBehavior(enemyName, enemyDesc, questId, playerHp); }
    897.             catch (e) { lerr("Enemy eval error:", e.message); idx = 0; }
    898.             for (let i = 0; i < ENEMY_BEHAVIORS.length; i++) {
    899.                 $gameSwitches.setValue(baseSwitch + i, i === idx);
    900.             }
    901.             log(`Enemy "${enemyName}" -> ${ENEMY_BEHAVIORS[idx]} (Switch ${baseSwitch + idx} = ON)`);
    902.             interp._waitCount = 0;
    903.         })();
    904.     });
    905.     PluginManager.registerCommand(PLUGIN_NAME, "setContextVar", function (args) {
    906.         _ctxVars.push({
    907.             label      : String(args.label      || ""),
    908.             variableId : Number(args.variableId || 0),
    909.             switchId   : Number(args.switchId   || 0),
    910.         });
    911.     });
    912.     PluginManager.registerCommand(PLUGIN_NAME, "clearMemory", function () {
    913.         clearMem($gameMap.mapId(), this._eventId);
    914.         log("Memory cleared for event:", this._eventId);
    915.     });
    916.     PluginManager.registerCommand(PLUGIN_NAME, "clearAllMemory", function () {
    917.         Object.keys(_npcMemory).forEach(k => { _npcMemory[k] = []; });
    918.         log("All NPC memory cleared.");
    919.     });
    920.     //=========================================================================
    921.     // Async helpers
    922.     //=========================================================================
    923.     function getPlayerInput(prompt) {
    924.         return new Promise(res => {
    925.             const w = getInputWin();
    926.             if (!w) { res(null); return; }
    927.             w.activate(prompt, text => res(text), () => res(null));
    928.         });
    929.     }
    930.     // Wraps text into lines of maxChars, then groups lines into pages of maxLines.
    931.     // MZ message window shows ~4 lines, ~50 chars per line with default font.
    932.     function wrapTextToPages(text, maxChars, maxLines) {
    933.         // Step 1: word-wrap into lines
    934.         const words = text.split(" ");
    935.         const lines = [];
    936.         let cur = "";
    937.         for (const word of words) {
    938.             const test = cur ? cur + " " + word : word;
    939.             if (test.length > maxChars) {
    940.                 if (cur) lines.push(cur);
    941.                 cur = word;
    942.             } else {
    943.                 cur = test;
    944.             }
    945.         }
    946.         if (cur) lines.push(cur);
    947.         // Step 2: group lines into pages
    948.         const pages = [];
    949.         for (let i = 0; i < lines.length; i += maxLines) {
    950.             pages.push(lines.slice(i, i + maxLines).join("\n"));
    951.         }
    952.         return pages.length ? pages : [text];
    953.     }
    954.     function showMsg(text) {
    955.         return new Promise(res => {
    956.             $gameMessage.clear();
    957.             // 50 chars per line, 4 lines per page - adjust if you use a larger font
    958.             const pages = wrapTextToPages(text, 50, 4);
    959.             pages.forEach(p => $gameMessage.add(p));
    960.             const t = setInterval(() => {
    961.                 if (!$gameMessage.isBusy()) { clearInterval(t); res(); }
    962.             }, 100);
    963.         });
    964.     }
    965.     function promptContinue() {
    966.         return new Promise(res => {
    967.             $gameMessage.clear();
    968.             $gameMessage.setChoices(["Continue", "Leave"], 0, 1);
    969.             $gameMessage.setChoiceCallback(i => res(i === 0));
    970.         });
    971.     }
    972.     // splitChunks removed in v2.1 - MZ handles word wrap natively
    973.     //=========================================================================
    974.     // Save / Load
    975.     //=========================================================================
    976.     const _origMake = DataManager.makeSaveContents;
    977.     DataManager.makeSaveContents = function () {
    978.         const c = _origMake.call(this);
    979.         c.groqNpcMemory  = _npcMemory;
    980.         c.groqPersonas   = _trainedPersonas;
    981.         c.groqQuests     = _quests;
    982.         c.groqCtxVars    = _ctxVars.slice();
    983.         return c;
    984.     };
    985.     const _origExtract = DataManager.extractSaveContents;
    986.     DataManager.extractSaveContents = function (c) {
    987.         _origExtract.call(this, c);
    988.         if (c.groqNpcMemory) Object.assign(_npcMemory,       c.groqNpcMemory);
    989.         if (c.groqPersonas)  Object.assign(_trainedPersonas, c.groqPersonas);
    990.         if (c.groqQuests)    Object.assign(_quests,          c.groqQuests);
    991.         if (c.groqCtxVars)   c.groqCtxVars.forEach(v => { if (!_ctxVars.find(x => x.label === v.label)) _ctxVars.push(v); });
    992.     };
    993.     log(`v2.2 loaded | model:${Config.model} | timeout:${Config.streamTimeout/1000}s | fmt:${Config.enableFmt} | debug:${Config.debugMode}`);
    994.     log("API key present:", Config.apiKey.length > 0, "| length:", Config.apiKey.length);
    995.     log("RAW params.apiKey:", JSON.stringify(params["apiKey"]));
    996. })();
    复制代码








    本贴来自国际rpgmaker官方论坛作者:Rpx处,因国际论坛即将永久关站,为了存档多年珍贵资料,署名转载到本论坛存档,由于官方帖子为英文原帖,需要中文翻译请点击论坛顶部切换语言为中文就可以将帖子翻译成中文浏览,方便大家随时查看,原文地址:https://forums.rpgmakerweb.com/threads/ai-dialogue.183612/

    本帖子中包含更多资源

    您需要 登录 才可以下载或查看,没有账号?立即注册

    x
    天天去同能,天天有童年!
    回复 送礼论坛版权

    使用道具 举报

    文明发言,和谐互动
    文明发言,和谐互动
    高级模式
    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    简体中文
    繁體中文
    English(英语)
    日本語(日语)
    Deutsch(德语)
    Русский язык(俄语)
    بالعربية(阿拉伯语)
    Türkçe(土耳其语)
    Português(葡萄牙语)
    ภาษาไทย(泰国语)
    한어(朝鲜语/韩语)
    Français(法语)
    关闭

    幸运抽奖

    社区每日抽奖来袭,快来试试你是欧皇还是非酋~

    立即查看

    聊天机器人
    Loading...

    QQ|Archiver|手机版|小黑屋|同能RPG制作大师 ( 沪ICP备12027754号-3 )

    GMT+8, 2026-7-26 04:52 , Processed in 0.127026 second(s), 53 queries .

    Powered by Discuz! X3.4

    Copyright © 2001-2020, Tencent Cloud.

    快速回复 返回顶部 返回列表