События игры
То, что происходит в игре, — игрок получает урон, возрождается, умирает,
покупает оружие, бросает гранату, раунд заканчивается, — это события на
game. Обработчик слышит событие до того, как игра что-то сделает, и может
пропустить его, изменить, остановить или ответить вместо игры:
// без урона по своим
game.addEventListener("takeDamage", (event) => {
if (event.attacker.team == event.player.team) event.preventDefault();
});
// двойной урон с ножа
game.addEventListener("takeDamage", onTakeDamage);
function onTakeDamage(event: TakeDamageEvent) {
const weapon = event.attacker.activeItem;
if (weapon && weapon.kind == "knife") event.damage = event.damage * 2;
}
// игра спрашивает, плагин отвечает: игроки слышат только свою команду
game.addEventListener("canPlayerHearPlayer", (event) => event.listener.team == event.sender.team);
События игры приходят из модулей ReAPI и Ham Sandwich.
game.addEventListener работает так же, как
server.addEventListener (Плагин): имя задаёт
тип event, редактор дополняет имена и подчёркивает опечатку, а
game.removeEventListener с той же функцией снимает обработчик.
Когда снят последний обработчик, событие перестаёт приходить в плагин, пока
обработчик не добавят снова.
Что может обработчик
| Чтобы | Напишите |
|---|---|
| игра шла дальше | ничего не возвращайте |
| остановить действие игры | event.preventDefault() |
| изменить то, что получит игра | присвойте поле: event.damage = 10 |
| ответить вместо игры | верните ответ: return true |
| остановить игру и все обработчики после этого | event.stopImmediatePropagation() |
Изменённое поле получат игра и обработчики после этого; если прочитать поле после записи, оно даст новое значение.
Возвращённое значение — ответ на вопрос игры: «слышит ли этот игрок
того?», «сколько урона от падения?», «можно ли ему это купить?», — и свой
код игра уже не выполняет. У ответа тип события (boolean, number,
Player, Entity): вернуть другой — ошибка и в редакторе, и в сборке.
Событие, чья функция в игре ничего не отвечает, ответа не принимает.
preventDefault() останавливает действие игры без ответа: урон не
наносится, стартовое оружие не выдаётся. Там, где игра ждёт ответ, она
получает 0 или false.
Функция возвращает значение либо на всех путях, либо ни на одном. Обработчик, который должен отвечать лишь иногда, ничего не возвращает и вызывает
event.preventDefault() там, где нужно остановить игру, — или
отвечает всегда, повторяя правило самой игры для остальных случаев.После того, как игра сработала
game.addEventListener("fallDamage", (event) => event.result / 2, true); // вдвое меньше урона от падения
game.addEventListener("playerSpawn", (event) => {
event.player.give("weapon_deagle");
}, true);
С true третьим аргументом — или { post: true }, это то же самое —
обработчик выполняется после функции игры. event.result — то, что игра
ответила, а возвращённое значение его заменяет. Менять поля здесь
бесполезно: игра их уже использовала. Чтобы снять такой обработчик,
передайте true и в removeEventListener.
Касания
game.addEventListener("touch", onTouch, { toucher: "player", touched: "player" });
function onTouch(event: TouchEvent) {
const player = new Player(event.toucher.id);
const below = new Player(event.touched.id);
if (player.groundEntity == below.id) console.log(`${player.name} стоит на ${below.name}`);
}
"touch" — касание двух сущностей: event.toucher вошла в event.touched,
обе — Entity. Касание случается каждый кадр у каждого игрока, который на
чём-то стоит, поэтому третий аргумент называет классы, о которых обработчик, — toucher, touched или оба, — и до плагина доходят только эти касания:
сервер отсеивает их раньше. Класс, который не задан, — любой класс.
event.preventDefault() блокирует касание. Два игрока, коснувшиеся друг
друга, — это два касания, по одному в каждую сторону.
Один класс сущностей
game.addEventListener("primaryAttack", onSlash, { classname: "weapon_knife" });
function onSlash(event: PrimaryAttackEvent) {
const knife = event.weapon;
knife.secondaryAttack(); // вместо этого правый клик; на нём срабатывают обработчики всех плагинов
event.preventDefault(); // а левый — нет
}
game.addEventListener("takeDamage", (event) => {
console.log(`${event.entity.classname} took ${event.damage}`);
}, { classname: "func_breakable" });
Атака оружия, использование двери, «мысль» сущности — события одного класса
сущностей: его называет classname в третьем аргументе, и до плагина доходят
только его сущности — сервер отсеивает остальные раньше. { classname, post: true } слушает после того, как игра сработала.
Событие об игроке — takeDamage, killed, spawn — без classname относится
к игроку, а с ним — к другому классу: event.player — игрок, event.entity —
сущность названного класса. Событие об оружии — canDeploy, itemPostFrame —
без classname относится ко всему оружию, с ним — к одному классу. Событию
только об одном классе сущностей — primaryAttack, deploy, think, use —
нужен classname. Поле такого события можно присвоить там, где игра его
принимает обратно, — сущность и вектор тоже.
Ham Sandwich перехватывает функцию игры по одному классу сущностей: обработчик события только об одном классе без
classname не добавляется, и
консоль об этом говорит. Он вызывает хуки всех плагинов, что бы один из них
ни ответил, поэтому stopImmediatePropagation() у события, которое он
доставил, блокирует функцию игры, как preventDefault(), но не обработчики
других плагинов.На сервере без ReAPI — обычном HLDS — событие игрока тоже доставляет Ham Sandwich, а событие оружия без
classname перехватывается на классе каждого
оружия: обработчик пишется так же и слышит то же, и к нему относится то, что
сказано выше о событиях Ham Sandwich. События, которые есть только у
ReGameDLL и ReHLDS, приходят там от других хуков, некоторые — не целиком
(сервер без ReAPI).Те же функции — методы сущности: игра выполняет их так же, как сама, и обработчики всех плагинов об этом слышат:
knife.deploy(); // снова в руках: модель и анимация
knife.deploy({ hooks: false }); // только функция игры, без обработчиков
player.jump();
door.use(player, player, "toggle", 0);
| Метод | У кого |
|---|---|
spawn, activate, think, use, blocked, heal, killed, restart | у любой сущности |
addFrags, addTeamScore, addItem, removeItem, giveAmmo, jump, duck | у игрока |
deploy, holster, drop, primaryAttack, secondaryAttack, reload, weaponIdle, retireWeapon, addToPlayer, attachToPlayer, extractAmmo, extractClipAmmo, resetEmptySound, sendWeaponAnim | у оружия |
Что есть у события
- Поля — данные события с типами:
event.damage—number,event.damageType—Damage[](флаги), текст —string. Поле называется словами:event.trace,event.info,event.direction,event.start; подсказка называет параметр так, как его называет ReAPI. - Сущности — объекты: игрок —
Player, оружие —Weapon, всё остальное —Entity.event.attacker.team,event.player.health. event.player— игрок, о котором событие: тот, кто получает урон, возрождается, прыгает, ослеплён.event.resultв обработчике, который выполняется после игры, — ответ игры.
Наведите на имя события — подсказка скажет, что это за событие, на поле —
что в нём лежит. Последняя строка подсказки называет его так, как его зовут
ReAPI и Ham Sandwich (Pawn: `RG_CBasePlayer_TakeDamage`, `Ham_TakeDamage` ).
С чего начать
| Событие | Когда | Ответ |
|---|---|---|
takeDamage | игрок вот-вот получит урон: player, attacker, inflictor, damage, damageType; чтобы изменить урон, присвойте event.damage | number |
traceAttack | выстрел попал в игрока, до урона | - |
playerKilled¹ | игрока убили: victim, killer, inflictor | - |
playerSpawn¹ | игрок возродился | - |
giveDefaultItems² | игра выдаёт возродившемуся игроку стартовое оружие | - |
newRound¹ | начинается новый раунд | - |
roundStart¹ | закончилось время заморозки в начале раунда | - |
roundEnd¹ | раунд заканчивается: winner, reason, delay | boolean |
buyWeapon¹ | игрок покупает оружие: weapon | Entity: купленное оружие |
itemRestricted¹ | игра спрашивает, запрещён ли игроку предмет | boolean: true запрещает |
canPlayerHearPlayer¹ | игра спрашивает, слышит ли listener игрока sender в голосовом чате | boolean |
fallDamage² | игра считает урон от падения | number |
addMoney¹ | меняются деньги игрока: amount, reason | - |
chooseTeam¹ | игрок выбирает команду в меню: choice | number |
throwHeGrenade¹ | игрок бросает осколочную гранату | Entity |
playerBlind¹ | флешка ослепляет игрока: fadeTime, fadeHold, alpha | - |
¹ На сервере без ReAPI слышно не всё, ² там не слышно: сервер без ReAPI.
Все события, их около 250, показывает редактор.
Событие называется по тому, что делает игра, в camelCase, какой бы модуль его
ни слышал: RG_CBasePlayer_TakeDamage и Ham_TakeDamage — оба "takeDamage",
RG_CSGameRules_PlayerSpawn — "playerSpawn" (появление игрока в раунде), а
RG_CBasePlayer_Spawn — "spawn", Ham_Weapon_PrimaryAttack —
"primaryAttack". Тип события — имя в PascalCase с Event: TakeDamageEvent,
PrimaryAttackEvent.
canPlayerHearPlayer игра спрашивает, только пока sv_alltalk равен 0: с
включённым alltalk она не спрашивает никого. Чтобы заглушить игрока при любом
alltalk, есть player.muted (Игроки).
Перечисления и флаги — имена
Поле, в котором лежит одно значение из фиксированного набора, — это имя, а поле с битовыми флагами — массив имён:
game.addEventListener("roundEnd", (event) => {
if (event.reason == "targetSaved") console.log("точку отстояли");
if (event.winner == "CT") console.log("победил спецназ");
});
game.addEventListener("showVguiMenu", (event) => {
if (event.menu == "team") event.preventDefault();
});
game.addEventListener("sendDeathMessage", (event) => {
if (event.rarity.includes("headshot")) console.log("в голову");
});
| Событие | Поле | Имена |
|---|---|---|
roundEnd | winner | RoundWinner: "TERRORIST", "CT", "draw", "none" |
roundEnd | reason | RoundEndReason: "ctsWin", "terroristsWin", "targetSaved", "targetBomb", "bombDefused", "endDraw", "gameRestart", "none", … |
roundEnd | delay | секунды до следующего раунда |
canSwitchTeam | team | Team |
chooseTeam | choice | TeamChoice: "TERRORIST", "CT", "VIP", "auto", "SPECTATOR" |
buyWeapon | weapon | WeaponKind, в тех же именах, что weapon.kind: "ak47", "awp", … |
addMoney | reason | RewardReason: "roundBonus", "enemyKilled", … |
itemRestricted | restriction | ItemRestriction: "buying", "touched", "equipped" |
itemRestricted | item | ItemKind: оружие по виду ("awp", "hegrenade", …) или "kevlar", "assault", "defusekit", "nvg", … |
pain | lastHitGroup | HitGroup: "head", "chest", "stomach", "leftArm", … — как их называет player.lastHitGroup |
setAnimation | animation | PlayerAnimation: "idle", "walk", "jump", "attack1", "reload", … |
addResource | resourceType | ResourceType: "sound", "model", "decal", "generic", "eventscript", … |
gameEvent | gameEvent | BotEvent: "playerDied", "bombPlanted", … |
showVguiMenu | menu | VguiMenu: "team", "classT", "classCT", "buy", … |
sendDeathMessage | flags | DeathMessageFlag[]: "position", "assistant", "killRarity" |
sendDeathMessage | rarity | KillRarity[]: "headshot", "noScope", "penetrated", "inAir", … |
Присвоенное имя меняет значение, которое получит игра:
event.reason = "terroristsWin". Значение, для которого нет имени, — его
передал другой плагин или мод, — читается как "unknown" ("none" у
RoundWinner и WeaponKind, "UNASSIGNED" у Team), а присваивание
"unknown" оставляет поле таким, каким его передала игра. Флаги без имени
при присваивании массива флагов сохраняются.
Сервер без ReAPI с v0.2
У ReGameDLL и ReHLDS есть собственные события — появление игрока в раунде,
начало и конец раунда, деньги, покупки, — их доставляет ReAPI. На сервере
без них — HLDS от Valve с AMX Mod X — большую часть из них слышно так, как
их слышит там плагин на Pawn: новый раунд — по сообщению HLTV, конец
заморозки и конец раунда — по логу игры, появление и смерть — через Ham
Sandwich, деньги — по сообщению Money, покупку — через cstrike. Плагин
пишется так же; способ amxts выбирает один раз, когда сервер
стартует.
Чего такое событие там дать не может, сказано в таблице ниже и в подсказке
редактора к событию. Обработчик, который всё равно этого просит, —
preventDefault(), ответ, запись поля, — это одна строка в консоли сервера,
один раз на событие; поле, которое событию нечем заполнить, читается как
0, "", null или мир.
| Событие | На сервере без ReAPI |
|---|---|
addMoney | Слышно, когда игра уже сделала своё, поэтому preventDefault() и запись поля ничего не делают; amount — на сколько сдвинулись его деньги с тех пор, как игра в последний раз их ему прислала, а reason читается как "none" |
becomeBomber | Слышно, когда игра уже сделала своё, поэтому preventDefault() и запись поля ничего не делают; ответ ничего не делает |
becomeVip | Слышно, когда игра уже сделала своё, поэтому preventDefault() и запись поля ничего не делают |
buyAmmo | Слышно через CS_OnBuy модуля cstrike: preventDefault() отменяет покупку; weapon читается как мир, blinkMoney — как true; ответ ничего не делает |
buyItem | Слышно через CS_OnBuy модуля cstrike: preventDefault() отменяет покупку; слышно для предметов меню снаряжения; ответ ничего не делает |
buyWeapon | Слышно через CS_OnBuy модуля cstrike: preventDefault() отменяет покупку; event.result читается как null; ответ ничего не делает |
canPlayerHearPlayer | Спрашивается, когда игра сообщает движку, кто кого слышит, — и при включённом sv_alltalk, и для игрока, заглушившего другого: ответ перекрывает и то и другое |
changeLevel | preventDefault() ничего не делает |
changeName | Слышно, когда игра уже сделала своё, поэтому preventDefault() и запись поля ничего не делают; info читается как ""; ответ ничего не делает |
chooseAppearance | Слышно по команде игрока: preventDefault() её отменяет; модель, которую игра выбирает сама, не слышна |
chooseTeam | Слышно по команде игрока: preventDefault() её отменяет; ответ ничего не делает, а команда, которую игра выбирает сама, не слышна |
clientConnected | Слышно, когда игра уже сделала своё, поэтому preventDefault() и запись поля ничего не делают |
connectClient | Слышно, когда игра уже сделала своё, поэтому preventDefault() и запись поля ничего не делают; слышно, когда игрока уже пустили |
deathNotice | Слышно, когда уходит сообщение о смерти: preventDefault() ничего не делает, а inflictor читается как мир |
defuseBombEnd | Слышно, когда игра уже сделала своё, поэтому preventDefault() и запись поля ничего не делают; слышно, только когда бомба обезврежена, а не когда разминирование прервано |
defuseBombStart | Слышно, когда игра уже сделала своё, поэтому preventDefault() и запись поля ничего не делают |
disconnectClient | Слышно, когда игра уже сделала своё, поэтому preventDefault() и запись поля ничего не делают; crash читается как false, а reason — причина, которую узнал AMX Mod X |
dropPlayerItem | Слышно по команде игрока drop: preventDefault() её отменяет; event.result читается как null, а выброс, который игра делает сама, не слышен |
explodeBomb | Слышно, когда игра уже сделала своё, поэтому preventDefault() и запись поля ничего не делают; trace и damageType читаются как 0 |
gameThink | Слышно, когда игра уже сделала своё, поэтому preventDefault() и запись поля ничего не делают |
gibSpawn | Слышно, когда игра уже сделала своё, поэтому preventDefault() и запись поля ничего не делают |
giveBomb | Слышно, когда игра уже сделала своё, поэтому preventDefault() и запись поля ничего не делают; ответ ничего не делает |
intermission | Слышно, когда игра уже сделала своё, поэтому preventDefault() и запись поля ничего не делают |
itemRestricted | Спрашивается только про "buying", через CS_OnBuyAttempt модуля cstrike: ответ true запрещает покупку, false оставляет решение игре |
mapReset | Слышно, когда игра уже сделала своё, поэтому preventDefault() и запись поля ничего не делают |
newRound | Слышно при перезапуске раунда — обработчики «до» игры, когда она объявляет раунд, обработчики «после» — когда её игроки возродились, — но preventDefault() ничего не делает |
plantBomb | Слышно, когда игра уже сделала своё, поэтому preventDefault() и запись поля ничего не делают; слышно, когда бомба получает модель; ответ ничего не делает |
playerBlind | Слышно, когда игра уже сделала своё, поэтому preventDefault() и запись поля ничего не делают; inflictor и attacker читаются как мир, color — нулевым |
playerGotWeapon | Слышно, когда игра уже сделала своё, поэтому preventDefault() и запись поля ничего не делают |
playerKilled | Слышно, когда игра уже сделала своё, поэтому preventDefault() и запись поля ничего не делают |
playerSpawn | Слышно, когда игра уже сделала своё, поэтому preventDefault() и запись поля ничего не делают |
precacheFile | preventDefault() пропускает прекэш, и он отвечает 0; ответ и запись file ничего не делают |
precacheModel | preventDefault() пропускает прекэш, и он отвечает 0; ответ и запись file ничего не делают |
precacheSound | preventDefault() пропускает прекэш, и он отвечает 0; ответ и запись file ничего не делают |
roundEnd | Слышно, когда игра уже сделала своё, поэтому preventDefault() и запись поля ничего не делают; ответ ничего не делает; delay — 5 секунд оригинальной игры, 3 для "gameCommence", если его не задал game.endRound |
roundStart | Слышно, когда игра уже сделала своё, поэтому preventDefault() и запись поля ничего не делают |
sendDeathMessage | preventDefault() отменяет сообщение; assister и inflictor читаются как мир, flags пуст, в rarity бывает только "headshot"; запись поля ничего не делает |
setModel | preventDefault() не даёт модели встать; запись modelName ничего не делает |
showVguiMenu | Слышно, когда меню уходит игроку с включёнными VGUI-меню (не боту): preventDefault() его отменяет; oldMenu читается как "", запись поля ничего не делает |
startSound | Слышно для звуков, которые игра проигрывает через EmitSound движка: preventDefault() его отменяет; recipients читается как 0, запись поля ничего не делает |
throwFlashbang | Слышно, когда игра уже сделала своё, поэтому preventDefault() и запись поля ничего не делают; слышно, когда граната получает модель |
throwGrenade | Слышно, когда игра уже сделала своё, поэтому preventDefault() и запись поля ничего не делают; слышно, когда граната получает модель, а eventIndex читается как 0 |
throwHeGrenade | Слышно, когда игра уже сделала своё, поэтому preventDefault() и запись поля ничего не делают; слышно, когда граната получает модель, а eventIndex читается как 0 |
throwSmokeGrenade | Слышно, когда игра уже сделала своё, поэтому preventDefault() и запись поля ничего не делают; слышно, когда граната получает модель, а eventIndex читается как 0 |
userInfoChange | Слышно, когда игра уже сделала своё, поэтому preventDefault() и запись поля ничего не делают; info читается как "" |
Порядок раунда — как у игры: newRound до того, как игроки возродятся,
их playerSpawn, newRound после игры, когда они живы,
roundStart, roundEnd.
Остальные события ReGameDLL и ReHLDS — вопросы правил игры (
fallDamage, canHaveItem, canRespawn, …),
движение игрока (jumpMovement, move, …), собственные события движка
(printf, addResource, …), giveDefaultItems и взрывы гранат — на обычном
HLDS не слышит никто. Обработчик такого события не вызывается никогда:
консоль говорит об этом один раз, когда его добавляют, об этом говорит
подсказка редактора к событию, а проект с target: "hlds" его не собирает.
Слушайте событие, которое там слышно, — playerSpawn вместо
giveDefaultItems, takeDamage для урона от падения, — или сначала
спросите hasModule("reapi").Конец раунда
game.endRound({ winner: "TERRORIST" }); // победа террористов; следующий раунд через 5 с
game.endRound({ winner: "CT", delay: 3 });
game.endRound({ winner: "draw", message: "Никто не победил" });
game.endRound({ winner: "none", delay: 0.1, message: "" }); // тихий рестарт
| Настройка | Значение |
|---|---|
winner | "TERRORIST", "CT", "draw" или "none" — кому очко, и обычные для такого конца сообщение и звук |
delay | секунды до следующего раунда; по умолчанию 5 |
message | сообщение посреди экрана или игровой текст вроде "#Terrorists_Win" вместо обычного; "" — без сообщения |
sound | звук рации, например "terwin", вместо обычного; "" — без звука |
dispatch | true сообщает об этом конце раунда обработчикам roundEnd всех плагинов, и плагинов на Pawn тоже, как будто раунд закончила сама игра. По умолчанию false: иначе обработчик roundEnd, который заканчивает раунд, вызывал бы сам себя |
Без ReAPI раунд кончается так же — победитель, задержка, сообщение и звук, — а
dispatch сообщает о нём так, как там сообщает игра: сообщение и звук
уходят хукам сообщений всех плагинов, а строки лога о конце раунда —
обработчикам roundEnd и logevent-ам плагинов на Pawn.Правила игры
if (game.isFreezeTime) console.log("раунд ещё не начался");
game.isFreezeTime = false; // закончилась — для собственных проверок игры
game.ctWins = 0; // счёт, на таблице сразу
if (game.roundWinner == "CT") console.log("последний раунд взял спецназ");
Собственное состояние игры — заморозка, счёт, время раунда, что есть на
карте — это поля game, типизированные так же, как поля игрока: число,
булево значение, текст, победитель раунда именем. Имена — слова игрока для
члена: m_bFreezePeriod — это isFreezeTime, m_iNumCTWins — ctWins,
m_iRoundWinStatus — roundWinner, остальные редактор показывает вместе со
значением и именем члена. Записанный счёт команды — ctWins, terroristWins — сразу
виден на таблице, как когда игра засчитывает раунд.
ReGameDLL добавляет к правилам игры поля, которые сервер без него хранит в другом месте или не хранит вовсе.
gameName — имя игры в списке серверов, и
запись меняет его там; timeLimit и gameStartTime
считаются из mp_timelimit и оставшегося времени AMX Mod X, которое идёт с
объявления рестарта, а запись timeLimit меняет mp_timelimit;
maxPlayers — слоты сервера, и запись в него ничего не делает. teamBalanced,
neededPlayers, skipShowMenu, escapeRatio и updateInterval у него
нет: они читаются как 0 и не записываются, а консоль говорит об этом один
раз.Async-обработчики
Обработчик может быть async и ждать внутри через await (async).
То, что он вернёт до первого await, — его ответ игре. После await игра
уже пошла дальше: обработчик ещё может действовать — написать в чат, выдать
предмет, — но то, что он вернёт, до игры уже не дойдёт.
Ограничения
Плагин
Плагин — это один файл .ts в папке плагинов проекта. Работу он делает на верхнем уровне файла: называет себя, добавляет команды и подписывается на события. То, чем он пользуется из ядра, — plugin, server, Player, print — строки импорта не требует (автоимпорты).
Форварды
Форвард — событие, которое один плагин поднимает, а другие слышат, и на TypeScript, и на Pawn. Так плагин сообщает остальному серверу, что начался раунд его режима, что игрок что-то купил, что изменилась настройка.