Всем привет. Хочу поделится одной наработкой, а именно которая заставляет NPC общаться не по скрипту.
Ниже будет пример для локальной работы, но к сожалению, я не нашел как это все перенести, чтобы работало 24/7 думаю решается обычным VDS
Для реализации понадобится:
- Сам AI, и это будет LM Studio
- Yandex Cloud для реализации озвучки, если кому-то важно
- Любить уток
Клиентская часть
Установка LM Studio
Скачать можно по специальной ссылке:
https://lmstudio.ai/
После установки система предложит скачать одну из моделей, вам нужно ее пропустить через кнопку Skip
После установки вы будете в общем меню
Далее в самом верху нужно выбрать
Select a model to load и в поиске написать
gemma-2-9b-it и выбираем от bartowski
Фотография для наглядности [Показать]
После установки модели, нужно ее выбрать в панели управления и также запустить сервер. Стандартный порт 1234
Настройка сервера JS
Для начала нужно создать папку в любом месте (для удобства), а в ней создать файл
server.js
После в папке нажимает
Shift+ПКМ и открываем терминал, вписываем следующее
npm install express axios iconv-lite fluent-ffmpeg, после установки зависимостей пишем
node server.js
Настройка Yandex Cloud для озвучки (опционально)
Тут все просто, нам нужно создать API ключ и ID аккаунта, но для начала:
- Регистрируемся по специальной ссылке в сервисе:
https://yandex.cloud/
- После регистрации система вас направит в консоль:
https://console.yandex.cloud/
Общее меню консоли [Показать]
- Переходим на получение гранта в 4000 рублей, привязываем карту и активируем (расходы будут минимальными тк я потратил всего 10 рублей).
- В самом вверху (показал на фотографии) у вас будет профиль default (копируем id) и сохраняем;
Далее у каталога default нажимаем на 3 точки и выбираем "
Создать сервисный аккаунт"
При создании указываем любое имя, например admin и выбираем следующие роли
ai.speechkit-tts.user и
ai.speechkit-stt.user
Отлично. Половина настройки озвучки уже завершена. В разделе "Ресурсы" у вас отобразится новый блок "
Identity and Access Management", нажимаем и нас перенаправляет в список сервисных аккаунтов, выбираем наш созданный
Фотография раздела ресурсов [Показать]
Теперь мы в нашем сервисном аккаунте. В самом верху будет "Создать новый ключ", выбираем "Создать Api-ключ"
При создании выбираем следующее: yc.ai.languageModels.execute, yc.ai.speechkitTts.execute и yc.ai.speechkitStt.execute. Создаем и сохраняем наш API
Создание API-ключа [Показать]
Настройка server.js
После создания ключа возвращаемся в наш файл server.js и вписываем следующий код. Нам нужно 2 строки YANDEX_API_KEY и YANDEX_FOLDER_ID, в YANDEX_API_KEY вписываем ключ, который создавали в сервисном аккаунте, а YANDEX_FOLDER_ID (это ID нашего каталога). Найти можно на странице
https://console.yandex.cloud/. Ранее на фотографии был как default.
В примере есть реализация сразу 3 систем, а именно: Озвучка нпс, текстовое общение, репутация и система пьянки, если вы будете слишком пьяны, то текст в чате для всех будет переписан для вас, например заменится слово или добавится лишнее.
Код server.js [Показать]
const express = require('express');
const axios = require('axios');
const iconv = require('iconv-lite');
const fs = require('fs');
const path = require('path');
const qs = require('querystring');
/**
* =============================================================================
* БЛОК ГЛОБАЛЬНОЙ КОНФИГУРАЦИИ
* =============================================================================
*/
const YANDEX_API_KEY = 'KEY';
const YANDEX_FOLDER_ID = 'KEY';
const LM_STUDIO_URL = "http://127.0.0.1:1234/v1/chat/completions";
const app = express();
const audioFolder = path.join(__dirname, 'public/voice');
const memoryFile = path.join(__dirname, 'memory.json');
// Хранилище для Cooldown (КД) событий
const crashCooldowns = {};
/**
* =============================================================================
* СИСТЕМА ПАМЯТИ И ЛОГИРОВАНИЯ
* =============================================================================
*/
let playerMemory = {};
if (fs.existsSync(memoryFile)) {
try {
playerMemory = JSON.parse(fs.readFileSync(memoryFile, 'utf-8'));
console.log("[SYSTEM] База данных игроков успешно загружена.");
} catch (e) {
console.error("[ERROR] Ошибка чтения memory.json, создаю новую базу.");
playerMemory = {};
}
}
const saveMemory = () => {
try {
fs.writeFileSync(memoryFile, JSON.stringify(playerMemory, null, 4));
} catch (e) {
console.error("[ERROR] Не удалось сохранить память в файл:", e.message);
}
};
// Подготовка директорий для аудиофайлов
if (!fs.existsSync(audioFolder)) {
fs.mkdirSync(audioFolder, { recursive: true });
console.log("[SYSTEM] Папка для аудио создана.");
}
/**
* =============================================================================
* НАСТРОЙКИ MIDDLEWARE
* =============================================================================
*/
app.use(express.raw({ type: 'application/x-www-form-urlencoded', limit: '5mb' }));
app.use('/voice', express.static(audioFolder, {
setHeaders: (res) => {
res.set('Access-Control-Allow-Origin', '*');
res.set('Cache-Control', 'no-store, no-cache, must-revalidate');
}
}));
/**
* =============================================================================
* ФУНКЦИЯ ФИНАЛЬНОЙ ОЧИСТКИ ТЕКСТА (БОРЬБА С ЦИФРАМИ И ЛАТИНИЦЕЙ)
* =============================================================================
*/
const finalizeText = (text) => {
if (!text) return "";
let clean = text;
// 1. Полное удаление знаков вопроса (требование для TTS)
clean = clean.split('?').join('');
// 2. Удаление ЛЮБОЙ латиницы (чтобы ИИ не подмешивал английские буквы)
clean = clean.replace(/[A-Za-z]/g, "");
// 3. Удаление пояснений в скобках (типа *вздыхает* или (кричит))
clean = clean.replace(/\(.*?\)/g, "");
clean = clean.replace(/\*.*?\*/g, "");
// 4. УДАЛЕНИЕ ЦИФР (чтобы машина не читала HP и скорость вслух)
clean = clean.replace(/[0-9]/g, "");
// 5. ЖЕСТКИЙ БЕЛЫЙ СПИСОК: оставляем только кириллицу и базовую пунктуацию
clean = clean.replace(/[^а-яА-ЯёЁ\s\.,!-]/g, "");
// 6. Схлопывание лишних пробелов
clean = clean.replace(/\s+/g, ' ');
// 7. Очистка знаков препинания в самом конце для корректного стыка с !!!
clean = clean.replace(/[!.,\s]+$/, "");
return clean.trim();
};
/**
* =============================================================================
* МАРШРУТ 1: /GET-STATS (СТАТИСТИКА РЕПУТАЦИИ)
* =============================================================================
*/
app.post('/get-stats', (req, res) => {
try {
const rawBody = iconv.decode(req.body, 'win1251');
const params = new URLSearchParams(rawBody);
const playerId = params.get('playerid') || "0";
if (!playerMemory[playerId]) {
playerMemory[playerId] = { history: [], carHistory: [], reputation: 50 };
}
const currentRep = playerMemory[playerId].reputation || 50;
const statusMsg = `[AI Система] Уровень доверия Шерифа: ${currentRep}/100.`;
console.log(`[LOG] Статистика игрока ${playerId}: ${currentRep}`);
res.setHeader('Content-Type', 'text/plain; charset=windows-1251');
// Отправляем чистый текст БЕЗ разделителей и звука
res.send(iconv.encode(statusMsg, 'win1251'));
} catch (err) {
console.error("[ERROR STATS]", err.message);
res.send(iconv.encode("Ошибка модуля статистики", 'win1251'));
}
});
/**
* =============================================================================
* МАРШРУТ 2: /DRUNK-CHAT (ПЬЯНЫЙ ФИЛЬТР)
* =============================================================================
*/
app.post('/drunk-chat', async (req, res) => {
try {
const rawBody = iconv.decode(req.body, 'win1251');
const params = new URLSearchParams(rawBody);
const userText = params.get('text') || "";
console.log(`[DRUNK IN] ${userText}`);
const aiResponse = await axios.post(LM_STUDIO_URL, {
messages: [
{
role: "system",
content: "Ты пьяный и общаешься на только русском. Коверкай слова, путай буквы, икай. ЗАПРЕЩЕНО использовать латиницу, цифры и знаки вопроса. Только кириллица."
},
{ role: "user", content: `Искази фразу: ${userText}` }
],
temperature: 1.1
});
let drunkText = finalizeText(aiResponse.data.choices[0].message.content);
// Гарантируем "ик" в конце, если ИИ забыл
if (!drunkText.toLowerCase().includes("ик")) {
drunkText += "... *ик*";
}
console.log(`[DRUNK OUT] ${drunkText}`);
res.setHeader('Content-Type', 'text/plain; charset=windows-1251');
res.send(iconv.encode(drunkText, 'win1251'));
} catch (err) {
console.error("[ERROR DRUNK]", err.message);
res.send(iconv.encode(iconv.decode(req.body, 'win1251').split('=')[1] + "... *ик*", 'win1251'));
}
});
/**
* =============================================================================
* МАРШРУТ 3: /PAWN-CHAT (ШЕРИФ ДЖОНСОН)
* =============================================================================
*/
app.post('/pawn-chat', async (req, res) => {
try {
const rawBody = iconv.decode(req.body, 'win1251');
const params = new URLSearchParams(rawBody);
const userText = params.get('text') || "";
const playerId = params.get('playerid') || "0";
if (!playerMemory[playerId]) playerMemory[playerId] = { history: [], carHistory: [], reputation: 50 };
let pData = playerMemory[playerId];
// Анализ репутации (маты / вежливость)
const toxicWords = /(нахуй|хуй|сука|бля|уебок|гавно|мать|*****|гандон)/gi;
const respectWords = /(сэр|шериф|офицер|пожалуйста|здравия)/gi;
if (toxicWords.test(userText)) pData.reputation -= 15;
else if (respectWords.test(userText.toLowerCase())) pData.reputation += 5;
pData.reputation = Math.max(0, Math.min(100, pData.reputation));
let sysRole = `Ты Шериф Джонсон. Репутация игрока: ${pData.reputation}/100. `;
let aiTemp = 0.8;
if (pData.reputation < 35) {
sysRole += "ТЫ В БЕШЕНСТВЕ. Используй жесткие маты (сука, нахуй).";
aiTemp = 1.1;
} else {
sysRole += "Ты строгий законник. Отвечай официально.";
}
const aiRes = await axios.post(LM_STUDIO_URL, {
messages: [
{ role: "system", content: sysRole + " Отвечай на русском, кратко, без вопросов и без цифр." },
...pData.history,
{ role: "user", content: userText }
],
temperature: aiTemp
});
let reply = finalizeText(aiRes.data.choices[0].message.content) || "Свободен, парень.";
// Синтез речи (Филипп)
const voiceResponse = await axios({
method: 'post',
url: 'https://tts.api.cloud.yandex.net/speech/v1/tts:synthesize',
headers: { 'Authorization': `Api-Key ${YANDEX_API_KEY}`, 'Content-Type': 'application/x-www-form-urlencoded' },
data: qs.stringify({ text: reply, voice: 'filipp', folderId: YANDEX_FOLDER_ID, format: 'mp3' }),
responseType: 'arraybuffer'
});
const timestamp = Date.now();
const fileName = `sh_${timestamp}.mp3`;
fs.writeFileSync(path.join(audioFolder, fileName), Buffer.from(voiceResponse.data));
// Обновление памяти
pData.history.push({ role: "user", content: userText }, { role: "assistant", content: reply });
if (pData.history.length > 8) pData.history.shift();
saveMemory();
console.log(`[SHERIFF] Игроку ${playerId}: ${reply}`);
res.setHeader('Content-Type', 'text/plain; charset=windows-1251');
res.send(iconv.encode(`${reply}!!!http://127.0.0.1:3000/voice/${fileName}?v=${timestamp}`, 'win1251'));
} catch (err) {
console.error("[ERROR SHERIFF]", err.message);
res.send(iconv.encode("Рация барахлит...!!!none", 'win1251'));
}
});
/**
* =============================================================================
* МАРШРУТ 4: /CAR-AI (БОРТОВОЙ ИИ INFERNUS)
* =============================================================================
*/
app.post('/car-ai', async (req, res) => {
try {
const rawBody = iconv.decode(req.body, 'win1251');
const params = new URLSearchParams(rawBody);
const playerId = params.get('playerid') || "0";
const eventType = params.get('event') || "none";
const carHp = params.get('hp') || "1000";
const carSpeed = params.get('speed') || "0";
const userMessage = params.get('text') || "Отчет систем";
// Проверка Cooldown на аварию (10 секунд)
if (eventType === "crash") {
const now = Date.now();
if (crashCooldowns[playerId] && (now - crashCooldowns[playerId] < 10000)) {
// ПУСТОЙ ОТВЕТ: Если КД не прошло, сервер просто завершает запрос без текста и звука
return res.end();
}
crashCooldowns[playerId] = now;
}
if (!playerMemory[playerId]) playerMemory[playerId] = { history: [], carHistory: [], reputation: 50 };
let pData = playerMemory[playerId];
// Лор машины (Алёна)
let carContext = `Ты — бортовой ИИ спорткара Infernus. Саркастичная, дерзкая женщина. `;
carContext += `Техническое состояние: HP ${carHp}, Скорость ${carSpeed}. `;
carContext += `ЗАПРЕЩЕНО называть любые цифры, проценты или технические данные вслух. Используй только слова. `;
let promptInput = userMessage;
if (eventType === "enter") {
carContext += "Водитель только что сел в салон. Поприветствуй его язвительно.";
promptInput = "Я в машине.";
} else if (eventType === "crash") {
carContext += "ПРОИЗОШЛО СТОЛКНОВЕНИЕ! Ты возмущена повреждениями. Ругай водителя.";
promptInput = "Мы врезались!";
}
const aiRes = await axios.post(LM_STUDIO_URL, {
messages: [
{ role: "system", content: carContext + " Отвечай на русском, очень кратко, БЕЗ ЦИФР и БЕЗ ВОПРОСОВ." },
...pData.carHistory,
{ role: "user", content: promptInput }
],
temperature: 0.9
});
let carReply = finalizeText(aiRes.data.choices[0].message.content) || "Смотри на дорогу.";
// Синтез речи (Алёна)
const voiceRes = await axios({
method: 'post',
url: 'https://tts.api.cloud.yandex.net/speech/v1/tts:synthesize',
headers: { 'Authorization': `Api-Key ${YANDEX_API_KEY}`, 'Content-Type': 'application/x-www-form-urlencoded' },
data: qs.stringify({ text: carReply, voice: 'alena', folderId: YANDEX_FOLDER_ID, format: 'mp3' }),
responseType: 'arraybuffer'
});
const mark = Date.now();
const fName = `car_${mark}.mp3`;
fs.writeFileSync(path.join(audioFolder, fName), Buffer.from(voiceRes.data));
// Обновление памяти машины
pData.carHistory.push({ role: "user", content: promptInput }, { role: "assistant", content: carReply });
if (pData.carHistory.length > 6) pData.carHistory.shift();
saveMemory();
console.log(`[CAR] Ответ ИИ: ${carReply}`);
res.setHeader('Content-Type', 'text/plain; charset=windows-1251');
res.send(iconv.encode(`${carReply}!!!http://127.0.0.1:3000/voice/${fName}?v=${mark}`, 'win1251'));
} catch (err) {
console.error("[ERROR CAR]", err.message);
res.send(iconv.encode("Бортовой компьютер: Ошибка системы!!!none", 'win1251'));
}
});
/**
* =============================================================================
* ЗАПУСК СЕРВЕРА
* =============================================================================
*/
const PORT = 3000;
app.listen(PORT, '0.0.0.0', () => {
console.log(" ");
console.log(" ==================================================");
console.log(` AI SAMP SERVER СТАРТОВАЛ НА ПОРТУ ${PORT}`);
console.log(" ==================================================");
console.log(" ");
});
Настройка серверной части. Pawn
Хочу сразу предупредить, что писал все в базовом моде bare без подключения сторонних плагинов.
В начало мода пишем следующий код:
Код [Показать]
#include <a_http>
// --- Настройки ---
#define AI_URL "127.0.0.1:3000/pawn-chat"
#define NPC_NAME "{66CCFF}Шериф Джонсон"
#define NPC_SKIN 280
#define DIALOG_SETTINGS 9955
new npcActor;
new Text3D:npcLabel;
new Float:npcPos[3] = {2031.4424, 1344.2286, 10.8203};
new Float:npcFacing = 270.0;
// Переменные настроек (для каждого игрока)
new bool:AIVoiceEnabled[MAX_PLAYERS];
new bool:AIEnabled[MAX_PLAYERS];
new bool:IsWaitingResponse[MAX_PLAYERS];
// --- Форварды ---
forward OnSheriffResponse(playerid, response_code, data[]);
forward OnDrunkChatResponse(playerid, response_code, data[]);
forward ResetNpcText();
forward StopNpcAnimation();
В
OnGameModeInit() прописываем следующий код
Код [Показать]
// Создание NPC
npcActor = CreateActor(NPC_SKIN, npcPos[0], npcPos[1], npcPos[2], npcFacing);
npcLabel = Create3DTextLabel(NPC_NAME"\n{FFFFFF}(Напиши в чат рядом, чтобы поговорить)", 0x66CCFFFF, npcPos[0], npcPos[1], npcPos[2] + 1.1, 20.0, 0);
К командам
OnPlayerCommandText добавим следующий код:
Код [Показать]
// Команда настроек
if (strcmp("/aisettings", cmdtext, true) == 0)
{
new string[256];
format(string, sizeof(string), "Озвучка ИИ\t\t[%s]\nИскусственный интеллект\t[%s]",
(AIVoiceEnabled[playerid] ? ("{00FF00}ВКЛ") : ("{FF0000}ВЫКЛ")),
(AIEnabled[playerid] ? ("{00FF00}ВКЛ") : ("{FF0000}ВЫКЛ")));
ShowPlayerDialog(playerid, DIALOG_SETTINGS, DIALOG_STYLE_LIST, "Настройки Шерифа", string, "Переключить", "Закрыть");
return 1;
}
if (strcmp("/drunkme", cmdtext, true) == 0)
{
SetPlayerDrunkLevel(playerid, 5000);
SendClientMessage(playerid, -1, "Вы выпили водку. Чат будет искажаться!");
return 1;
}
if (strcmp("/clearai", cmdtext, true) == 0)
{
new query[64];
format(query, sizeof(query), "playerid=%d", playerid);
HTTP(playerid, HTTP_POST, "127.0.0.1:3000/clear-memory", query, "OnAiMemoryClear");
return 1;
}
if (strcmp("/airep", cmdtext, true) == 0)
{
new query[64];
format(query, sizeof(query), "playerid=%d", playerid);
HTTP(playerid, HTTP_POST, "127.0.0.1:3000/get-stats", query, "OnAiRepShow");
return 1;
}
- Команда /aisettings настраивает наш искусственный интеллект, то есть, позволяет включить озвучку или отключить AI полностью
- Команда /drunkme, та самая команда, которая поможет протестить пьянку в чате;
- Команда /clearai очищает память NPC к вам;
- Команда /airep помогает посмотреть репутацию.
Слова для репутации можно исправить в server.js
На данный момент слова, который понижают репутацию (нахуй|хуй|сука|бля|уебок|гавно|мать|завали)
Слова, которые повышают репутацию и делают нпс добрее к вам (сэр|шериф|офицер|пожалуйста)
Тему не сносите из-за текста выше, это лишь пример.
В OnDialogResponse добавим следующий код
Код [Показать]
if(dialogid == DIALOG_SETTINGS && response)
{
if(listitem == 0) AIVoiceEnabled[playerid] = !AIVoiceEnabled[playerid];
if(listitem == 1) AIEnabled[playerid] = !AIEnabled[playerid];
OnPlayerCommandText(playerid, "/aisettings"); // Обновляем меню
return 1;
}
В OnPlayerText добавим следующий код
Код [Показать]
[b]public OnPlayerText(playerid, text[])
{
if(!AIEnabled[playerid]) return 1;
new Float:px, Float:py, Float:pz;
GetPlayerPos(playerid, px, py, pz);
// Считаем реальную дистанцию до координат NPC
new Float:dist = GetPlayerDistanceFromPoint(playerid, npcPos[0], npcPos[1], npcPos[2]);
// --- ЛОГИКА ШЕРИФА ---
// Если игрок в радиусе 6 метров, вызываем ИИ Шерифа
if(dist < 6.0)
{
if(IsWaitingResponse[playerid]) return 0;
IsWaitingResponse[playerid] = true;
Update3DTextLabelText(npcLabel, 0xFFFF00FF, NPC_NAME"\n{FFFF00}Шериф обдумывает ответ...");
ApplyActorAnimation(npcActor, "GANGS", "prtial_gngtlkA", 4.1, 0, 1, 1, 1, 1);
new query[512];
// ВАЖНО: передаем dist, чтобы Node.js видел расстояние!
format(query, sizeof(query), "text=%s&playerid=%d&posx=%.2f&posy=%.2f&dist=%.2f", text, playerid, px, py, dist);
HTTP(playerid, HTTP_POST, "127.0.0.1:3000/pawn-chat", query, "OnSheriffResponse");
return 0;
}
// --- ЛОГИКА ПЬЯНИЦЫ ---
if(GetPlayerDrunkLevel(playerid) > 2000)
{
new query[256];
format(query, sizeof(query), "text=%s", text);
HTTP(playerid, HTTP_POST, "127.0.0.1:3000/drunk-chat", query, "OnDrunkChatResponse");
return 0;
}
return 1;
}
В конец мода вписываем следующий код и завершаем pawn подключение
Код [Показать]
public OnSheriffResponse(playerid, response_code, data[])
{
IsWaitingResponse[playerid] = false;
if(response_code == 200)
{
new pos = strfind(data, "!!!");
if(pos != -1)
{
new s_text[144], s_url[256], string[256];
strmid(s_text, data, 0, pos);
strmid(s_url, data, pos + 3, strlen(data));
format(string, sizeof(string), "%s: {FFFFFF}%s", NPC_NAME, s_text);
SendClientMessage(playerid, -1, string);
Update3DTextLabelText(npcLabel, 0x66CCFFFF, string);
if(AIVoiceEnabled[playerid] && strcmp(s_url, "none") != 0)
{
// ОСТАНОВКА И ОЧИСТКА
StopAudioStreamForPlayer(playerid);
// Сохраняем URL в память игрока
SetPVarString(playerid, "VoiceUrl", s_url);
// ЗАПУСК ЧЕРЕЗ ПАУЗУ (300мс)
// Это дает GTA время сбросить аудио-канал
SetTimerEx("PlaySheriffVoice", 300, false, "i", playerid);
}
SetTimer("ResetNpcText", 10000, false);
SetTimer("StopNpcAnimation", 4000, false);
}
}
return 1;
}
// Эту функцию добавь в конец скрипта
forward PlaySheriffVoice(playerid);
public PlaySheriffVoice(playerid)
{
new s_url[256];
GetPVarString(playerid, "VoiceUrl", s_url, sizeof(s_url));
if(strlen(s_url) > 5)
{
PlayAudioStreamForPlayer(playerid, s_url);
DeletePVar(playerid, "VoiceUrl"); // Очищаем после запуска
}
return 1;
}
public OnDrunkChatResponse(playerid, response_code, data[])
{
if(response_code == 200)
{
new string[256], pName[MAX_PLAYER_NAME];
GetPlayerName(playerid, pName, sizeof(pName));
format(string, sizeof(string), "%s[%d] говорит: %s", pName, playerid, data);
for(new i = 0; i < MAX_PLAYERS; i++) if(IsPlayerConnected(i)) SendClientMessage(i, -1, string);
}
return 1;
}
public ResetNpcText()
{
Update3DTextLabelText(npcLabel, 0x66CCFFFF, NPC_NAME"\n{FFFFFF}(Напиши в чат рядом, чтобы поговорить)");
}
public StopNpcAnimation()
{
ClearActorAnimations(npcActor);
}
forward OnAiMemoryClear(playerid, response_code, data[]);
public OnAiMemoryClear(playerid, response_code, data[])
{
if(response_code == 200) SendClientMessage(playerid, 0x00FF00FF, "Шериф забыл ваше прошлое.");
else SendClientMessage(playerid, 0xFF0000FF, "Ошибка связи с сервером ИИ.");
return 1;
}
forward OnAiRepShow(playerid, response_code, data[]);
public OnAiRepShow(playerid, response_code, data[])
{
if(response_code == 200) {
new pos = strfind(data, "!!!");
if(pos != -1) {
new clean_text[144];
strmid(clean_text, data, 0, pos); // Копируем только текст до !!!
SendClientMessage(playerid, 0xFFFF00FF, clean_text);
}
} else {
SendClientMessage(playerid, 0xFF0000FF, "Ошибка связи с Шерифом.");
}
return 1;
}
Завершение
Остается запустить наш сервер, далее перейти в папку с файлом server.js и через Shift+ПКМ повторно открыть терминал и прописать node server.js
Также убедитесь, что порт 1234 свободен и в панеле из начала решения сервер также запущен. Он будет все время открыт на фоне и вы его не заметите
Результат
На фотографии можно заметить настройку, переадресацию на озвучку и вывод текста в чате.
Я добавил небольшую задержку между сообщением и ответом, чтобы флуда не было. Теперь нейронка для каждого нпс работает индивидуально.
Вероятный минус хостинга будет в том, что все будут одновременно одному боту флудить и что-то будет ломаться. Не тестил
Сообщение отредактировал KrasavaGhost: 21 декабря 2025 - 06:47