Основы

События игры

То, что происходит в игре, — игрок получает урон, возрождается, умирает, покупает оружие, бросает гранату, раунд заканчивается, — это события на 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
Ham Sandwich перехватывает функцию игры по одному классу сущностей: обработчик события только об одном классе без classname не добавляется, и консоль об этом говорит. Он вызывает хуки всех плагинов, что бы один из них ни ответил, поэтому stopImmediatePropagation() у события, которое он доставил, блокирует функцию игры, как preventDefault(), но не обработчики других плагинов.
Сервер без ReAPI
На сервере без 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.damagenumber
traceAttackвыстрел попал в игрока, до урона-
playerKilled¹игрока убили: victim, killer, inflictor-
playerSpawn¹игрок возродился-
giveDefaultItems²игра выдаёт возродившемуся игроку стартовое оружие-
newRound¹начинается новый раунд-
roundStart¹закончилось время заморозки в начале раунда-
roundEnd¹раунд заканчивается: winner, reason, delayboolean
buyWeapon¹игрок покупает оружие: weaponEntity: купленное оружие
itemRestricted¹игра спрашивает, запрещён ли игроку предметboolean: true запрещает
canPlayerHearPlayer¹игра спрашивает, слышит ли listener игрока sender в голосовом чатеboolean
fallDamage²игра считает урон от паденияnumber
addMoney¹меняются деньги игрока: amount, reason-
chooseTeam¹игрок выбирает команду в меню: choicenumber
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
("в голову");
});
СобытиеПолеИмена
roundEndwinnerRoundWinner: "TERRORIST", "CT", "draw", "none"
roundEndreasonRoundEndReason: "ctsWin", "terroristsWin", "targetSaved", "targetBomb", "bombDefused", "endDraw", "gameRestart", "none", …
roundEnddelayсекунды до следующего раунда
canSwitchTeamteamTeam
chooseTeamchoiceTeamChoice: "TERRORIST", "CT", "VIP", "auto", "SPECTATOR"
buyWeaponweaponWeaponKind, в тех же именах, что weapon.kind: "ak47", "awp", …
addMoneyreasonRewardReason: "roundBonus", "enemyKilled", …
itemRestrictedrestrictionItemRestriction: "buying", "touched", "equipped"
itemRestricteditemItemKind: оружие по виду ("awp", "hegrenade", …) или "kevlar", "assault", "defusekit", "nvg", …
painlastHitGroupHitGroup: "head", "chest", "stomach", "leftArm", … — как их называет player.lastHitGroup
setAnimationanimationPlayerAnimation: "idle", "walk", "jump", "attack1", "reload", …
addResourceresourceTypeResourceType: "sound", "model", "decal", "generic", "eventscript", …
gameEventgameEventBotEvent: "playerDied", "bombPlanted", …
showVguiMenumenuVguiMenu: "team", "classT", "classCT", "buy", …
sendDeathMessageflagsDeathMessageFlag[]: "position", "assistant", "killRarity"
sendDeathMessagerarityKillRarity[]: "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, и для игрока, заглушившего другого: ответ перекрывает и то и другое
changeLevelpreventDefault() ничего не делает
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() и запись поля ничего не делают
precacheFilepreventDefault() пропускает прекэш, и он отвечает 0; ответ и запись file ничего не делают
precacheModelpreventDefault() пропускает прекэш, и он отвечает 0; ответ и запись file ничего не делают
precacheSoundpreventDefault() пропускает прекэш, и он отвечает 0; ответ и запись file ничего не делают
roundEndСлышно, когда игра уже сделала своё, поэтому preventDefault() и запись поля ничего не делают; ответ ничего не делает; delay — 5 секунд оригинальной игры, 3 для "gameCommence", если его не задал game.endRound
roundStartСлышно, когда игра уже сделала своё, поэтому preventDefault() и запись поля ничего не делают
sendDeathMessagepreventDefault() отменяет сообщение; assister и inflictor читаются как мир, flags пуст, в rarity бывает только "headshot"; запись поля ничего не делает
setModelpreventDefault() не даёт модели встать; запись 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.

События, которые без ReAPI никто не слышит
Остальные события 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", вместо обычного; "" — без звука
dispatchtrue сообщает об этом конце раунда обработчикам roundEnd всех плагинов, и плагинов на Pawn тоже, как будто раунд закончила сама игра. По умолчанию false: иначе обработчик roundEnd, который заканчивает раунд, вызывал бы сам себя
Сервер без ReAPI
Без 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 — сразу виден на таблице, как когда игра засчитывает раунд.

Сервер без ReAPI
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 игра уже пошла дальше: обработчик ещё может действовать — написать в чат, выдать предмет, — но то, что он вернёт, до игры уже не дойдёт.

Ограничения

  • Поле-вектор записывается целиком. event.direction = new Vector(0, 0, 1) поворачивает выстрел traceAttack; event.direction.z = 1 меняет копию, и игра её не увидит.
  • Сообщения клиентам слышны через server.addMessageListener (Сообщения); временные эффекты — это эффекты.