Добавлен , опубликован
Раздел:
Триггеры и объекты

Идея

Создать способность, которая запустит снаряд из точки А в точку B.

Подготовка

Для начала нам понадобится последняя версия WarCraft III, умение читать справочник и пользоваться английской раскладкой клавиатуры.
Писать код в стандартном редакторе то ещё удовольствие, потому настраиваем себе подсветку во внешнем.
По умолчанию карты компилируются в jass, переключим их в режим lua.
Как и всякие культурные картоделы, способность будем делать на основе ANcl Канал, настройки которого прекрасно описаны здесь.
Осталось только подготовить карту для тестирования и можно приступать к написанию кода.

Двигаем снаряд

Сперва наперво, удаляем всё из стандартных триггеров и создаём блок кода, в котором и будем работать.
Наш код не должен конфликтовать с другим кодом в карте и запускаться после инициализации карты, поэтому применим следущую конструкцию.
do -- создаём область видимости, чтоб не конфликтовать с другим кодом
	local InitGlobalsOrigin = InitGlobals -- хукаем функцию InitGlobals
	function InitGlobals()
		InitGlobalsOrigin()
		-- в этом моменте прошла инициализация карты и можно смело работать
	end
end
Более подробно можете прочитать здесь.
Наконец-то настала пора погладить манула открыть справочник и почитать в нём про переменные и циклы. Вооружившись этой бесценной информацией создадим триггер и для каждого игрока добавим событие для отлова каста.
local SpellEffectTrigger = CreateTrigger()
for i = 0, bj_MAX_PLAYER_SLOTS - 1 do
	TriggerRegisterPlayerUnitEvent(SpellEffectTrigger, Player(i), EVENT_PLAYER_UNIT_SPELL_EFFECT)
end
Подробней про события
Есть пять событий, которые срабатывают при касте в таком порядке:
  • EVENT_PLAYER_UNIT_SPELL_CHANNEL
  • EVENT_PLAYER_UNIT_SPELL_CAST
  • EVENT_PLAYER_UNIT_SPELL_EFFECT
  • EVENT_PLAYER_UNIT_SPELL_ENDCAST
  • EVENT_PLAYER_UNIT_SPELL_FINISH
Порядок срабатывания можно проверить нехитрым способом
local SpellTrigger = CreateTrigger()
TriggerRegisterPlayerUnitEvent(SpellTrigger, GetLocalPlayer(), EVENT_PLAYER_UNIT_SPELL_EFFECT)
TriggerAddAction(SpellTrigger, function()
	local ID = GetHandleId(GetTriggerEventId())
	print('-----------------------')
	print('channel', ID == GetHandleId(EVENT_PLAYER_UNIT_SPELL_CHANNEL))
	print('cast', ID == GetHandleId(EVENT_PLAYER_UNIT_SPELL_CAST))
	print('effect', ID == GetHandleId(EVENT_PLAYER_UNIT_SPELL_EFFECT))
	print('endcast', ID == GetHandleId(EVENT_PLAYER_UNIT_SPELL_ENDCAST))
	print('finish', ID == GetHandleId(EVENT_PLAYER_UNIT_SPELL_FINISH))
end)
PT153
EFFECT означает фактический старт каста способности, когда cast time способности и cast point юнита прошли. Во время этого события игрок уже может юнита контролировать, до этого - нет. Также если до этого события сбить каст, то юнит заново начнёт кастовать, после и во время этого события - нет. ENDCAST не может сработать раньше EFFECT, либо во время, либо после.
Вновь открыв справочник на странице с анонимными функциями добавим триггеру действие и сделаем так, чтоб он срабатывал только при касте нашего заклинания.
TriggerAddAction(SpellEffectTrigger, function()
	if GetSpellAbilityId() ~= FourCC('Amis') then return end
end)
Почему я не использую условие
Большое количество триггеров перегружает карту и для каста я использую всего один триггер подобным образом:
if GetSpellAbilityId() == FourCC('A001') then
	-- spell 1
elseif GetSpellAbilityId() == FourCC('A002') then
	-- spell 2
elseif GetSpellAbilityId() == FourCC('A003') then
	-- spell 3
end
Притом условие является лишней сущностью от которых один старик с бритвой просил избавляться.
Осознав тот факт, что точки у нас фиксированные и почитав про полярную систему координат и такую единицу измерения угла как радиан объявим нужные для работы переменные.
local caster = GetTriggerUnit() -- юнит, применивший способность
local xa, ya = GetUnitX(caster), GetUnitY(caster) -- координаты точки начала полёта
local xb, yb = GetSpellTargetX(), GetSpellTargetY() -- координаты точки окончания полёта
local angle    = math.atan(yb - ya, xb - xa) -- угол между точками в радианах
local cos, sin = math.cos(angle), math.sin(angle) -- косинус и синус этого угла
local distance= math.sqrt((xb - xa) * (xb - xa) + (yb - ya) * (yb - ya)) -- расстояние между точками
После таких непосильных трудов, можно наконец-то создать эффект и полюбоваться на плоды делов своих.
do
	-- создаём область видимости, чтоб не конфликтовать с другим кодом
	local InitGlobalsOrigin = InitGlobals -- хукаем функцию InitGlobals
	function InitGlobals()
		InitGlobalsOrigin()
		-- в этом моменте прошла инициализация карты и можно смело работать
		local SpellEffectTrigger = CreateTrigger()
		for i = 0, bj_MAX_PLAYER_SLOTS - 1 do
			TriggerRegisterPlayerUnitEvent(SpellEffectTrigger, Player(i), EVENT_PLAYER_UNIT_SPELL_EFFECT)
		end
		TriggerAddAction(SpellEffectTrigger, function()
			if GetSpellAbilityId() ~= FourCC('Amis') then return end
			local caster   = GetTriggerUnit() -- юнит, применивший способность
			local xa, ya   = GetUnitX(caster), GetUnitY(caster) -- координаты точки начала полёта
			local xb, yb   = GetSpellTargetX(), GetSpellTargetY() -- координаты точки окончания полёта
			local angle    = math.atan(yb - ya, xb - xa) -- угол между точками в радианах
			local cos, sin = math.cos(angle), math.sin(angle) -- косинус и синус этого угла
			local distance = math.sqrt((xb - xa) * (xb - xa) + (yb - ya) * (yb - ya)) -- расстояние между точками
			
			local missile  = AddSpecialEffect('Abilities/Weapons/Mortar/MortarMissile.mdl', xa, ya) -- создаём эффект снаряда
			BlzSetSpecialEffectYaw(missile, angle) -- поворачиваем эффект на нужный угол
		end)
	end
end
Всё прекрасно работает, но как вы догадались, что-бы снаряд двигался - его нужно двигать. Но для начала объявим ещё больше переменных и вынесем их повыше, для удобной настройки.
local ABILITY_ID         = FourCC('Amis')
local MISSILE_EFFECT     = 'Abilities/Weapons/Mortar/MortarMissile.mdl'
local TIMER_PERIOD       = 0.03125 -- Период срабатывания таймера: 1/32 секунды
local SPEED              = 1200 -- Расстояние, которое снаряд преодолеет за секунду
local SPEED_INC          = SPEED * TIMER_PERIOD -- расстояние, которое снаряд пройдёт на каждый тик таймера
Когда все данные есть на руках, наконец-то можно приступать к движению снаряда используя старые добрые таймеры.
TimerStart(CreateTimer(), TIMER_PERIOD, true, function()
	BlzSetSpecialEffectX(missile, BlzGetLocalSpecialEffectX(missile) + cos * SPEED_INC)
	BlzSetSpecialEffectY(missile, BlzGetLocalSpecialEffectY(missile) + sin * SPEED_INC)
end)
Можно себя поздравить с первыми успехами, но включив аналитический ум можно заметить, что снаряд летит сквозь рельеф, продолжает лететь при достижении точки каста и долетая до края карты крашит игру. Благо исправить то не составит труда.
Пробему с вылетом за границы карты решает простая функция:
---@param x real
---@param y real
---@return boolean
local function InMapXY(x, y)
	return x > GetRectMinX(bj_mapInitialPlayableArea) and x < GetRectMaxX(bj_mapInitialPlayableArea) and y > GetRectMinY(bj_mapInitialPlayableArea) and y < GetRectMaxY(bj_mapInitialPlayableArea)
end
Проблему с расстоянием можно решить просто посчитав пройденный путь. В итоге немного переделаем таймер.
TimerStart(CreateTimer(), TIMER_PERIOD, true, function()
	distance   = distance - SPEED_INC -- отнимаем от расстояния пройденный путь
	local x, y = BlzGetLocalSpecialEffectX(missile) + cos * SPEED_INC, BlzGetLocalSpecialEffectY(missile) + sin * SPEED_INC -- считаем новое положение снаряда
	
	if
			distance <= 0 -- если расстояние равно 0, значит снаряд уже долетел
			or
			not InMapXY(x, y) --снаряд вышел за пределы карты
	then
		DestroyEffect(missile)
		PauseTimer(GetExpiredTimer()) -- останавливаем таймер перед уничтожением
		DestroyTimer(GetExpiredTimer()) -- уничтожаем таймер
		return -- завершаем функцию, чтоб пропустить дальнейшие действия
	end
	BlzSetSpecialEffectX(missile, x)
	BlzSetSpecialEffectY(missile, y)
end)
Проблема с рельефом решается тремя способами
  • заставить снаряд катиться по рельефу
  • запускать по параболе и уничтожать при столкновении с рельефом
  • забить
В статье мы рассмотрим только первые два, третий способ я оставлю вам в качестве домашнего задания.
Для определения высоты рельефа нам пригодится ещё одна хорошая функция:
local GetTerrainZ_location = Location(0, 0)
---@param x real
---@param y real
---@return real
local  function GetTerrainZ(x, y)
	MoveLocation(GetTerrainZ_location, x, y)
	return GetLocationZ(GetTerrainZ_location)
end
Которая сразу решает проблему с передвижением по рельефу.
BlzSetSpecialEffectHeight(missile, GetTerrainZ(x, y))
Движение по параболе мне кажется красивее, для чего нам пригодится функция:
---@param zs real начальная высота высота одного края дуги
---@param ze real конечная высота высота другого края дуги
---@param h real максимальная высота на середине расстояния (x = d / 2)
---@param d real общее расстояние до цели
---@param x real расстояние от исходной цели до точки
---@return real
function GetParabolaZ(zs, ze, h, d, x)
	return (2 * (zs + ze - 2 * h) * (x / d - 1) + (ze - zs)) * (x / d) + zs
end
Теперь можно вспомнить решение прямоугольного треугольника, оптимизировать код и наслаждаться.
do
	-- создаём область видимости, чтоб не конфликтовать с другим кодом
	local InitGlobalsOrigin = InitGlobals -- хукаем функцию InitGlobals
	function InitGlobals()
		InitGlobalsOrigin()
		
		---@param x real
		---@param y real
		---@return boolean
		local function InMapXY(x, y)
			return x > GetRectMinX(bj_mapInitialPlayableArea) and x < GetRectMaxX(bj_mapInitialPlayableArea) and y > GetRectMinY(bj_mapInitialPlayableArea) and y < GetRectMaxY(bj_mapInitialPlayableArea)
		end
		
		local GetTerrainZ_location = Location(0, 0)
		---@param x real
		---@param y real
		---@return real
		local function GetTerrainZ(x, y)
			MoveLocation(GetTerrainZ_location, x, y)
			return GetLocationZ(GetTerrainZ_location)
		end
		
		---@param za real начальная высота высота одного края дуги
		---@param zb real конечная высота высота другого края дуги
		---@param h real максимальная высота на середине расстояния (x = d / 2)
		---@param d real общее расстояние до цели
		---@param x real расстояние от исходной цели до точки
		---@return real
		local function GetParabolaZ(za, zb, h, d, x)
			return (2 * (za + zb - 2 * h) * (x / d - 1) + (zb - za)) * (x / d) + za
		end
		
		local ABILITY_ID         = FourCC('Amis')
		local MISSILE_EFFECT     = 'Abilities/Weapons/Mortar/MortarMissile.mdl'
		local MISSILE_ARC        = 0.5 -- изгиб дуги полёта снаряда
		local TIMER_PERIOD       = 0.03125 -- период срабатывания таймера: 1/32 секунды
		local SPEED              = 200 -- расстояние, которое снаряд преодолеет за секунду
		local SPEED_INC          = SPEED * TIMER_PERIOD -- расстояние, которое снаряд пройдёт на каждый тик таймера
		
		-- в этом моменте прошла инициализация карты и можно смело работать
		local SpellEffectTrigger = CreateTrigger()
		for i = 0, bj_MAX_PLAYER_SLOTS - 1 do
			TriggerRegisterPlayerUnitEvent(SpellEffectTrigger, Player(i), EVENT_PLAYER_UNIT_SPELL_EFFECT)
		end
		TriggerAddAction(SpellEffectTrigger, function()
			if GetSpellAbilityId() ~= ABILITY_ID then return end
			local caster   = GetTriggerUnit() -- юнит, применивший способность
			local xa, ya   = GetUnitX(caster), GetUnitY(caster) -- координаты точки начала полёта
			local za       = GetTerrainZ(xa, ya) -- высота точки начала полёта
			local xb, yb   = GetSpellTargetX(), GetSpellTargetY() -- координаты точки окончания полёта
			local zb       = GetTerrainZ(xb, yb) -- высота точки окончания полёта
			local angle    = math.atan(yb - ya, xb - xa) -- угол между точками в радианах
			local cos, sin = math.cos(angle), math.sin(angle) -- косинус и синус этого угла
			local distance = math.sqrt((xb - xa) * (xb - xa) + (yb - ya) * (yb - ya)) -- расстояние между точками
			local way      = 0 -- Пройденный путь
			
			local missile  = AddSpecialEffect(MISSILE_EFFECT, xa, ya) -- создаём эффект снаряда
			
			local x, y, z  = xa, ya, za -- начальное положение эффекта
			TimerStart(CreateTimer(), TIMER_PERIOD, true, function()
				way  = way + SPEED_INC -- считаем пройденный путь
				x, y = x + cos * SPEED_INC, y + sin * SPEED_INC -- считаем новое положение снаряда
				
				if
						way >= distance -- если расстояние равно 0, значит снаряд уже долетел
						or
						not InMapXY(x, y) --снаряд вышел за пределы карты
				then
					BlzSetSpecialEffectOrientation(missile, angle, 0, 0) -- устанавливаем финальное положение эффекта
					DestroyEffect(missile) -- уничтожаем эффект
					
					PauseTimer(GetExpiredTimer()) -- останавливаем таймер перед уничтожением
					DestroyTimer(GetExpiredTimer()) -- уничтожаем таймер
					return -- завершаем функцию, чтоб пропустить дальнейшие действия
				end
				
				BlzSetSpecialEffectX(missile, x) -- устанавливаем положение эффекта
				BlzSetSpecialEffectY(missile, y) -- устанавливаем положение эффекта
				local zNew  = GetParabolaZ(za, zb, distance * MISSILE_ARC, distance, way) -- считаем новую высоту эффекта
				local zDiff = zNew - z -- считаем разницу высот
				BlzSetSpecialEffectZ(missile, zNew) -- устанавливаем новую высоту эффекта
				local zAngle = zDiff > 0 and math.atan(SPEED_INC / zDiff) - math.pi / 2 or math.atan(math.abs(zDiff) / SPEED_INC) - math.pi * 2 -- считаем угол наклона снаряда
				BlzSetSpecialEffectOrientation(missile, angle, zAngle, 0) -- устанавливаем направление эффекта
				z = zNew -- запоминаем новую высоту эффекта
			end)
		end)
	end
end

Наносим урон

В нашем случае алгоритм нанесения урона прост: после взрыва снаряда выбрать всех юнитов в радиусе и нанести им необходимое количество урона. Поэтому для начала обзаведёмся группой, чтоб не создавать её каждый раз заново.
local GROUP = CreateGroup()
Так как у нас видимый круг выбора радиуса, то желательно в коде сразу получить значение из редактора:
Сделать это нам поможет функция
BlzGetAbilityRealLevelField()
Игра нам даёт возможность получать/изменять некоторые поля у конкретных способностей юнитов не трогая при этом такие же способности у других.
Так как нужное поле имеет тип real и уникально для каждого уровня, то название можно даже угадать: BlzGetAbilityRealLevelField.
Нужное поле угадать не так просто, но можно сильно упростить себе задачу: нажимаем Ctrl+D в редакторе и смотрим на название поля:
Теперь можно просто в IDE набрать ABILITY_RLF_AREA (RLF это какраз аббревиатура от Real Level Field)
Когда с функцией и полем определились, можно и разобрать другие параметры:
---@param whichAbility ability
---@param whichField abilityreallevelfield
---@param level integer
---@return real
function BlzGetAbilityRealLevelField(whichAbility, whichField, level) end
  • whichAbility
Это способность, поле которой на мнужно. Получаем её с помощью
---@param whichUnit unit
---@param abilId integer
---@return ability
function BlzGetUnitAbility(whichUnit, abilId) end
  • whichField
Поле способности, которое мы уже узнали ABILITY_RLF_AREA_OF_EFFECT
  • level
Уровень способности. Только будте внимательны, нумерация вагонов уровней начинается с нуля.
---@param whichUnit unit
---@param abilcode integer
---@return integer
function GetUnitAbilityLevel(whichUnit, abilcode) end
local ability      = BlzGetUnitAbility(caster, ABILITY_ID)
local abilityLevel = GetUnitAbilityLevel(caster, ABILITY_ID)
local range        = BlzGetAbilityRealLevelField(ability, ABILITY_RLF_AREA_OF_EFFECT, abilityLevel - 1)
Выбрав из циклов самый красивый подходящий, можно провести маленький тест.
GroupEnumUnitsInRange(GROUP, x, y, range)
while true do
	local target = FirstOfGroup(GROUP)
	if target == nil then break end
	GroupRemoveUnit(GROUP, target)
	KillUnit(target)
end
Работает не совсем так, как нам бы хотелось. На это есть свои причины: мы не делаем никаких проверок и GroupEnumUnitsInRange проверяет координаты юнита, а не физический размер. Но мы схитрим: добавим в группу юнитов на большем расстоянии:
GroupEnumUnitsInRange(GROUP, x, y, range + 256)
А при переборе уже воспользуемся функцией, которая учитывает размер юнита.
---@param whichUnit unit
---@param x real
---@param y real
---@param distance real
---@return boolean
function IsUnitInRangeXY(whichUnit, x, y, distance) end
Так как тесты лишними не бывают, проведём ещё один.
GroupEnumUnitsInRange(GROUP, x, y, range + 256)
while true do
	local target = FirstOfGroup(GROUP)
	if target == nil then break end
	GroupRemoveUnit(GROUP, target)
	if UnitAlive(target) -- юнит жив
			and IsPlayerEnemy(GetOwningPlayer(caster), GetOwningPlayer(target)) -- юнит враг
			and not IsUnitType(target, UNIT_TYPE_MAGIC_IMMUNE) -- юнит не имунен к маггии
			and not IsUnitType(target, UNIT_TYPE_FLYING) -- юнит не летающий
			and IsUnitInRangeXY(target, x, y, range) -- юнит на нужном расстоянии
	then
		KillUnit(target)
	end
end
Теперь настало время наносить урон, а так как просто указать значение для каждого уровня способности удел начинающих гуишников, мы придумаем хитрую формулу, чтоб заклинание казалось умным и проработанным. Например:
уровень способности * меньшую из характеристику героя + макимальный урон героя
local damage       = abilityLevel
		* math.min(GetHeroStr(caster, true), GetHeroAgi(caster, true), GetHeroInt(caster, true))
		+ BlzGetUnitBaseDamage(caster, 0) + BlzGetUnitDiceNumber(caster, 0) * BlzGetUnitDiceSides(caster, 0)
Когда значение посчитано, можно смело наносить урон
UnitDamageTarget(caster, target, damage, false, true, ATTACK_TYPE_MAGIC, DAMAGE_TYPE_NORMAL, WEAPON_TYPE_WHOKNOWS)
Полюбуемся на весь код целиком:
do
	-- создаём область видимости, чтоб не конфликтовать с другим кодом
	local InitGlobalsOrigin = InitGlobals -- хукаем функцию InitGlobals
	function InitGlobals()
		InitGlobalsOrigin()
		-- в этом моменте прошла инициализация карты и можно смело работать
		
		---@param x real
		---@param y real
		---@return boolean
		local function InMapXY(x, y)
			return x > GetRectMinX(bj_mapInitialPlayableArea) and x < GetRectMaxX(bj_mapInitialPlayableArea) and y > GetRectMinY(bj_mapInitialPlayableArea) and y < GetRectMaxY(bj_mapInitialPlayableArea)
		end
		
		local GetTerrainZ_location = Location(0, 0)
		---@param x real
		---@param y real
		---@return real
		local function GetTerrainZ(x, y)
			MoveLocation(GetTerrainZ_location, x, y)
			return GetLocationZ(GetTerrainZ_location)
		end
		
		---@param za real начальная высота высота одного края дуги
		---@param zb real конечная высота высота другого края дуги
		---@param h real максимальная высота на середине расстояния (x = d / 2)
		---@param d real общее расстояние до цели
		---@param x real расстояние от исходной цели до точки
		---@return real
		local function GetParabolaZ(za, zb, h, d, x)
			return (2 * (za + zb - 2 * h) * (x / d - 1) + zb - za) * x / d + za
		end
		
		local ABILITY_ID         = FourCC('Amis')
		local MISSILE_EFFECT     = 'Abilities/Weapons/Mortar/MortarMissile.mdl'
		local MISSILE_ARC        = 0.5 -- изгиб дуги полёта снаряда
		local TIMER_PERIOD       = 0.03125 -- период срабатывания таймера: 1/32 секунды
		local SPEED              = 200 -- расстояние, которое снаряд преодолеет за секунду
		local SPEED_INC          = SPEED * TIMER_PERIOD -- расстояние, которое снаряд пройдёт на каждый тик таймера
		local GROUP              = CreateGroup()
		
		local SpellEffectTrigger = CreateTrigger()
		for i = 0, bj_MAX_PLAYER_SLOTS - 1 do
			TriggerRegisterPlayerUnitEvent(SpellEffectTrigger, Player(i), EVENT_PLAYER_UNIT_SPELL_EFFECT)
		end
		TriggerAddAction(SpellEffectTrigger, function()
			--print(collectgarbage("count") * 1024)
			--collectgarbage()
			if GetSpellAbilityId() ~= ABILITY_ID then return end
			local caster       = GetTriggerUnit() -- юнит, применивший способность
			local xa, ya       = GetUnitX(caster), GetUnitY(caster) -- координаты точки начала полёта
			local za           = GetTerrainZ(xa, ya) -- высота точки начала полёта
			local xb, yb       = GetSpellTargetX(), GetSpellTargetY() -- координаты точки окончания полёта
			local zb           = GetTerrainZ(xb, yb) -- высота точки окончания полёта
			local angle        = math.atan(yb - ya, xb - xa) -- угол между точками в радианах
			local cos, sin     = math.cos(angle), math.sin(angle) -- косинус и синус этого угла
			local distance     = math.sqrt((xb - xa) * (xb - xa) + (yb - ya) * (yb - ya)) -- расстояние между точками
			local ability      = BlzGetUnitAbility(caster, ABILITY_ID)
			local abilityLevel = GetUnitAbilityLevel(caster, ABILITY_ID)
			local range        = BlzGetAbilityRealLevelField(ability, ABILITY_RLF_AREA_OF_EFFECT, abilityLevel - 1)
			local damage       = abilityLevel
					* math.min(GetHeroStr(caster, true), GetHeroAgi(caster, true), GetHeroInt(caster, true))
					+ BlzGetUnitBaseDamage(caster, 0) + BlzGetUnitDiceNumber(caster, 0) * BlzGetUnitDiceSides(caster, 0)
			
			local way          = 0 -- Пройденный путь
			
			local missile      = AddSpecialEffect(MISSILE_EFFECT, xa, ya) -- создаём эффект снаряда
			
			local x, y, z      = xa, ya, za -- начальное положение эффекта
			TimerStart(CreateTimer(), TIMER_PERIOD, true, function()
				way  = way + SPEED_INC -- считаем пройденный путь
				x, y = x + cos * SPEED_INC, y + sin * SPEED_INC -- считаем новое положение снаряда
				
				if
						way >= distance -- если расстояние равно 0, значит снаряд уже долетел
						or
						not InMapXY(x, y) --снаряд вышел за пределы карты
				then
					BlzSetSpecialEffectOrientation(missile, angle, 0, 0) -- устанавливаем финальное положение эффекта
					DestroyEffect(missile) -- уничтожаем эффект
					
					GroupEnumUnitsInRange(GROUP, x, y, range + 256)
					while true do
						local target = FirstOfGroup(GROUP)
						if target == nil then break end
						GroupRemoveUnit(GROUP, target)
						if UnitAlive(target) -- юнит жив
								and IsPlayerEnemy(GetOwningPlayer(caster), GetOwningPlayer(target)) -- юнит враг
								and not IsUnitType(target, UNIT_TYPE_MAGIC_IMMUNE) -- юнит не имунен к маггии
								and not IsUnitType(target, UNIT_TYPE_FLYING) -- юнит не летающий
								and IsUnitInRangeXY(target, x, y, range) -- юнит на нужном расстоянии
						then
							UnitDamageTarget(caster, target, damage, false, true, ATTACK_TYPE_MAGIC, DAMAGE_TYPE_NORMAL, WEAPON_TYPE_WHOKNOWS)
						end
					end
					
					PauseTimer(GetExpiredTimer()) -- останавливаем таймер перед уничтожением
					DestroyTimer(GetExpiredTimer()) -- уничтожаем таймер
					return -- завершаем функцию, чтоб пропустить дальнейшие действия
				end
				
				BlzSetSpecialEffectX(missile, x) -- устанавливаем положение эффекта
				BlzSetSpecialEffectY(missile, y) -- устанавливаем положение эффекта
				local zNew  = GetParabolaZ(za, zb, distance * MISSILE_ARC, distance, way) -- считаем новую высоту эффекта
				local zDiff = zNew - z -- считаем разницу высот
				BlzSetSpecialEffectZ(missile, zNew) -- устанавливаем новую высоту эффекта
				local zAngle = zDiff > 0 and math.atan(SPEED_INC / zDiff) - math.pi / 2 or math.atan(-zDiff / SPEED_INC) - math.pi * 2 -- считаем угол наклона снаряда
				BlzSetSpecialEffectOrientation(missile, angle, zAngle, 0) -- устанавливаем направление эффекта
				z = zNew -- запоминаем новую высоту эффекта
			end)
		end)
	end
end

Наводим красоту

В принципе, заклинание уже можно использовать, но оно ничем не выделяется среди прочих и просто скучное. Для сравнения посмотрим на стандартное заклинание у Механика.
Обратим внимание на некоторые вещи:
  • выпускается несколько ракет
  • ракета создаётся не под ногами героя
  • нет минимальной дистанции каста и при касте под себя выглядит некрасиво
  • ракеты оглушают врага
Для начала приведём в порядок стартовую позицию, заодно и заменим героя и снаряд на более подходящих.
local MISSILE_Z_START = 100 -- начальная высота снаряда
local za = GetTerrainZ(xa, ya) + MISSILE_Z_START -- высота точки начала полёта
Так как минимальную дистанцию нельзя задать в редакторе объектов то просто будем сдвигать точку каста.
if distance < MISSILE_MIN_DISTANCE then
	distance = MISSILE_MIN_DISTANCE
	xb, yb   = xb + cos * distance, yb + sin * distance
end
Теперь можно взять отсюда функцию проверки на глубокую воду и сделать различные взрывы.
---@param x real
---@param y real
---@return boolean
function IsTerrainDeepWater (x, y)
	return not IsTerrainPathable(x, y, PATHING_TYPE_FLOATABILITY) and IsTerrainPathable(x, y, PATHING_TYPE_WALKABILITY)
end
local MISSILE_EFFECT_GROUND = 'Objects/Spawnmodels/Human/FragmentationShards/FragBoomSpawn.mdl' -- взрыв на суше
local MISSILE_EFFECT_WATER  = 'Abilities/Spells/Other/CrushingWave/CrushingWaveDamage.mdl' -- взрыв на воде

DestroyEffect(AddSpecialEffect(IsTerrainDeepWater(x, y) and MISSILE_EFFECT_WATER or MISSILE_EFFECT_GROUND, x, y))
Теперь можно завернуть создание таймера в цикл и полюбоваться на результат.
do
	-- создаём область видимости, чтоб не конфликтовать с другим кодом
	local InitGlobalsOrigin = InitGlobals -- хукаем функцию InitGlobals
	function InitGlobals()
		InitGlobalsOrigin()
		-- в этом моменте прошла инициализация карты и можно смело работать
		
		---@param x real
		---@param y real
		---@return boolean
		local function InMapXY(x, y)
			return x > GetRectMinX(bj_mapInitialPlayableArea) and x < GetRectMaxX(bj_mapInitialPlayableArea) and y > GetRectMinY(bj_mapInitialPlayableArea) and y < GetRectMaxY(bj_mapInitialPlayableArea)
		end
		
		local GetTerrainZ_location = Location(0, 0)
		---@param x real
		---@param y real
		---@return real
		local function GetTerrainZ(x, y)
			MoveLocation(GetTerrainZ_location, x, y)
			return GetLocationZ(GetTerrainZ_location)
		end
		
		---@param za real начальная высота высота одного края дуги
		---@param zb real конечная высота высота другого края дуги
		---@param h real максимальная высота на середине расстояния (x = d / 2)
		---@param d real общее расстояние до цели
		---@param x real расстояние от исходной цели до точки
		---@return real
		local function GetParabolaZ(za, zb, h, d, x)
			return (2 * (za + zb - 2 * h) * (x / d - 1) + zb - za) * x / d + za
		end
		
		---@param x real
		---@param y real
		---@return boolean
		local function IsTerrainDeepWater(x, y)
			return not IsTerrainPathable(x, y, PATHING_TYPE_FLOATABILITY) and IsTerrainPathable(x, y, PATHING_TYPE_WALKABILITY)
		end
		
		local ABILITY_ID            = FourCC('Amis')
		local MISSILE_EFFECT        = 'Abilities/Weapons/RocketMissile/RocketMissile.mdl' -- модель снаряда
		local MISSILE_EFFECT_GROUND = 'Objects/Spawnmodels/Human/FragmentationShards/FragBoomSpawn.mdl' -- взрыв на суше
		local MISSILE_EFFECT_WATER  = 'Abilities/Spells/Other/CrushingWave/CrushingWaveDamage.mdl' -- взрыв на воде
		local MISSILE_COUNT         = { 5, 7, 9 } -- количество снарядов для каждого уровня
		local MISSILE_STEP          = 100
		local MISSILE_Z_START       = 100 -- начальная высота снаряда
		local MISSILE_MIN_DISTANCE  = 300 -- минимальдая дистанция каста
		local MISSILE_ARC           = 0.5 -- изгиб дуги полёта снаряда
		local TIMER_PERIOD          = 0.03125 -- период срабатывания таймера: 1/32 секунды
		local SPEED                 = 200 -- расстояние, которое снаряд преодолеет за секунду
		local SPEED_INC             = SPEED / (1 / TIMER_PERIOD) -- расстояние, которое снаряд пройдёт на каждый тик таймера
		local GROUP                 = CreateGroup()
		
		local SpellEffectTrigger    = CreateTrigger()
		for i = 0, bj_MAX_PLAYER_SLOTS - 1 do
			TriggerRegisterPlayerUnitEvent(SpellEffectTrigger, Player(i), EVENT_PLAYER_UNIT_SPELL_EFFECT)
		end
		TriggerAddAction(SpellEffectTrigger, function()
			if GetSpellAbilityId() ~= ABILITY_ID then return end
			local caster   = GetTriggerUnit() -- юнит, применивший способность
			local xa, ya   = GetUnitX(caster), GetUnitY(caster) -- координаты точки начала полёта
			local za       = GetTerrainZ(xa, ya) + MISSILE_Z_START -- высота точки начала полёта
			local xb, yb   = GetSpellTargetX(), GetSpellTargetY() -- координаты точки окончания полёта
			local distance = math.sqrt((xb - xa) * (xb - xa) + (yb - ya) * (yb - ya)) -- расстояние между точками
			local angle    = math.atan(yb - ya, xb - xa) -- угол между точками в радианах
			local cos, sin = math.cos(angle), math.sin(angle) -- косинус и синус этого угла
			if distance < MISSILE_MIN_DISTANCE then
				distance = MISSILE_MIN_DISTANCE
				xb, yb   = xb + cos * distance, yb + sin * distance
			end
			local ability      = BlzGetUnitAbility(caster, ABILITY_ID)
			local abilityLevel = GetUnitAbilityLevel(caster, ABILITY_ID)
			local range        = BlzGetAbilityRealLevelField(ability, ABILITY_RLF_AREA_OF_EFFECT, abilityLevel - 1)
			local damage       = abilityLevel
					* math.min(GetHeroStr(caster, true), GetHeroAgi(caster, true), GetHeroInt(caster, true))
					+ BlzGetUnitBaseDamage(caster, 0) + BlzGetUnitDiceNumber(caster, 0) * BlzGetUnitDiceSides(caster, 0)
			
			for i = 0, MISSILE_COUNT[abilityLevel] - 1 do
				local way             = 0 -- Пройденный путь
				local missile         = AddSpecialEffect(MISSILE_EFFECT, xa, ya) -- создаём эффект снаряда
				local distanceCurrent = i * MISSILE_STEP + distance -- расстояние между точками
				local zb              = GetTerrainZ(xa + cos * distanceCurrent, yb + sin * distanceCurrent) -- высота точки окончания полёта
				
				local x, y, z         = xa, ya, za -- начальное положение эффекта
				TimerStart(CreateTimer(), TIMER_PERIOD, true, function()
					way  = way + SPEED_INC -- считаем пройденный путь
					x, y = x + cos * SPEED_INC, y + sin * SPEED_INC -- считаем новое положение снаряда
					
					if way >= distanceCurrent -- если расстояние равно 0, значит снаряд уже долетел
							or
							not InMapXY(x, y) --снаряд вышел за пределы карты
					then
						BlzSetSpecialEffectOrientation(missile, angle, 0, 0) -- устанавливаем финальное положение эффекта
						DestroyEffect(missile) -- уничтожаем эффект
						DestroyEffect(AddSpecialEffect(IsTerrainDeepWater(x, y) and MISSILE_EFFECT_WATER or MISSILE_EFFECT_GROUND, x, y))
						
						GroupEnumUnitsInRange(GROUP, x, y, range + 256)
						while true do
							local target = FirstOfGroup(GROUP)
							if target == nil then break end
							GroupRemoveUnit(GROUP, target)
							if UnitAlive(target) -- юнит жив
									and IsPlayerEnemy(GetOwningPlayer(caster), GetOwningPlayer(target)) -- юнит враг
									and not IsUnitType(target, UNIT_TYPE_MAGIC_IMMUNE) -- юнит не имунен к маггии
									and not IsUnitType(target, UNIT_TYPE_FLYING) -- юнит не летающий
									and IsUnitInRangeXY(target, x, y, range) -- юнит на нужном расстоянии
							then
								UnitDamageTarget(caster, target, damage, false, true, ATTACK_TYPE_MAGIC, DAMAGE_TYPE_NORMAL, WEAPON_TYPE_WHOKNOWS)
							end
						end
						
						PauseTimer(GetExpiredTimer()) -- останавливаем таймер перед уничтожением
						DestroyTimer(GetExpiredTimer()) -- уничтожаем таймер
						return -- завершаем функцию, чтоб пропустить дальнейшие действия
					end
					
					BlzSetSpecialEffectX(missile, x) -- устанавливаем положение эффекта
					BlzSetSpecialEffectY(missile, y) -- устанавливаем положение эффекта
					local zNew  = GetParabolaZ(za, zb, distanceCurrent * MISSILE_ARC, distanceCurrent, way) -- считаем новую высоту эффекта
					local zDiff = zNew - z -- считаем разницу высот
					BlzSetSpecialEffectZ(missile, zNew) -- устанавливаем новую высоту эффекта
					local zAngle = zDiff > 0 and math.atan(SPEED_INC / zDiff) - math.pi / 2 or math.atan(-zDiff / SPEED_INC) - math.pi * 2 -- считаем угол наклона снаряда
					BlzSetSpecialEffectOrientation(missile, angle, zAngle, 0) -- устанавливаем направление эффекта
					z = zNew -- запоминаем новую высоту эффекта
				end)
			end
		end)
	end
end
Так как игра не позволяет просто накладывать бафы, на помощь придёт даммикаст. Которому тоже можно посвятить целую статью. Поэтому создание даммика и способности мы опустим, а сразу начнём с кода.
local Dummy       = CreateUnit(Player(PLAYER_NEUTRAL_PASSIVE), FourCC('dumy'), 0, 0, 0)
local DummyStunId = FourCC('stun')
UnitAddAbility(Dummy, DummyStunId)
local StunAbility = BlzGetUnitAbility(Dummy, DummyStunId)

---@param target unit
---@param durationUnit real
---@param durationHero real
local function DummyCastStun(target, durationUnit, durationHero)
	SetUnitX(Dummy, GetUnitX(target))
	SetUnitY(Dummy, GetUnitY(target))
	BlzSetAbilityRealLevelField(StunAbility, ABILITY_RLF_DURATION_NORMAL, 0, durationUnit)
	BlzSetAbilityRealLevelField(StunAbility, ABILITY_RLF_DURATION_HERO, 0, durationHero)
	IssueTargetOrderById(Dummy, 852095, target) -- thunderbolt
end
Подробнее
Так как у даммика нет модели, то касты способностей проходят моментально. Потому приказы даммику можно отдавать в цикле. А изменение данных способности перед кастом освобождает нас от создания кучи способностей с разным временем действия.
Поэтому сразу в начале игры можно создать даммика для нейтрально пассивного и сразу выдать ему способность, чтоб небыло лага при первом использовании.
Напоследок сделаем нелинейное изменение скорости. Так как точки у нас фиксированные, то мы можем зарание посчитать время полёта и переписать его исходя не из расстояния, а от времени.
Код
do
	-- создаём область видимости, чтоб не конфликтовать с другим кодом
	local InitGlobalsOrigin = InitGlobals -- хукаем функцию InitGlobals
	function InitGlobals()
		InitGlobalsOrigin()
		-- в этом моменте прошла инициализация карты и можно смело работать
		
		---@param x real
		---@param y real
		---@return boolean
		local function InMapXY(x, y)
			return x > GetRectMinX(bj_mapInitialPlayableArea) and x < GetRectMaxX(bj_mapInitialPlayableArea) and y > GetRectMinY(bj_mapInitialPlayableArea) and y < GetRectMaxY(bj_mapInitialPlayableArea)
		end
		
		local GetTerrainZ_location = Location(0, 0)
		---@param x real
		---@param y real
		---@return real
		local function GetTerrainZ(x, y)
			MoveLocation(GetTerrainZ_location, x, y)
			return GetLocationZ(GetTerrainZ_location)
		end
		
		---@param za real начальная высота высота одного края дуги
		---@param zb real конечная высота высота другого края дуги
		---@param h real максимальная высота на середине расстояния (x = d / 2)
		---@param d real общее расстояние до цели
		---@param x real расстояние от исходной цели до точки
		---@return real
		local function GetParabolaZ(za, zb, h, d, x)
			return (2 * (za + zb - 2 * h) * (x / d - 1) + zb - za) * x / d + za
		end
		
		---@param x real
		---@param y real
		---@return boolean
		local function IsTerrainDeepWater(x, y)
			return not IsTerrainPathable(x, y, PATHING_TYPE_FLOATABILITY) and IsTerrainPathable(x, y, PATHING_TYPE_WALKABILITY)
		end
		
		local Dummy       = CreateUnit(Player(PLAYER_NEUTRAL_PASSIVE), FourCC('dumy'), 0, 0, 0)
		local DummyStunId = FourCC('stun')
		UnitAddAbility(Dummy, DummyStunId)
		local StunAbility = BlzGetUnitAbility(Dummy, DummyStunId)
		
		---@param target unit
		---@param durationUnit real
		---@param durationHero real
		local function DummyCastStun(target, durationUnit, durationHero)
			SetUnitX(Dummy, GetUnitX(target))
			SetUnitY(Dummy, GetUnitY(target))
			BlzSetAbilityRealLevelField(StunAbility, ABILITY_RLF_DURATION_NORMAL, 0, durationUnit)
			BlzSetAbilityRealLevelField(StunAbility, ABILITY_RLF_DURATION_HERO, 0, durationHero)
			IssueTargetOrderById(Dummy, 852095, target) -- thunderbolt
		end
		
		local ABILITY_ID            = FourCC('Amis')
		local MISSILE_EFFECT        = 'Abilities/Weapons/RocketMissile/RocketMissile.mdl' -- модель снаряда
		local MISSILE_EFFECT_GROUND = 'Objects/Spawnmodels/Human/FragmentationShards/FragBoomSpawn.mdl' -- взрыв на суше
		local MISSILE_EFFECT_WATER  = 'Abilities/Spells/Other/CrushingWave/CrushingWaveDamage.mdl' -- взрыв на воде
		local MISSILE_COUNT         = { 5, 7, 9 } -- количество снарядов для каждого уровня
		local MISSILE_STEP          = 100
		local MISSILE_Z_START       = 100 -- начальная высота снаряда
		local MISSILE_MIN_DISTANCE  = 300 -- минимальдая дистанция каста
		local MISSILE_ARC           = 0.5 -- изгиб дуги полёта снаряда
		local TIMER_PERIOD          = 0.03125 -- период срабатывания таймера: 1/32 секунды
		local SPEED                 = 200 -- расстояние, которое снаряд преодолеет за секунду
		local GROUP                 = CreateGroup()
		
		local SpellEffectTrigger    = CreateTrigger()
		for i = 0, bj_MAX_PLAYER_SLOTS - 1 do
			TriggerRegisterPlayerUnitEvent(SpellEffectTrigger, Player(i), EVENT_PLAYER_UNIT_SPELL_EFFECT)
		end
		TriggerAddAction(SpellEffectTrigger, function()
			if GetSpellAbilityId() ~= ABILITY_ID then return end
			local caster   = GetTriggerUnit() -- юнит, применивший способность
			local xa, ya   = GetUnitX(caster), GetUnitY(caster) -- координаты точки начала полёта
			local za       = GetTerrainZ(xa, ya) + MISSILE_Z_START -- высота точки начала полёта
			local xb, yb   = GetSpellTargetX(), GetSpellTargetY() -- координаты точки окончания полёта
			local distance = math.sqrt((xb - xa) * (xb - xa) + (yb - ya) * (yb - ya)) -- расстояние между точками
			local angle    = math.atan(yb - ya, xb - xa) -- угол между точками в радианах
			local cos, sin = math.cos(angle), math.sin(angle) -- косинус и синус этого угла
			if distance < MISSILE_MIN_DISTANCE then
				distance = MISSILE_MIN_DISTANCE
				xb, yb   = xb + cos * distance, yb + sin * distance
			end
			local ability      = BlzGetUnitAbility(caster, ABILITY_ID)
			local abilityLevel = GetUnitAbilityLevel(caster, ABILITY_ID)
			local range        = BlzGetAbilityRealLevelField(ability, ABILITY_RLF_AREA_OF_EFFECT, abilityLevel - 1)
			local damage       = abilityLevel
					* math.min(GetHeroStr(caster, true), GetHeroAgi(caster, true), GetHeroInt(caster, true))
					+ BlzGetUnitBaseDamage(caster, 0) + BlzGetUnitDiceNumber(caster, 0) * BlzGetUnitDiceSides(caster, 0)
			
			for i = 0, MISSILE_COUNT[abilityLevel] - 1 do
				local missile            = AddSpecialEffect(MISSILE_EFFECT, xa, ya) -- создаём эффект снаряда
				local distanceAll        = i * MISSILE_STEP + distance -- расстояние между точками
				local zb                 = GetTerrainZ(xa + cos * distanceAll, yb + sin * distanceAll) -- высота точки окончания полёта
				local timeAll            = distanceAll / SPEED -- время, за которо снаряд достигнет цели
				local timeCur            = 0 -- время полёта снаряда
				
				local z                  = za -- начальняая высота эффекта
				local distanceCurrentOld = 0
				TimerStart(CreateTimer(), TIMER_PERIOD, true, function()
					timeCur               = timeCur + TIMER_PERIOD
					local distanceCurrent = distanceAll * (timeCur / timeAll)
					local x, y            = xa + cos * distanceCurrent, ya + sin * distanceCurrent -- считаем новое положение снаряда
					
					if timeCur >= timeAll -- если расстояние равно 0, значит снаряд уже долетел
							or
							not InMapXY(x, y) --снаряд вышел за пределы карты
					then
						BlzSetSpecialEffectOrientation(missile, angle, 0, 0) -- устанавливаем финальное положение эффекта
						DestroyEffect(missile) -- уничтожаем эффект
						DestroyEffect(AddSpecialEffect(IsTerrainDeepWater(x, y) and MISSILE_EFFECT_WATER or MISSILE_EFFECT_GROUND, x, y))
						
						GroupEnumUnitsInRange(GROUP, x, y, range + 256)
						while true do
							local target = FirstOfGroup(GROUP)
							if target == nil then break end
							GroupRemoveUnit(GROUP, target)
							if UnitAlive(target) -- юнит жив
									and IsPlayerEnemy(GetOwningPlayer(caster), GetOwningPlayer(target)) -- юнит враг
									and not IsUnitType(target, UNIT_TYPE_MAGIC_IMMUNE) -- юнит не имунен к маггии
									and not IsUnitType(target, UNIT_TYPE_FLYING) -- юнит не летающий
									and IsUnitInRangeXY(target, x, y, range) -- юнит на нужном расстоянии
							then
								DummyCastStun(target, abilityLevel * 2, abilityLevel)
								UnitDamageTarget(caster, target, damage, false, true, ATTACK_TYPE_MAGIC, DAMAGE_TYPE_NORMAL, WEAPON_TYPE_WHOKNOWS)
							end
						end
						
						PauseTimer(GetExpiredTimer()) -- останавливаем таймер перед уничтожением
						DestroyTimer(GetExpiredTimer()) -- уничтожаем таймер
						return -- завершаем функцию, чтоб пропустить дальнейшие действия
					end
					
					BlzSetSpecialEffectX(missile, x) -- устанавливаем положение эффекта
					BlzSetSpecialEffectY(missile, y) -- устанавливаем положение эффекта
					local zNew  = GetParabolaZ(za, zb, distanceAll * MISSILE_ARC, distanceAll, distanceCurrent) -- считаем новую высоту эффекта
					local zDiff = zNew - z -- считаем разницу высот
					BlzSetSpecialEffectZ(missile, zNew) -- устанавливаем новую высоту эффекта
					local distanceDiff = distanceCurrent - distanceCurrentOld
					local zAngle       = zDiff > 0 and math.atan(distanceDiff / zDiff) - math.pi / 2 or math.atan(-zDiff / distanceDiff) - math.pi * 2 -- считаем угол наклона снаряда
					BlzSetSpecialEffectOrientation(missile, angle, zAngle, 0) -- устанавливаем направление эффекта
					distanceCurrentOld = distanceCurrent
					z                  = zNew -- запоминаем новую высоту эффекта
				end)
			end
		end)
	end
end
Ключевое изменение здесь то, что новую позицию мы считаем из отношения общего расстояния к прошедшему времени
local distanceCurrent = distanceAll * (timeCur / timeAll)
Теперь возьмём отсюда понравившуюся функцию и применим.
local distanceCurrent = distanceAll * (1 - math.cos((timeCur / timeAll) * math.pi / 2))

Заключение

Статья конечно вышла сумбурной и обширной, но я надеюсь, что у меня получилось раскрыть тему простого движения снарядов. Если остались вопросы, то их всегда можно задать в комментариях.
`
ОЖИДАНИЕ РЕКЛАМЫ...

Показан только небольшой набор комментариев вокруг указанного. Перейти к актуальным.
0
29
5 лет назад
0
Хотя странно - у тебя же раньше сжирало анонимные таймеры.
Сжирало до инициализации, скорее всего в ней и дело.
0
24
5 лет назад
0
Сжирало до инициализации, скорее всего в ней и дело.
Видимо да.
0
27
5 лет назад
0
хороший мануал))
2
29
5 лет назад
Отредактирован nazarpunk
2
Я бы, правда, для чистоты эксперимента вызывал сборку вне контекста в котором плодятся потенциально мусорные объекты
Запилил наработку для сборщика, нормально код работает при вызове сборщика из другого контекста.

Дополнил главу про нанесение урона.
0
21
5 лет назад
Отредактирован scopterectus
0
NazarPunk, чтобы получить урон героя, по-моему, нужно к базовому урону еще добавить основную характеристику героя. А зелёный бонус еще нельзя определить.
0
23
5 лет назад
0
ScopteRectuS, можно =)
0
17
5 лет назад
0
А зелёный бонус еще нельзя определить.
Последние нативки позволяют читать поля объекта задаваемые в редакторе. Так что технически можно.
0
29
5 лет назад
Отредактирован nazarpunk
0
чтобы получить урон героя, по-моему, нужно к базовому урону еще добавить основную характеристику героя.
Без бонусов прибавлять ничего не нужно
print(BlzGetUnitBaseDamage(caster, 0), BlzGetUnitBaseDamage(caster, 0) + BlzGetUnitDiceNumber(caster, 0) * BlzGetUnitDiceSides(caster, 0))
А вот с бонусными характеристиками непонятно
можно =)
Обожаю таких людей, которые говорят, что можно и не указывают способ.
Последние нативки позволяют читать поля объекта задаваемые в редакторе. Так что технически можно.
Бонусные характеристика как-бы не задаются в редакторе.
Загруженные файлы
0
17
5 лет назад
0
Бонусные характеристика как-бы не задаются в редакторе.
Я о полях способностей значение которых можно получить и высчитать зеленую атаку.
0
29
5 лет назад
Отредактирован nazarpunk
0
Я о полях способностей значение которых можно получить и высчитать зеленую атаку.
А с предметами и бафами как быть?

Решение тупо оказалось в мануале
local min          = BlzGetUnitBaseDamage(caster, 0) + BlzGetUnitDiceNumber(caster, 0)
local max          = BlzGetUnitBaseDamage(caster, 0) + BlzGetUnitDiceNumber(caster, 0) * BlzGetUnitDiceSides(caster, 0)
print(min, max)
Загруженные файлы
0
32
5 лет назад
0
Простите а есть ли смысл этим заниматся, когда завезли нормальный детект урона и стак баффов? Аксид бомбу берем или ракеты тинкера и улыбаемсо, никакой математики. Т.к на новых патчах изи узнать от кого прилетел снаряд.
Показан только небольшой набор комментариев вокруг указанного. Перейти к актуальным.
Чтобы оставить комментарий, пожалуйста, войдите на сайт.