markelov666
Умник
TypeScript вместо Pawn: зачем и как писать мод
open.mp + omp-node: простым языком для тех, кто привык к .pwn
open.mp + omp-node: простым языком для тех, кто привык к .pwn
Зачем вообще уходить с Pawn
Pawn много лет был стандартом SA-MP. Он простой на старте, куча гайдов, фильтры и инклуды на любой вкус. Но когда сервер растет, начинаются одни и те же боли:
- Нет нормальных типов: опечатка в имени переменной или перепутанный id игрока ловится уже в игре, а не при сборке
- Код быстро превращается в кашу из define, forward, public и гигантских switch
- Сложно делить проект на модули: все тянется через инклуды и общие массивы на MAX_PLAYERS
- Рефакторить страшно: переименовал функцию - ищи глазами по всему gamemode
- Современный JS/TS экосистема (npm, IDE, автодополнение, поиск по проекту) на Pawn почти не работает
TypeScript (поверх Node и биндингов вроде omp-node) дает другое ощущение разработки:
- Редактор подсказывает методы игрока, ошибки типов видны до запуска
- Модули: команда в одном файле, пикапы в другом, без #include-ада
- Привычный синтаксис: функции, объекты, async/await, Map/Set вместо костылей
- Легче писать читаемый RP-код: диалоги, входы, организации, склады
- Тот же open.mp на клиенте, просто серверная логика на другом языке
Pawn не "плохой". Он отличный для маленьких режимов и обучения. TypeScript начинает выигрывать, когда логики много и над проектом работает не один человек.
Pawn vs TypeScript коротко
- Типы: в Pawn почти все integer/float/string. В TS - Player, Account, координаты со структурой
- Структура: Pawn - один большой режим + инклуды. TS - папки и import/export
- Ошибки: Pawn часто падает в рантайме. TS много ловит компилятором
- Асинхронность: в Pawn таймеры и callback-ад. В TS - Promise, async, setInterval без боли
- Библиотеки: npm вместо ручного копирования .inc
Как это выглядит на практике
Ниже упрощенные примеры в духе open.mp + TypeScript. Это не копипаст боевого проекта, а учебный стиль: чтобы было понятно "как делают".
1. Простая команда /me
В Pawn обычно что-то вроде CMD:me или zcmd, плюс цикл по игрокам рядом.
В TypeScript идея та же: зарегистрировали команду, проверили текст, разослали сообщение рядом стоящим.
JavaScript:
import { omp } from "@omp-node/core";
// Условный хелпер регистрации (у вас может называться иначе)
function registerCommand(name: string, handler: (player: any, args: string) => void) {
// внутри: omp.on("playerCommandText", ...)
}
const CHAT_RADIUS = 20;
const COLOR_ACTION = 0xc2a2daff;
registerCommand("me", (player, args) => {
const text = args.trim();
if (!text) {
player.sendClientMessage(0xff6347ff, "Использование: /me [действие]");
return;
}
const name = player.getName();
const line = `* ${name} ${text}`;
const pos = player.getPos();
omp.players.forEach((other) => {
try {
const otherPos = other.getPos();
const dist = Math.hypot(
pos.x - otherPos.x,
pos.y - otherPos.y,
pos.z - otherPos.z
);
if (dist > CHAT_RADIUS) {
return;
}
other.sendClientMessage(COLOR_ACTION, line);
} catch {
// Слот пустой или игрок уже вышел
}
});
});
- Нет ручного for(new i = 0; i < MAX_PLAYERS; i++) и IsPlayerConnected
- try/catch вокруг сетевых вызовов - слот мог освободиться
- Текст и радиус читаются сразу, без кучи define сверху файла
2. Пикап на карте
Классика: создать пикап и раз в тик проверять, стоит ли игрок рядом (или через событие подбора, если биндинг это дает).
JavaScript:
import { omp, Pickup } from "@omp-node/core";
const PICKUP_MODEL = 19132; // стрелка/маркер
const PICKUP_TYPE = 1;
const WORLD = 0;
const RADIUS = 1.5;
const POINT = {
x: 1481.06,
y: -1771.96,
z: 18.8,
};
new Pickup(PICKUP_MODEL, PICKUP_TYPE, POINT.x, POINT.y, POINT.z, WORLD);
setInterval(() => {
omp.players.forEach((player) => {
try {
if (player.getState() !== 1) {
return; // не пешком
}
if (player.getVirtualWorld() !== WORLD) {
return;
}
const pos = player.getPos();
const dist = Math.hypot(pos.x - POINT.x, pos.y - POINT.y, pos.z - POINT.z);
if (dist > RADIUS) {
return;
}
player.sendClientMessage(0xffffffff, "Вы у входа. Нажмите ...");
// дальше: телепорт, диалог, выдача предмета
} catch {
// игрок вышел
}
});
}, 200);
3. Вход в интерьер (улица <-> здание)
Два пикапа: с улицы внутрь и обратно. Телепорт через setPos / setInterior / setVirtualWorld (в обвязке часто есть хелпер placeAt).
JavaScript:
import { omp, Pickup } from "@omp-node/core";
type Point = {
x: number;
y: number;
z: number;
angle: number;
interior: number;
world: number;
};
const STREET: Point = {
x: 1481.11,
y: -1769.03,
z: 18.8,
angle: 0,
interior: 0,
world: 0,
};
const INSIDE: Point = {
x: -788.37,
y: -665.26,
z: 4001.09,
angle: 89.56,
interior: 0,
world: 3, // отдельный виртуальный мир здания
};
const ENTER_PICKUP = { x: 1481.06, y: -1771.96, z: 18.8 };
const EXIT_PICKUP = { x: -786.3, y: -665.28, z: 4001.09 };
function teleport(player: any, point: Point) {
player.setPos(point.x, point.y, point.z);
player.setFacingAngle(point.angle);
player.setInterior(point.interior);
player.setVirtualWorld(point.world);
}
function near(
pos: { x: number; y: number; z: number },
p: { x: number; y: number; z: number },
radius = 1.5
) {
return Math.hypot(pos.x - p.x, pos.y - p.y, pos.z - p.z) <= radius;
}
new Pickup(19132, 1, ENTER_PICKUP.x, ENTER_PICKUP.y, ENTER_PICKUP.z, 0);
new Pickup(19132, 1, EXIT_PICKUP.x, EXIT_PICKUP.y, EXIT_PICKUP.z, INSIDE.world);
const lastTeleport = new Map<number, number>();
setInterval(() => {
omp.players.forEach((player) => {
try {
if (player.getState() !== 1) {
return;
}
const id = player.getID();
const now = Date.now();
if ((lastTeleport.get(id) ?? 0) + 1500 > now) {
return; // антиспам телепорта
}
const pos = player.getPos();
const world = player.getVirtualWorld();
const interior = player.getInterior();
if (world === 0 && interior === 0 && near(pos, ENTER_PICKUP)) {
lastTeleport.set(id, now);
teleport(player, INSIDE);
return;
}
if (world === INSIDE.world && near(pos, EXIT_PICKUP)) {
lastTeleport.set(id, now);
teleport(player, STREET);
}
} catch {
// слот пустой
}
});
}, 200);
- Тип Point - координаты не "просто 6 чисел", а понятная структура
- Map для кулдауна вместо массива на MAX_PLAYERS
- Легко добавить проверку "только сотрудники организации" перед teleport
4. Простой диалог
Список или сообщение. В Pawn: ShowPlayerDialog + OnDialogResponse с кучей id.
В TS удобно держать id диалога константой рядом с обработчиком.
JavaScript:
import { Dialog, omp } from "@omp-node/core";
const DIALOG_SHOP = 100;
const STYLE_LIST = 2;
function showShop(player: any) {
Dialog.show(
player,
DIALOG_SHOP,
STYLE_LIST,
"Магазин",
"Хлеб ($10)\nВода ($5)\nСигареты ($20)",
"Купить",
"Отмена"
);
}
omp.on("dialogResponse", (player, dialogId, response, listItem, inputText) => {
if (dialogId !== DIALOG_SHOP) {
return;
}
if (!response) {
return; // нажал Отмена
}
if (listItem === 0) {
player.sendClientMessage(0xffffffff, "Вы купили хлеб.");
} else if (listItem === 1) {
player.sendClientMessage(0xffffffff, "Вы купили воду.");
} else if (listItem === 2) {
player.sendClientMessage(0xffffffff, "Вы купили сигареты.");
}
});
5. Команда + диалог вместе
JavaScript:
registerCommand("shop", (player) => {
showShop(player);
});
6. Организация доступа (идея)
В Pawn часто: PlayerInfo[playerid][pMember] == ORG_ARMY.
В TS удобнее работать с объектом аккаунта и функцией членства:
JavaScript:
function canOpenStaffDoor(account: { orgId: number } | null): boolean {
if (!account) {
return false;
}
// 3 - мерия, 6 - FBI (пример)
return account.orgId === 3 || account.orgId === 6;
}
// перед телепортом:
if (!canOpenStaffDoor(getAccount(player))) {
player.sendClientMessage(0xff6347ff, "Проход только для сотрудников.");
return;
}
teleport(player, ROOF_POINT);
Чем жить проще на TypeScript в большом RP
- Модули по зонам: больница, мерия, банды, склады - отдельные файлы
- Общие хелперы: цвет чата, "рядом ли игрок", placeAt, сохранение в БД
- Меньше магии глобальных массивов: состояние игрока в Map или сессии после логина
- Проще онбординг: новый разработчик открывает commands/me.ts и сразу понимает /me
- Рефакторинг безопаснее: переименовал ApplyOrgVisuals - IDE поправит импорты
- Учебный сервер на выходные
- Маленький DM/TDM без аккаунтов и БД
- Нет желания ставить Node, собирать TS, деплоить dist
- Команда знает только Pawn и не планирует расти
- Поставьте open.mp и связку с Node (omp-node или аналог)
- Соберите минимальный index: коннект, спавн, одна команда /me
- Перенесите один вход в интерьер - так проще понять VW и interior
- Добавьте один диалог-магазин
- Только потом лезьте в БД, организации и античит
Итог:Не нужно сразу переписывать весь gamemode. Возьмите один кусок геймплея и сделайте его на TS чище, чем был на Pawn. Ощущение разницы придет быстро.
TypeScript не делает RP "автоматически лучше". Он убирает песок из шестеренок: типы, модули, нормальный язык, меньше скрытых багов. Pawn остается понятным и быстрым для старта. Если сервер живет годами и код разрастается - TS обычно окупается.
Примеры выше специально короткие. В реальном проекте вокруг них появятся сессия аккаунта, сохранение денег, кулдауны, стример объектов и проверки на больницу/тюрьму. Но база та же: команда, пикап, телепорт, диалог.
Последнее редактирование:
