Добавлен , опубликован

История одного заклинания

Содержание:
До сего момента наше заклинание давало постоянный эффект, теперь добавим ему шансовый эффект. Заодно научимся работать c hashtable и предсказывать будущее.

Идея

При использовании заклинания всем дружественным героям с шансом 10*уровень способности сбросим перезарядку последней используемой способности.

Hashtable

Подробно использование хэштаблиц описано в этой статье. Проблема состоит в том, что с разрастанием карты, разработчики боятся случайно перезаписать значения таблицы и напороться на трудновыявимый баг. Поэтому на каждый чих, инициализируют новую таблицу, что крайне расточительно по памяти. Мы же пойдём другим путём и используем обёртку над таблицами, которая позволяет используя всего одну таблицу не бояться случайно перезаписать/удалить не те значения. Так как импорт прост (нужно скопировать всего один триггер), посмотрим на примеры использования.
code
Example usage of HashTable
local HashTable hash = HashTable.create() create it
set hash['hfoo'][StringHash("poop")] = 66 access large parent and child keys as needed
set hash['hfoo'].unit[99999] = GetTriggerUnit() still works with multiple-type syntax so you still have the full hashtable API.
call hash.remove('hfoo') This literally is calling FlushChildHashtable, and should be used when the parentkey and/or HashTable are to be retired
call hash.destroy() DOES NOT FLUSH THE HASHTABLE. You must manually remove each parent key, first, otherwise you'll have a lot of leaked Tables.

Example Usage of Table
struct table_demo extends array
private static method demo takes nothing returns nothing
Create it:
local Table a = Table.create()

Use it:
local boolean b = a.has(69)
set a[654321] = 'A'
set a[54321] = 'B'
set a.unit[12345] = GetTriggerUnit()
set a.unit[GetHandleId(a.unit[12345])] = GetSpellTargetUnit()
set a.real['ABCD'] = 3.14159
set a.integer[133] = 21
remove entries
call a.handle.remove('ABCD')
call a.remove(54321)
Flush/destroy it:
call a.destroy()
Or, only flush it:
call a.flush()
endmethod
endstruct
Example Usage of TableArray
Create it:
local TableArray da = TableArray[0x2000]

Use it:
local thistype this = 0
loop
set this = this.next
exitwhen this == 0
set this.save = this.save + 1
set da[this].real[this.save * 3] = GetUnitX(this.unit)
set da[this].real[this.save * 3 + 1] = GetUnitY(this.unit)
set da[this].real[this.save * 3 + 2] = GetUnitFlyHeight(this.unit)
endloop

Flush/destroy it:
call da.flush()

Or, only destroy it (more efficient if you manage memory yourself)
call da.destroy()

Запоминаем последнюю способность

Логика здесь проста, если герой использует способность, мы её запомним. Заодно немного схитрим, ведь триггер с событием "Боевая единица приводит способность в действие" у нас уже есть.
TriggerAddCondition(t, Condition( function()-> boolean {
	return GetSpellAbilityId() == AbilityID;
}));
Для начала нам понадобится экземпляр хэштаблицы. Посему, в начале заклинания мы его объявим.
Table CooldownTable;
Если вас смущает тип данных Table, который не описан в хорошей статье, то просто запомните, что мы сначала объявляем таблицу CooldownTable типа Table, а в функции onInit мы её создадим.
CooldownTable = Table.create();
Теперь немножко изменим условие триггера, чтобы сохранить id способности.
TriggerAddCondition(t, Condition( function()-> boolean {
	integer id = GetSpellAbilityId();
            
	// Запоминаем ид заклинания для сброса перезарядки
	if (IsUnitType(GetTriggerUnit(), UNIT_TYPE_HERO)){
		CooldownTable.integer[GetHandleId(GetTriggerUnit())] = id;
	}
            
	return id == AbilityID;
}));

Перезаряжаем способность

К сожалению разработчики предусмотрели только функцию UnitResetCooldown.
native UnitResetCooldown takes unit whichUnit returns nothing
Которая по очевидным причинам нам не подходит. Поэтому напишем свою функцию и попытаемся угадать, как её реализуют в Reforged.
// Сброс перезарядки способности, пока не реализовали.
// native UnitResetAbilityCooldown takes unit whichUnit, integer abilityId returns nothing
function UnitResetAbilityCooldown(unit whichUnit, integer abilityId){
	integer level = GetUnitAbilityLevel(whichUnit, abilityId);
	UnitRemoveAbility	(whichUnit, abilityId);
	UnitAddAbility      	(whichUnit, abilityId);
	SetUnitAbilityLevel 	(whichUnit, abilityId, level);
}
И наконец-то перезарядим способности
//if (GetRandomInt(1, 100) <= AbilityLevel * CooldownChance) { // Считаем шанс сброса перезарядки для каждого юнита отдельно
if (true){ // Для тестов будем перезаряжать всегда
	// Сбрасываем перезарядку способностей
	if (IsUnitType(target, UNIT_TYPE_HERO)){
		AbilityToCooldown = CooldownTable.integer[GetHandleId(target)];
		// Если в таблице нет значения, то результат будет null
		if (AbilityToCooldown != null){
			UnitResetAbilityCooldown(target, AbilityToCooldown);
			DestroyEffect(AddSpecialEffectTarget(CooldownEffect, target, "origin"));
		}
	}
}

Предсказываем баги

Для начала проверим, записывается ли id приказа при использовании способностей предмета. Для этого немного переделаем условие.
// Условие срабатывания триггера
TriggerAddCondition(t, Condition( function()-> boolean {
	integer id = GetSpellAbilityId();
	unit u = GetTriggerUnit();
            
	// Запоминаем ид заклинания для сброса перезарядки
	if (IsUnitType(u, UNIT_TYPE_HERO)){
		BJDebugMsg(GetUnitName(u) + " использовал " + I2S(id));
		CooldownTable.integer[GetHandleId(u)] = id;
	}
            
	u = null;
	return id == AbilityID;
}));
Сразу заметно две проблемы:
  • Мы можем добавить предметную способность герою.
  • В BJDebugMsg, выводится непонятное длинное число, вместо всемипонятных равкодов.
Вторую проблему можно запросто решить с посощью функции, которую мы обернём в библиотеку. А вот с первой не так уже и просто. Логически можно было предположить, чтобы отсечь предметы, нужно проверить уровень способности.
native GetUnitAbilityLevel takes unit whichUnit, integer abilcode returns integer
Но она вернёт уровень предметной способности. Так что мы схитрим и заглянем в скрытые идентификаторы приказов.
852008 to 852013 (useslot): Заставляют героя, отдавшего приказ, использовать предмет, находящийся в соответствующем слоте инвентаря. Идентификатор 852008 использует предмет в первом слоте, идентификатор 852009 – во втором и т.д.
// Запоминаем ид заклинания для сброса перезарядки
if (IsUnitType(u, UNIT_TYPE_HERO) && !(GetUnitCurrentOrder(u) >= 852008 && GetUnitCurrentOrder(u) <= 852013)){
	BJDebugMsg(GetUnitName(u) + " использовал " + RAW2S(id));
	CooldownTable.integer[GetHandleId(u)] = id;
}

Теперь если провервить, на разных заклинаниях могут быть непредсказуемые эффекты. Простого способа это определить не существует, поэтому применим подход в лоб - тупо перечислим разрешённые заклинания. Что-бы не засорять основной код, обернём проверку в библиотеку.
library Cooldown {
    public {
        // Сброс перезарядки способности, пока не реализовали.
        // native UnitResetAbilityCooldown takes unit whichUnit, integer abilityId returns nothing
        function UnitResetAbilityCooldown(unit whichUnit, integer abilityId){
            integer level = GetUnitAbilityLevel(whichUnit, abilityId);
            UnitRemoveAbility   (whichUnit, abilityId);
            UnitAddAbility      (whichUnit, abilityId);
            SetUnitAbilityLevel (whichUnit, abilityId, level);
        }
        
        function IsAbilityHasCooldown(integer id) -> boolean {
            return (
                id == 'AHhb' || // Благодать
                id == 'AHbn' || // Изгнание в астрал
                id == 'AHtb' || // Молот бурь
                id == 'AHtc' || // Удар грома
                id == 'AOsh' || // Волна силы
                id == 'AOws' || // Громовая поступь
                id == 'AOfs' || // Громовая поступь
                id == 'AOhx' || // Сглаз
                id == 'AOsw' || // Сторожевая змея
                id == 'AUfn' || // Ледяная звезда
                id == 'AUdc' || // Лик смерти
                id == 'AUim' || // Пронзающая смерть
                id == 'AUdp' || // Смертельный союз
                id == 'AUsl' || // Сон
                id == 'AUcs' || // Тёмная стая
                id == 'AUdr' // Тёмный ритуал
                
                // .. Список можно дополнить по желанию
            );
        }
    }
}
По традиции, составить красивое описание способности оставим в качестве домашнего задания.

Содержание
`
ОЖИДАНИЕ РЕКЛАМЫ...