Доступ к готовым решениям

Переход в группу "Пользователь"

300.00 ₽
Одноразовый платёж
Быстрый переход в группу "Пользователи", без надобности написания постов и ожидания.

Покупка дает возможность:
Быть полноправным участником форума
Нормальное копирование кода
Создавать темы
Скачивать файлы
Доступ к архиву Pawno-Info

TypeScript вместо Pawn: зачем и как писать мод

markelov666

Умник
TypeScript вместо Pawn: зачем и как писать мод

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 {
      // Слот пустой или игрок уже вышел
    }
  });
});
Что здесь приятно по сравнению с Pawn:
  • Нет ручного 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);
В Pawn часто CreatePickup + OnPlayerPickUpPickup. В TS можно и так, и через свой тик с радиусом - удобно, когда пикап только "метка", а логика сложнее (нужен нужный VW, interior, состояние пешком).


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);
Фишки TS тут:
  • Тип 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, "Вы купили сигареты.");
  }
});
Дальше логично вынести товары в массив объектов { name, price } и не плодить if. В Pawn так тоже можно, но в TS это выглядит естественнее.


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 поправит импорты
Когда Pawn все еще ок
  • Учебный сервер на выходные
  • Маленький DM/TDM без аккаунтов и БД
  • Нет желания ставить Node, собирать TS, деплоить dist
  • Команда знает только Pawn и не планирует расти
С чего начать, если вы с Pawn
  • Поставьте open.mp и связку с Node (omp-node или аналог)
  • Соберите минимальный index: коннект, спавн, одна команда /me
  • Перенесите один вход в интерьер - так проще понять VW и interior
  • Добавьте один диалог-магазин
  • Только потом лезьте в БД, организации и античит
Не нужно сразу переписывать весь gamemode. Возьмите один кусок геймплея и сделайте его на TS чище, чем был на Pawn. Ощущение разницы придет быстро.
Итог:

TypeScript не делает RP "автоматически лучше". Он убирает песок из шестеренок: типы, модули, нормальный язык, меньше скрытых багов. Pawn остается понятным и быстрым для старта. Если сервер живет годами и код разрастается - TS обычно окупается.

Примеры выше специально короткие. В реальном проекте вокруг них появятся сессия аккаунта, сохранение денег, кулдауны, стример объектов и проверки на больницу/тюрьму. Но база та же: команда, пикап, телепорт, диалог.
 
Последнее редактирование:
Сверху Снизу