Игроки: команда, действия, списки
Player — это игрок на сервере. Его даёт событие или команда, к которой он
относится (event.player, player в обработчике команды), а
server.players перечисляет всех. Игровые данные игрока — gravity, origin,
hideHud и остальные — это свойства, они описаны на странице
Игроки и сущности. Эта страница — о том, что с игроком можно
делать: найти, перевести в команду, выдать оружие, показать текст и
поделиться его состоянием с другими плагинами.
server.addCommand("/rearm", rearm, { access: "slay" });
function rearm() {
for (const player of server.players.filter(other => other.isAlive && other.team === "CT")) {
player.removeAllItems();
player.give("weapon_knife");
player.give("weapon_m4a1");
player.setAmmo("weapon_m4a1", 90);
player.showHud("Перевооружены!", { color: [0, 200, 0], hold: 2 });
}
}
Список игроков с v0.2
for (const player of server.players) console.log(player.name); // все
const aliveCts = server.players.filter(player => player.isAlive && player.team === "CT");
const people = server.players.filter(player => !player.isBot); // без ботов
const bots = server.players.filter(player => player.isBot);
const dead = server.players.filter(player => !player.isAlive);
server.players — все подключённые, и читается заново при каждом обращении:
держите список в переменной, только пока игроки в нём не могут измениться.
Это массив, поэтому часть списка — filter, а один игрок — find, по
свойствам самого игрока: isAlive, isBot, team ("TERRORIST",
"CT", "SPECTATOR" или "UNASSIGNED"). HLTV-прокси в списке не бывает
никогда.
Команда
if (player.team == "CT") console.log("спецназовец");
player.team = "TERRORIST";
player.team — это "TERRORIST", "CT", "SPECTATOR" или "UNASSIGNED"
(тип Team), и сразу после смены команды он уже верный. Присваивание
переводит игрока: модель меняется на модель новой команды, таблица счёта
показывает новую команду. Условия победы в раунде при этом не проверяются —
если перевод должен закончить раунд, закончите его сами через
game.endRound (События игры).
server.addEventListener("putInServer", async (event) => {
await sleep(200, { signal: event.player.signal }); // игра ещё ставит его в мир
event.player.joinTeam("CT"); // false, если игра отказала
});
player.joinTeam(team) вводит игрока в сторону так, как игра вводит того, кто
выбрал её в меню команд, а внешность выбирается за него. У только что
пришедшего игрока стороны нет, и в игре его ещё нет; присваивание
player.team даёт ему сторону, а joinTeam ещё и вводит его в игру, так что
он может появиться. Живой игрок, отправленный в зрители, тихо умирает: без
смерти и без фрага.
mp_limitteamsБез ReAPI игра отказывает в стороне, которую
mp_limitteams перегрузил бы, как
и в меню команд, и мёртвому игроку, который уже сменил сторону в этом раунде;
joinTeam возвращает false. С ReAPI вызов эти проверки пропускает и ставит
игрока на ту сторону, которую ему дали.Голос
player.muted = true; // в голосовом чате его никто не слышит
if (player.muted) console.log(`${player.name} без голоса`);
player.muted = false;
player.heardByEveryone = true; // его слышат обе стороны
player.hearsEveryone = true; // он слышит обе стороны — например, зритель
Кто кого слышит, игра решает по сторонам и sv_alltalk; эти три свойства
меняют это для одного игрока, каждое само по себе. muted работает и с
включённым sv_alltalk, и с выключенным и сильнее heardByEveryone. Кто кого
слышит среди игроков на сторонах, можно решать и в каждом случае отдельно —
событием игры canPlayerHearPlayer (События игры).
Квары его игры
server.addCommand("/fps", async ({ player }) => {
const limit = await player.queryCvar("fps_max"); // "100" или null
print(player, `fps_max ${limit ?? "неизвестно"}`);
});
player.queryCvar(name) спрашивает у игры игрока один из её кваров и отдаёт
промис ответа: значение текстом или null, если такого квара в его игре нет
или она его не называет. Ответ — то, что говорит его игра, заявление, которое
чит может подменить. У бота игры нет, и он сразу отвечает null. Если игрок
уходит раньше ответа, промис отклоняется с "AbortError", а его асинхронная
команда тихо останавливается (асинхронность).
Как его игра подтвердила, кто он с v0.2
server.addCommand("/game", ({ player }) => {
if (player.authType === "steam") print(player, "Ваша игра из Steam");
else print(player, `Ваша игра: ${player.authType}, протокол ${player.protocol}`); // "revEmu2013, протокол 48"
});
Reunion — плагин ReHLDS, который пускает на сервер игроков без Steam. С ним
player.authType говорит, как игра игрока подтвердила, кто он (тип
AuthType): "steam" — игра из Steam; у игры без Steam — эмулятор, которым
она себя подтвердила: "steamEmu", "revEmu", "revEmu2013",
"oldRevEmu", "sc2009", "avsmp", "sxei", "sse3"; "dproto";
"hltv" — HLTV-прокси. player.protocol — сетевой протокол его игры: 48
у нынешней игры, 47 у старой, которую пускает Reunion. player.authKey —
ключ, которым его игра себя подтвердила, из него сделан его SteamID
(player.steamId): "STEAM_..." или "VALVE_...", как скажут настройки
Reunion на сервере.
Всё это знает только Reunion, а ReAPI читает это у него. На сервере без Reunion или без ReAPI
authType — "unknown", protocol — 0, а
authKey — "".Действия
player.give("weapon_flashbang"); // или "item_kevlar", "item_assaultsuit", "item_thighpack"
player.setAmmo("weapon_flashbang", 2); // патроны к нему с собой
player.getAmmo("weapon_flashbang"); // 2
player.switchWeapon("weapon_knife"); // false, если ножа нет
player.removeAllItems(); // всё оружие
player.resetMaxSpeed(); // скорость снова по оружию
player.respawn();
player.kill(); // kill({ keepFrags: true }) - без штрафа во фрагах
player.command("stop"); // в его собственной консоли, как будто он набрал сам
player.deaths = 0; // таблица счёта тоже обновится
| Метод | Что делает |
|---|---|
give(item) | выдаёт оружие или предмет; false, если игра не выдала |
setAmmo(weapon, amount) | задаёт патроны, которые игрок носит к своему оружию, помимо обоймы |
getAmmo(weapon) | патроны, которые игрок носит к своему оружию; для гранаты — сколько их; 0 для оружия, которого у него нет |
switchWeapon(weapon) | берёт в руки оружие, которое у него есть; false, если такого нет |
removeAllItems(removeSuit?) | забирает всё оружие; костюм остаётся, если removeSuit не true |
resetMaxSpeed() | возвращает скорость, которую позволяет оружие в руках, — например, после замедления |
respawn() | возрождает в текущем раунде, на точке, которую выберет игра |
kill(options?) | убивает, как команда kill; { keepFrags: true } не отнимает фраг |
command(text) | выполняет команду в консоли игрока: её выполняет его игра, а не сервер |
Имена оружия — тип WeaponName ("weapon_knife", "weapon_awp", …), а
предметы, которые принимает give, — ItemName; редактор их подсказывает и
не пропускает опечатку. У оружия, которое игрок несёт, имя — weapon.classname
(оружие): other.give(weapon.classname).
name, health, armor, frags и deaths — свойства: их можно читать, а
health, armor, frags и deaths — ещё и присваивать. health, равный
0 или меньше, убивает игрока.
Действия работают на любом сервере. С ReAPI они идут через него, без него —
через стандартные модули AMX Mod X fun, cstrike и hamsandwich. Плагин
пишется одинаково в обоих случаях.
Фейковые клиенты с v0.2
Фейковый клиент — бот — это игрок, которого ведёт сам сервер: он занимает
слот, есть в server.players и играет по правилам игры, но игры за ним нет.
server.addBot(name) добавляет такого:
const bot = server.addBot("Dummy"); // null, если свободного слота нет
if (bot) {
bot.joinTeam("CT");
bot.respawn();
}
Он приходит, как любой игрок, — для него срабатывают "connect" и
"putInServer", — bot.isBot равно true, а bot.kick() убирает его, с
"disconnected".
Своего разума у бота нет: он стоит, где появился, и ничего не делает, пока
плагин его не двинет. bot.move(options) — это один кадр его клавиш и мыши,
поэтому его вызывают каждый кадр, в событии "frame", пока бот должен
двигаться:
server.addEventListener("frame", () => {
bot?.move({ forward: 250, buttons: ["jump"], angles: [0, 90, 0] });
});
| Параметр | Что это |
|---|---|
forward | скорость вперёд, единиц в секунду; назад, если отрицательная (250 — бег с ножом) |
side | вправо; влево, если отрицательная |
up | вверх, в воде и на лестнице; вниз, если отрицательная |
buttons | кнопки, зажатые на время шага: "attack", "attack2", "jump", "duck", "use", "reload", … (тип Button) |
angles | куда он смотрит, [pitch, yaw, roll] или Vector; если не задано — куда смотрит сейчас |
msec | сколько длится шаг, от 1 до 255 миллисекунд; если не задано — время кадра |
move для игрока, который не бот, бросает Error: плагин двигает только
своих ботов.
addBot даёт тело, а не игрока: сам он не ходит, не целится, не стреляет и
не покупает. Всё, что он делает, — это вызовы move плагина, кадр за кадром.
Шаг, вызванный реже — из таймера раз в 0,1 секунды, — двигает его только на
свой msec, а не до следующего вызова.Есть ли модуль на сервере
if (!hasModule("reapi")) console.warn("myplugin: событиям раунда нужен ReAPI");
hasModule возвращает true, если на сервере работает этот модуль AMX Mod X:
"reapi", "cstrike", "fun", "hamsandwich", "engine" или
"fakemeta". Спрашивайте его перед нативом, который есть только в одном
модуле.
HUD
player.showHud("-35 HP");
player.showHud("Раунд 3", { color: [255, 40, 40], x: 0.02, y: 0.88, hold: 2 });
player.showHud("печатается по буквам", { effect: "typewriter", channel: 2 });
player.showHud("ТЕРРОРИСТЫ ПОБЕДИЛИ", { large: true, y: 0.3, hold: 5 }); // крупные буквы
server.showHud("Для всех", { y: 0.6 });
HUD-сообщение — это текст на экране вне чата. Любую настройку можно не писать:
| Настройка | По умолчанию | Значение |
|---|---|---|
color | [200, 100, 0] | красный, зелёный, синий, от 0 до 255 |
x, y | -1, 0.35 | позиция, от 0 до 1 от левого верхнего угла; -1 — по центру |
hold | 12 | сколько секунд держится на экране |
effect | "fade" | "fade", "flicker" или "typewriter" (по буквам) |
fadeIn, fadeOut | 0.1, 0.2 | секунды на появление и на исчезновение |
effectTime | 6 | сколько секунд идут эффекты "flicker" и "typewriter" |
channel | -1 | канал, от 1 до 4; -1 берёт свободный. Новое сообщение на канале заменяет старое |
large | false | крупные буквы — для итога или заголовка; каналов у них нет |
Строка, которая обновляется, — HudLine
const countdown = new HudLine();
function tick(player: Player, left: number) {
countdown.show(player, `Бомба: ${left}`, { color: [255, 50, 50], hold: 1.1 });
}
HudLine — одно место на экране: каждый show заменяет то, что оно
показывало, а обычный showHud занял бы другой канал, и прошлая цифра
угасала бы под новой. countdown.clear(player) убирает строку с экрана
одного игрока раньше времени, countdown.clearAll() — у всех.
Эффекты экрана — player.screen
player.screen.fade({ color: [200, 0, 0, 100], duration: 0.5 }); // красная вспышка, которая проходит
player.screen.fade({ color: [0, 0, 0, 255], duration: 0.1, hold: 1, stay: true }); // чёрный экран, и он остаётся
player.screen.fade({ color: [0, 0, 0, 0], duration: 0.2 }); // снова ясно
player.screen.shake({ amplitude: 8, duration: 1, frequency: 5 });
player.screen.statusIcon("dmg_cold", "show", [0, 200, 255]); // "hide", "show", "flash"
player.screen.roundTime(90); // часы раунда, в секундах
player.screen.hideHud(["money", "timer"]); // сразу
player.screen.crosshair(false);
player.screen.flashlight(false); // значок фонарика
player.screen.progressBar(5); // полоса заполнится за 5 секунд; 0 её убирает
player.screen.progressBar(4, { startPercent: 50 }); // наполовину полна, заполнится за 2 секунды
То, что один игрок видит поверх мира. Время — в секундах.
Обработчик сообщений слышит то, что шлёт player.screen,
как слышит сообщения игры.
Настройка fade | По умолчанию | Значение |
|---|---|---|
color | [0, 0, 0, 255] | красный, зелёный, синий и непрозрачность, от 0 до 255 |
duration | 1 | сколько секунд идёт переход |
hold | 0 | сколько секунд держится полный цвет |
direction | "in" | "in" — от цвета к ясной картинке, "out" — от ясной картинки к цвету |
stay | false | цвет остаётся до следующего fade |
modulate | false | подкрашивает картинку, а не закрашивает её |
Переход или удержание дольше 16 секунд обрезается до 16.
shake:amplitude— насколько сильно трясёт, до16(по умолчанию4);durationв секундах (1);frequency— толчков в секунду (5).statusIcon(sprite, state, color): спрайт из игровогоsprites/hud.txt("dmg_cold","buyzone","c4", …) — показан, мигает или скрыт.roundTime(seconds): часы раунда вверху его HUD.hideHud(parts): сразу скрывает эти части его HUD. Свойствоplayer.hideHud(Флаги) делает то же со следующего кадра игры.crosshair(shown): собственный прицел игры.flashlight(on, battery?): значок фонарика и заряд батареи в процентах.progressBar(seconds, { startPercent }): полоса посреди его экрана, заполненная черезsecondsсекунд;0её убирает. СstartPercentс v0.2 она начинается заполненной настолько и заполняет остатокseconds: при50— за половину.
Общие поля игрока
Плагины могут добавлять в Player поля друг для друга: одно значение на
игрока, которое читают и пишут все плагины сервера — и на TypeScript, и на
Pawn. Так один плагин сообщает остальным, что игрок защищён после появления,
заморожен или получил бонус.
Плагин, которому принадлежат поля, объявляет их в своём маленьком файле, в папке внутри папки плагинов:
// plugins/myplugin/player.ts
import "@amxts/core";
declare module "@amxts/core" {
interface Player {
spawnProtected: boolean;
kills: number;
bonus: number;
tag: string;
glow: {
enabled: true | false | "default";
seenBy: Player[];
};
}
}
Каждый плагин, который ими пользуется, — и сам владелец тоже — импортирует этот файл, и поля становятся свойствами каждого игрока:
// plugins/shop.ts
import "~/myplugin/player";
function onKill(killer: Player, victim: Player) {
if (victim.spawnProtected) return;
killer.kills = killer.kills + 1;
killer.tag = "охотник";
killer.glow.enabled = true; // один член, записывается сразу
killer.glow.seenBy.push(victim); // push записывает обратно
killer.glow = { enabled: "default", seenBy: [] };
}
Файл в папке внутри папки плагинов — не отдельный плагин, а код, который
плагины импортируют по его пути после ~/.
- Типы. Поле —
boolean,number,string, юнион строковых литералов ("red" | "blue", аtrue | false | "default"— одно значение: да, нет или слово),Player[]или объект из них. Необязательных полей не бывает, и члены объекта — не объекты. - Поле, в которое ещё не писали, читается как
false,0,""или[], а юнион — как его первый строковый литерал (выше это"default"). - Поле-объект пишется прямо в объявлении, как
glowвыше, или интерфейсом из того же файла.player.glow.enabled = trueсразу записывает этот член; присваивание всего объекта записывает все члены. Player[]— массив, чейpushзаписывает обратно, как список флагов: чтобы убрать игрока, присвойте отфильтрованный массив.includesиindexOfнаходят игрока по его номеру. Ушедший с сервера игрок убирается из списков у всех.- Проверяется. Опечатка в поле (
player.gost) или значение не того типа — ошибка и в редакторе, и в сборке. Так же и одно поле, объявленное с двумя типами, и поле с именем, которое уPlayerуже есть (solid,health): дайте ему другое имя. - Только то, что плагин импортирует. Сборка знает поля файлов, которые
плагин импортирует, а редактор — всех файлов папки плагинов. Если редактор
поле принимает, а сборка говорит, что его нет, не хватает
import "~/myplugin/player". - Когда сбрасываются. Поля игрока очищаются, когда он уходит, — после
того как отработают обработчики
"disconnected"всех плагинов, так что обработчик ещё может их прочитать. Поля всех игроков очищаются при смене карты.amxts_reloadих сохраняет.
Как узнать об изменении
Когда у игрока меняется поле, сервер поднимает событие "playerChange".
Каждая запись, которая меняет значение, — любого плагина, на TypeScript или
Pawn, — сразу доходит до всех плагинов, которые слушают это поле. Какое
именно, говорит field в третьем аргументе:
import "~/myplugin/player";
server.addEventListener("playerChange", (event) => {
print(event.player, event.value ? "Вы под защитой" : "Защита после появления закончилась");
}, { field: "spawnProtected" });
event.value и event.previous — значение поля после изменения и до него,
типа этого поля: здесь boolean, для glow — объект. Плагин, который
слушает spawnProtected, не вызывается, когда меняется kills. Поле
пишется явно, как и имя события, и сборка проверяет, что это поле, которое
плагин импортирует.
Без field слышно любое поле, а какое — говорит event.field; значение
читается у игрока:
server.addEventListener("playerChange", (event) => {
console.log(`${event.player.name}: ${event.field} changed`);
});
Обработчик, написанный отдельной функцией, принимает событие своего поля:
server.addEventListener("playerChange", onProtection, { field: "spawnProtected" });
function onProtection(event: PlayerChangeEvent<"spawnProtected">) {
event.player.renderMode = event.value ? "additive" : "normal";
}
- Поле-объект меняется по одному члену, и
event.field— имя члена через точку,"glow.enabled".{ field: "glow.enabled" }слушает этот член;{ field: "glow" }— каждый из них, и вevent.valueвесь объект. Присваивание всего объекта — по изменению на каждый член, чьё значение другое. Player[]меняется, когда меняется список:pushили присвоенный список.- Не изменение: запись значения, которое у поля уже есть; уход игрока —
его поля и его место в списках других уходят без события, для этого есть
"disconnected"; смена карты. - Сразу. Обработчик выполняется внутри записи, до следующей строки плагина, который писал. Обработчик, который пишет поле, снова поднимает событие — для этого поля.
Из Pawn
Плагин на Pawn читает и пишет те же поля через amxts.inc.
amxts build --deploy и amxts dev кладут его в
addons/amxmodx/scripting/include сервера. Ключ — имя поля, как его
объявляет TypeScript:
#include <amxts>
if (amxts_get_player_data(id, "spawnProtected")) { ... }
amxts_set_player_data(id, "spawnProtected", 1);
new Float:bonus = amxts_get_player_data_float(id, "bonus");
amxts_set_player_data_float(id, "bonus", 1.5);
new tag[32];
amxts_get_player_data_string(id, "tag", tag, charsmax(tag));
amxts_set_player_data_string(id, "tag", "VIP");
// член поля-объекта: его ключ через точку
new enabled[16];
amxts_get_player_data_string(id, "glow.enabled", enabled, charsmax(enabled));
amxts_set_player_data_string(id, "glow.enabled", "true");
| Натив | Что делает |
|---|---|
amxts_get_player_data(id, const key[]) | числовое поле, целым; логическое — 1 или 0 |
amxts_set_player_data(id, const key[], value) | записывает числовое или логическое поле |
Float:amxts_get_player_data_float(id, const key[]) | числовое поле как Float |
amxts_set_player_data_float(id, const key[], Float:value) | записывает числовое поле |
amxts_get_player_data_string(id, const key[], out[], len) | текстовое поле; возвращает число записанных байт, обрезает перед буквой, которая не влезла |
amxts_set_player_data_string(id, const key[], const value[]) | записывает текстовое поле |
Поле, прочитанное не тем видом (текстовое через amxts_get_player_data),
читается как 0 или "". Юнион литералов — текст: true | false | "default" — это "true", "false" или "default", а пока в поле не
писали — "" (TypeScript читает это как первый литерал, "default").
Player[] — тоже текст, номера через запятую: "3,5".
Игроки и сущности
Всё в игровом мире — сущности: игроки, оружие, гранаты, двери, бомба. В плагине сущность — это объект, а её данные — свойства, которые читаются и присваиваются:
Эффекты
Временный эффект — то, что игра рисует на мгновение и забывает: луч, ударная волна, взрыв, искры, свет, кровь. Каждый эффект — функция effects, её настройки — аргументы эффекта, а второй аргумент говорит, кто его видит.