This article is the documentation for the game "Team Fortress 2". Click here to go to the main page of the development documents.

TF2 VScript Examples

From Valve Developer Community
Jump to navigation Jump to search
English (en)Русский (ru)Translate (Translate)


Team Fortress 2 Эта страница содержит примеры скриптов: vscripts(en) для Team Fortress 2.

Итерация по сущностям

С помощью цикла while и функции Entities.FindByClassname() вы можете перебрать все сущности с подходящим именем класса, основываясь на ваших аргументах.

Первый параметр Entities.FindByClassname() называется 'previous' (предыдущий), который принимает хэндл скрипта (если сущность наследует 'CBaseEntity'), который проверяет, имеет ли найденная сущность индекс сущности выше, чем текущий в аргументе 'previous' (предыдущий). Если это не так, то он игнорируется.

local ent = null
while( ent = Entities.FindByClassname(ent, "prop_physics") )
{
   printl(ent)
}

}}

Итерация по Игрокам

Это может быть полезно для итерации только по игрокам. Однако делать это с помощью Entities.FindByClassname() неэффективно, так как нужно перебирать каждую сущность. Для эффективного перебора игроков можно использовать причуду. Каждая сетевая сущность имеет связанный с ней 'индекс сущности', который варьируется от 0 до MAX_EDICTS(en). Обычно они непредсказуемы, однако есть две группы сущностей, которые имеют зарезервированные индексы: worldspawn(en) и игроки. Worldspawn(en) is always reserved at entity index 0, and players are reserved from entity index 1 to [[Maxplayers|maxplayer всегда зарезервирован под индекс сущности 0, а игроки зарезервированы от индекса сущности 1 до maxplayers(en) + 1. Используя этот факт, игроков можно просто итерировать, как показано ниже:

for (local i = 1; i <= Constants.Server.MAX_PLAYERS; i++)
{
    local player = PlayerInstanceFromIndex(i)
    if (player == null) continue
    printl(player)
}

Создание entity

Этот код показывает, как создать entity, а именно ракету. Ракета будет создана перед первым доступным игроком.

// Код от ZooL_Smith

local ply = Entities.FindByClassname(null, "player")

local rocket = SpawnEntityFromTable("tf_projectile_rocket", 
{
    // Это таблица ключевых значений, которая аналогична ключевым значениям, определенным в Hammer Editor
    // Параметр    Значение
    basevelocity = ply.EyeAngles().Forward()*250,
    teamnumber   = ply.GetTeam(),
    origin       = ply.EyePosition()+ply.EyeAngles().Forward()*32,
    angles       = ply.EyeAngles()
})

rocket.SetOwner(ply) // условие, чтобы ракета не сталкивалась с владельцем и давала соответствующие очки за убийство

Изменение команды !activator (игрока)

Whenever a script's code gets executed by inputs (e.g.,RunScriptCode/CallScriptFunctionon an entity with an attached entity script(en)), the activator and caller variables will be set to the handles of the input's !activator and !caller respectively, allowing you to access them in code.

Всякий раз, когда код скрипта выполняет input'ы (например, RunScriptCode/CallScriptFunction на entity с присоединенным скриптом сущности), переменные !activator и !caller будут установлены в хэндлы !activator и !caller входа соответственно, что позволит вам получить к ним доступ в коде.

Мы можем использовать это, чтобы, например, легко сделать что-то с игроками, которые идут в определенном триггере.

Что касается смены команды !activator, нам нужно изменить их команду в зависимости от того, в какой команде они находятся, а также изменить команду их косметических предметов на их новую команду.

Сначала нам нужно выяснить, в какой команде находится !activator. Для этого мы можем использовать метод GetTeam() на !activator (activator.GetTeam()), чтобы получить номерной индекс его команды. Вы можете либо использовать функцию: If для сравнения возвращаемого значения с нужным индексом команды, либо сохранить его в переменной, чтобы использовать позже. Последнее в данном случае может быть лучше для сокращения длины скрипта. Для справки: 0 — Unassigned (команды еще нет), 1 — Наблюдатели, 2 — Команда красных, 3 — Команда синих.

Чтобы изменить команду !activator'а, мы должны использовать метод ForceChangeTeam(Int : Team index, Bool : Kill player if game is in Highlander mode + remove their dominations) на них через activator.ForceChangeTeam(...).

Чтобы изменить цвет их косметических предметов, нам нужно перебрать все tf_wearable entity, а затем изменить их команду с помощью SetTeam(Int : Team index), если их носит !activator.

  • Для итерации нам нужно указать нулевую переменную (например, local cosmetic = null), а затем передать ее в следующий цикл while:
local cosmetic = null // Присвоить переменной "cosmetic" значение null, ей будут присваиваться новые значения при переходе к косметическим предметам
while (cosmetic = Entities.FindByClassname(cosmetic, "tf_wearable"))
{
    // Здесь выполняются действия с отдельными entity — tf_wearables, переменная "cosmetic" устанавливается на текущую сущность tf_wearable, через которую мы итерируем
}
  • Чтобы проверить, принадлежит ли косметический предмет !activator (игроку), мы можем просто сравнить значение, возвращаемое методом GetOwner(), когда мы используем его на tf_wearable (например, cosmetic.GetOwner()), с !activator (игроком). Пример кода:
if (cosmetic.GetOwner() == activator)
{
    // Do things if the cosmetic's wearer is the !activator
}
  • Чтобы изменить команду косметикого предмета (шапка, одежда), нужно использовать метод SetTeam(Int : Team index) на сущности косметического элемента (шапка, одежда).

Полный код с комментариями:

function ActivatorSwapTeam() // Вызовите это в карте через RunScriptCode > ActivatorSwapTeam() или CallScriptFunction > ActivatorSwapTeam на logic_script, который имеет Entity Script (сценарий сущности) с этой функцией
{
    // Следующий фрагмент проверяет, был ли указан !activator и является ли он игроком
    // Если ответ на любой из вопросов отрицательный, то не выполняйте остальную часть функции
    if (activator == null || activator.IsPlayer() == false)
    {
        return
    }
    
    // В следующем фрагменте сравнивается номер команды активатора (игрока) — (от 0 до 3) с номерами, используемыми в командах Unassigned (0) и Наблюдатель (1).
    // Если они совпадают с Unassigned или Наблюдатель, мы не выполняем остальную часть функции.
    // Используется для игнорирования любых потенциальных зрительских !activator'ов
    if (activator.GetTeam() == 0 || activator.GetTeam() == 1)
    {
        return
    }
    
    // Следующий фрагмент определяет локальное значение newTeam, а затем мы устанавливаем ее в номер команды, основанный на текущем номере команды !activator
    local newTeam = 0
    if (activator.GetTeam() == 2) // Проверяет, что номер команды !activator (игрока) равен значению 2 (красной команды), и устанавливает переменную newTeam равной значению 3 (синей команды).
    {   
        newTeam = 3
    } else { // Если номер команды !activator (игрока) не равен значению 2 (красной команды), вместо этого переменная newTeam устанавливается на значение 2 (синей команды).
        newTeam = 2
    }

    // Следующий фрагмент вызывает метод ForceChangeTeam на !activator (игроке)
    // Первый параметр: Номер команды, на которую нужно переключиться
    // Второй параметр: Если false, игра сбросит все доминирования над противником игрока и уберёт их, если включен mp_highlander.
    activator.ForceChangeTeam(newTeam, true)
    
    local cosmetic = null // Присвойте переменной "cosmetic" значение null, ей будут присваиваться новые значения при переходе к косметическим вещам (одежда, шапки)
    // Следующий фрагмент перебирает все имеющиеся на данный момент косметические предметы и меняет их цвета на соответствующие команде, если они принадлежат !activator (игроку)
    while (cosmetic = Entities.FindByClassname(cosmetic, "tf_wearable")) // Просматривает каждый косметический предмет (шапка, одежда), выполняя приведенный ниже код
    {
        if (cosmetic.GetOwner() == activator) // Проверяет, является ли владелец текущего итерируемого косметического предмета !activator (игроком)
        {
            cosmetic.SetTeam(newTeam) // Устанавливает команду косметических вещей (шапок, одежды) на новый номер команды, который мы сохранили в newTeam
        }
    }
}

Прослушивание Событий

Многие действия в игре вызывают события, чтобы уведомить другие части кода о том, что что-то произошло. Например, когда игрок умирает или когда здание инженера истощается. Каждое событие также может содержать данные определенного типа, например, индекс задействованного объекта.

VScript может перехватывать эти события, анализировать данные и выполнять некоторый код, когда они происходят (известный как 'callback'). Список доступных событий можно найти здесь: https://wiki.alliedmods.net/Team_Fortress_2_Events

Warning.pngПредупреждение:Обратные вызовы событий являются глобальными и не очищаются при перезапуске раунда. Обратные вызовы событий должны быть очищены через ClearGameEventCallbacks() чтобы они не накапливались между раундами.

Следующий код показывает, как прослушать событие post_inventory_application, а затем добавить uber-защиту на 2 секунды.

// Событие "post_inventory_application" отправляется, когда игрок получает новый набор предметов, а именно: прикасается к шкафчику здоровья и боеприпасов в зоне возрождения / или возрождение после смерти.
function OnGameEvent_post_inventory_application(params)
{
	local player = GetPlayerFromUserID(params.userid)

	// добавляет игроку uber-защиту на 2 секунды
	player.AddCondEx(52, 2.0, null)
}

__CollectGameEventCallbacks(this)

Настройка панели здоровья босса

Полоса боссов, появляющаяся при появлении боссов на карте (например, ГЛАЗАСТУС!), обрабатывается сущностью monster_resource, которая удобно существует на карте в обычном режиме. Вы можете сделать Entities.FindByClassname(null, "monster_resource"), чтобы получить сущность monster_resource в большинстве случаев, но было бы удобно хранить ее в переменной.

Для того чтобы изменить процент полоски здоровья, сначала нужно узнать о NetProps:

  • NetProps — Это сетевые свойства сущности, которые доступны только на стороне сервера (например боссы).
  • Они могут быть доступны и изменены с помощью методов класса NetProps

Сущность monster_resource имеет NetProp m_iBossHealthPercentageByte, который определяет процентное состояние полоски здоровья на основе значения байта — его значение должно быть между 0 и 255, где 0 — это 0%, а 255 — это 100%.

Следующий код добавит полоску здоровья с 25% здоровья после выполнения.

local healthBar = Entities.FindByClassname(null, "monster_resource") // Получите объект "Полоса здоровья".
if (healthBar && healthBar.IsValid) { // Проверьте, существует ли сущность health bar и действительна ли она, на всякий случай, чтобы предотвратить ошибки или возможные сбои.
    // Следующая строка обновит процент полоски здоровья до 25%, изменив ее NetProp.
    // Обратите внимание, что поскольку это байт (0 — 255), нам нужно умножить наш процент на 255.
    NetProps.SetPropInt(healthBar, "m_iBossHealthPercentageByte", 0.25 * 255)
}

Отключение элементов HUD

Некоторые элементы HUD можно скрыть, поместив битовое значение в один из этих NetProps:

NetProps.SetPropInt(player, "m_Local.m_iHideHUD", value )
NetProps.SetPropInt(player, "localdata.m_Local.m_iHideHUD", value )
Note.pngПримечание:Обе функции одинаковы.
Note.pngПримечание:Значение начинается с 0.

Функции бита можно найти в разделе: TF2 Script Functions.

Например:

local HideHudValue = NetProps.GetPropInt(player, "m_Local.m_iHideHUD")
NetProps.SetPropInt(player, "m_Local.m_iHideHUD", HideHudValue | (
                                                  Constants.HideHUD.HIDEHUD_CROSSHAIR |
                                                  Constants.HideHUD.HIDEHUD_HEALTH |
                                                  Constants.HideHUD.HIDEHUD_WEAPONSELECTION)
                    ) // добавляет HUD на экране игрока
NetProps.SetPropInt(player, "m_Local.m_iHideHUD", HideHudValue & ~(
                                                  Constants.HideHUD.HIDEHUD_CROSSHAIR |
                                                  Constants.HideHUD.HIDEHUD_HEALTH |
                                                  Constants.HideHUD.HIDEHUD_WEAPONSELECTION)
                    ) // удаляет HUD на экране игрока

Строка 2 добавляет биты для скрытия перекрестия, здоровья и отключения переключения оружия от игрока.

В строке 3 удалены биты для отображения перекрестия, здоровья и повторного включения переключения оружия для игрока.

Создание ботов, использующих Navmesh

Следующий код показывает пример создания примитивного бота, который перемещается с помощью navmesh(en) (навигационной сетки). При выполнении этого сценария бот будет порожден в перекрестии главного игрока. Этот бот будет просто следовать за игроком и искать путь по схеме карты. Код подробно прокомментирован, а отладочные визуализации позволяют увидеть алгоритм поиска пути в действии.

Warning.pngПредупреждение:Создание ботов — Задача не из легких. Этот код показывает, сколько работы требуется для создания простого бота!
// Constrains an angle into [-180, 180] range
function NormalizeAngle(target)
{
	target %= 360.0;
	if (target > 180.0)
		target -= 360.0;
	else if (target < -180.0)
		target += 360.0;
	return target;
}

// Approaches an angle at a given speed
function ApproachAngle(target, value, speed)
{
	target = NormalizeAngle(target);
	value = NormalizeAngle(value);
	local delta = NormalizeAngle(target - value);
	if (delta > speed)
		return value + speed;
	else if (delta < -speed)
		return value - speed;
	return value;
}

// Converts a vector direction into angles
function VectorAngles(forward)
{
	local yaw, pitch;
	if ( forward.y == 0.0 && forward.x == 0.0 )
	{
		yaw = 0.0;
		if (forward.z > 0.0)
			pitch = 270.0;
		else
			pitch = 90.0;
	}
	else
	{
		yaw = (atan2(forward.y, forward.x) * 180.0 / Constants.Math.Pi);
		if (yaw < 0.0)
			yaw += 360.0;
		pitch = (atan2(-forward.z, forward.Length2D()) * 180.0 / Constants.Math.Pi);
		if (pitch < 0.0)
			pitch += 360.0;
	}

	return QAngle(pitch, yaw, 0.0);
}

// Coordinate which is part of a path
class PathPoint
{
	constructor(_area, _pos, _how)
	{
		area = _area;
		pos = _pos;
		how = _how;
	}

	area = null;	// Which area does this point belong to?
	pos = null;		// Coordinates of the point
	how = null;		// Type of traversal. See Constants.ENavTraverseType
}

// The big boy that handles all our behavior
class Bot
{
	function constructor(bot_ent, follow_ent)
	{
		bot = bot_ent;

		move_speed = 230.0;
		turn_rate = 5.0;
		search_dist_z = 128.0;
		search_dist_nearest = 128.0;

		path = [];
		path_index = 0;
		path_reach_dist = 16.0;
		path_follow_ent = follow_ent;
		path_follow_ent_dist = 50.0;
		path_target_pos = follow_ent.GetOrigin();
		path_update_time_next = Time();
		path_update_time_delay = 0.2;
		path_update_force = true;
		area_list = {};

		seq_idle = bot_ent.LookupSequence("Stand_MELEE");
		seq_run = bot_ent.LookupSequence("Run_MELEE");
		pose_move_x = bot_ent.LookupPoseParameter("move_x");

		debug = true;

		// Add behavior that will run every tick
		AddThinkToEnt(bot_ent, "BotThink");
	}

	function UpdatePath()
	{
		// Clear out the path first
		ResetPath();

		// If there is a follow entity specified, then the bot will pathfind to the entity
		if (path_follow_ent && path_follow_ent.IsValid())
			path_target_pos = path_follow_ent.GetOrigin();

		// Pathfind from the bot's position to the target position
		local pos_start = bot.GetOrigin();
		local pos_end = path_target_pos;

		local area_start = NavMesh.GetNavArea(pos_start, search_dist_z);
		local area_end = NavMesh.GetNavArea(pos_end, search_dist_z);

		// If either area was not found, try use the closest one
		if (area_start == null)
			area_start = NavMesh.GetNearestNavArea(pos_start, search_dist_nearest, false, true);
		if (area_end == null)
			area_end = NavMesh.GetNearestNavArea(pos_end, search_dist_nearest, false, true);

		// If either area is still missing, then bot can't progress
		if (area_start == null || area_end == null)
			return false;

		// If the start and end area is the same, one path point is enough and all the expensive path building can be skipped
		if (area_start == area_end)
		{
			path.append(PathPoint(area_end, pos_end, Constants.ENavTraverseType.NUM_TRAVERSE_TYPES));
			return true;
		}

		// Build list of areas required to get from the start to the end
		if (!NavMesh.GetNavAreasFromBuildPath(area_start, area_end, pos_end, 0.0, Constants.ETFTeam.TEAM_ANY, false, area_list))
			return false;

		// No areas found? Uh oh
		if (area_list.len() == 0)
			return false;

		// Now build points using the list of areas, which the bot will then follow
		local area_target = area_list["area0"];
		local area = area_target;
		local area_count = area_list.len();

		// Iterate through the list of areas in order and initialize points
		for (local i = 0; i < area_count && area != null; i++)
		{
			path.append(PathPoint(area, area.GetCenter(), area.GetParentHow()));
			area = area.GetParent(); // Advances to the next connected area
		}

		// Reverse the list of path points as the area list is connected backwards
		path.reverse();

		// Now compute accurate path points, using adjacent points + direction data from nav
		local path_first = path[0];
		local path_count = path.len();

		// First point is simply our current position
		path_first.pos = bot.GetOrigin();
		path_first.how = Constants.ENavTraverseType.NUM_TRAVERSE_TYPES; // No direction specified

		for (local i = 1; i < path_count; i++)
		{
			local path_from = path[i - 1];
			local path_to = path[i];

			// Computes closest point within the "portal" between adjacent areas
			path_to.pos = path_from.area.ComputeClosestPointInPortal(path_to.area, path_to.how, path_from.pos);
		}

		// Add a final point so the bot can precisely move towards the end point when it reaches the final area
		path.append(PathPoint(area_end, pos_end, Constants.ENavTraverseType.NUM_TRAVERSE_TYPES));
	}

	function AdvancePath()
	{
		// Check for valid path first
		local path_len = path.len();
		if (path_len == 0)
			return false;

		local path_pos = path[path_index].pos;
		local bot_pos = bot.GetOrigin();

		// Are we close enough to the path point to consider it as 'reached'?
		if ((path_pos - bot_pos).Length2D() < path_reach_dist)
		{
			// Start moving to the next point
			path_index++;
			if (path_index >= path_len)
			{
				// End of the line!
				ResetPath();
				return false;
			}
		}

		return true;
	}

	function ResetPath()
	{
		area_list.clear();
		path.clear();
		path_index = 0;
	}

	function Move()
	{
		// Recompute the path if forced to do so
		if (path_update_force)
		{
			UpdatePath();
			path_update_force = false;
		}
		// Recompute path to our target if present
		else if (path_follow_ent && path_follow_ent.IsValid())
		{
			// Is it time to re-compute the path?
			local time = Time();
			if (path_update_time_next < time)
			{
				// Check if target has moved far away enough
				if ((path_target_pos - path_follow_ent.GetOrigin()).Length() > path_follow_ent_dist)
				{
					UpdatePath();
					// Don't recompute again for a moment
					path_update_time_next = time + path_update_time_delay;
				}
			}
		}

		// Check and advance up our path
		if (AdvancePath())
		{
			local path_pos = path[path_index].pos;
			local bot_pos = bot.GetOrigin();

			// Direction towards path point
			local move_dir = (path_pos - bot_pos);
			move_dir.Norm();

			// Convert direction into angle form
			local move_ang = VectorAngles(move_dir);

			// Approach new desired angle but only on the Y axis
			local bot_ang = bot.GetAbsAngles()
			move_ang.x = bot_ang.x;
			move_ang.y = ApproachAngle(move_ang.y, bot_ang.y, turn_rate);
			move_ang.z = bot_ang.z;

			// Set our new position and angles
			// Velocity is calculated from direction times speed, and converted from per-second to per-tick time
			bot.SetAbsOrigin(bot_pos + (move_dir * move_speed * FrameTime()));
			bot.SetAbsAngles(move_ang);

			return true;
		}

		return false;
	}

	function Update()
	{
		// Try moving
		if (Move())
		{
			// Moving, set the run animation
			if (bot.GetSequence() != seq_run)
			{
				bot.SetSequence(seq_run);
				bot.SetPoseParameter(pose_move_x, 1.0); // Set the move_x pose to max weight
			}
		}
		else
		{
			// Not moving, set the idle animation
			if (bot.GetSequence() != seq_idle)
			{
				bot.SetSequence(seq_idle);
				bot.SetPoseParameter(pose_move_x, 0.0); // Clear the move_x pose
			}
		}

		// Replay animation if it has finished
		if (bot.GetCycle() > 0.99)
			bot.SetCycle(0.0);

		// Run animations
		bot.StudioFrameAdvance();
		bot.DispatchAnimEvents(bot);

		// Visualize current path in debug mode
		if (debug)
		{
			// Stay around for 1 tick
			// Debugoverlays are created on 1st tick but start rendering on 2nd tick, hence this must be doubled
			local frame_time = FrameTime() * 2.0;

			// Draw connected path points
			local path_len = path.len();
			if (path_len > 0)
			{
				local path_start_index = path_index;
				if (path_start_index == 0)
					path_start_index++;

				for (local i = path_start_index; i < path_len; i++)
				{
					DebugDrawLine(path[i - 1].pos, path[i].pos, 0, 255, 0, true, frame_time);
				}
			}

			// Draw areas from built path
			foreach (name, area in area_list)
			{
				area.DebugDrawFilled(255, 0, 0, 30, frame_time, true, 0.0);
				DebugDrawText(area.GetCenter(), name, false, frame_time);
			}
		}

		return 0.0; // Think again next frame
	}

	function OnKilled()
	{
		// Change life state to "dying"
		// The bot won't take any more damage, and sentries will stop targeting it
		NetProps.SetPropInt(bot, "m_lifeState", 1);
		// Reset health, preventing the default base_boss death behavior
		bot.SetHealth(bot.GetMaxHealth() * 20);
		// Custom death behavior can be added here
		// For this example, turn into a ragdoll with the saved damage force
		bot.BecomeRagdollOnClient(damage_force);
	}

	bot = null;						// The bot entity we belong to

	move_speed = null;				// How fast to move
	turn_rate = null;				// How fast to turn
	search_dist_z = null;			// Maximum distance to look for a nav area downwards
	search_dist_nearest = null; 	// Maximum distance to look for any nearby nav area

	path = null;					// List of BotPathPoints
	path_index = null;				// Current path point bot is at, -1 if none
	path_reach_dist = null;			// Distance to a path point to be considered as 'reached'
	path_follow_ent = null;			// What entity to move towards
	path_follow_ent_dist = null;	// Maximum distance after which the path is recomputed
									// if follow entity's current position is too far from our target position
	path_target_pos = null;			// Position where bot wants to navigate to
	path_update_time_next = null;	// Timer for when to update path again
	path_update_time_delay = null;  // Seconds to wait before trying to attempt to update path again
	path_update_force = null;		// Force path recomputation on the next tick
	area_list = null;				// List of areas built in path

	seq_idle = null;				// Animation to use when idle
	seq_run = null;					// Animation to use when running
	pose_move_x = null;				// Pose parameter to set for running animation

	damage_force = null;			// Damage force from the bot's last OnTakeDamage event

	debug = null;					// When true, debug visualization is enabled

}

function BotThink()
{
	// Let the bot class handle all the work
	return self.GetScriptScope().my_bot.Update();
}

function BotCreate()
{
	// Find point where player is looking
	local player = GetListenServerHost();
	local trace =
	{
		start = player.EyePosition(),
		end = player.EyePosition() + (player.EyeAngles().Forward() * 32768.0),
		ignore = player
	};

	if (!TraceLineEx(trace))
	{
		printl("Invalid bot spawn location");
		return null;
	}

	// Spawn bot at the end point
	local bot = SpawnEntityFromTable("base_boss",
	{
		targetname = "bot",
		origin = trace.pos,
		model = "models/bots/heavy/bot_heavy.mdl",
		playbackrate = 1.0, // Required for animations to be simulated
		health = 300
	});

	// Add scope to the entity
	bot.ValidateScriptScope();
	// Append custom bot class and initialize its behavior
	bot.GetScriptScope().my_bot <- Bot(bot, player);

	return bot;
}

function OnScriptHook_OnTakeDamage(params)
{
	local ent = params.const_entity;
	local inf = params.inflictor;
    if (ent.IsPlayer() && HasBotScript(inf) && params.damage_type == 1)
    {
		// Don't crush the player if a bot pushes them into a wall
        params.damage = 0;
    }
	if (ent.GetClassname() == "base_boss" && HasBotScript(ent))
	{
		// Save the damage force into the bot's data
		ent.GetScriptScope().my_bot.damage_force = params.damage_force;
	}
}

function OnGameEvent_npc_hurt(params)
{
	local ent = EntIndexToHScript(params.entindex);
	if (HasBotScript(ent))
	{
		// Check if a bot is about to die
		if ((ent.GetHealth() - params.damageamount) <= 0)
		{
			// Run the bot's OnKilled function
			ent.GetScriptScope().my_bot.OnKilled();
		}
	}
}

function HasBotScript(ent)
{
	// Return true if this entity has the my_bot script scope
	return (ent.GetScriptScope() != null && ent.GetScriptScope().my_bot != null);
}

__CollectGameEventCallbacks(this)
BotCreate();

Смотрите также