Энкаунтеры в море несколько сложнее остальных, так как состоят из двух больших систем - это энкаунтеры на глобальной карте и их последующий перенос на боевую.
В этом уроке мы не_будем копаться в деталях обработки координат, динамических таблиц и событий глобальной карты, а также не_будем разбирать квестовые и сухопутные энкаунтеры - всё это отдельные большие темы. Здесь мы разберём непосредственно генерацию энкаунтеров и работу с их параметрами с точки зрения влияния на игровой процесс и баланс.
Точка начала
Кораблики на глобальной карте начинаются с ивента #event_handler("WorldMap_EncounterCreate", "wdmEvent_EncounterCreate");. Его вызова нет в скриптах. Скорее всего, он вызывается передачей сообщений в движок, но об этом позже. Пока посмотрим на функцию, которую он вызывает:
void wdmEvent_EncounterCreate()
{
float dltTime = GetEventData(); // сбор информации, которая была передана в ивент
float playerShipX = GetEventData();
float playerShipZ = GetEventData();
float playerShipAY = GetEventData();
if(CheckAttribute(worldMap, "noenc") != 0) // проверка не отключены ли энкаунтеры
{
if(worldMap.noenc == "true") return;
}
worldMap.playerShipX = playerShipX; // сохраняем положение игрока
worldMap.playerShipZ = playerShipZ;
worldMap.playerShipAY = playerShipAY;
wdmStormGen(dltTime, playerShipX, playerShipZ, playerShipAY); // генерация шторма
wdmShipEncounter(dltTime, playerShipX, playerShipZ, playerShipAY); // генерация энкаунтера-корабля
}
В целом, ничего интересного или полезного здесь не происходит. Собирается информация, переданная в событие, которая затем передаётся в функции генерации шторма и корабликов. Также проверяется активация режима отключения энкаунтеров. Он может активироваться в квестах или при тестировании игры.
//Частота штормов в секунду
#define WDM_STORM_RATE 0.0001
//Частота торговцев в секунду
#define WDM_MERCHANTS_RATE 0.09
//Частота воюющих кораблей в секунду
#define WDM_WARRING_RATE 0.015
//Частота нападающих кораблей в секунду
#define WDM_FOLLOW_RATE 0.025
//Частота специальных событий (бочка или шлюпка) в секунду
#define WDM_SPECIAL_RATE 0.002
// генерация энкаунтера-корабля
void wdmShipEncounter(float dltTime, float playerShipX, float playerShipZ, float playerShipAY)
{
// текущее количество корабликов на карте
int numShips = wdmGetNumberShipEncounters();
// сравнение с кол-вом кораблей, записанных в структуре pchar
if( CheckAttribute(pchar, "worldmap.shipcounter") ) {
numShips = numShips - sti(pchar.worldmap.shipcounter);
}
if( numShips < 0 )
{
trace("Warning! World map ship quantity < 0 : numShips = " + numShips);
trace("pchar.worldmap.shipcounter = " + pchar.worldmap.shipcounter);
numShips = 0;
}
// лимит активных кораблей на карте
if(numShips < 8)
{
//Вероятности появления
wdmTimeOfLastMerchant = wdmTimeOfLastMerchant + dltTime*WDM_MERCHANTS_RATE*1000.0*iEncountersRate;
wdmTimeOfLastWarring = wdmTimeOfLastWarring + dltTime*WDM_WARRING_RATE*1000.0*iEncountersRate;
wdmTimeOfLastFollow = wdmTimeOfLastFollow + dltTime*WDM_FOLLOW_RATE*1000.0*iEncountersRate;
wdmTimeOfLastSpecial = wdmTimeOfLastSpecial + dltTime*WDM_SPECIAL_RATE*1000.0*iEncountersRate;
//Вероятность от количества созданных
// раньше использовался как коэффициент замедляния генерации,
// если кораблей становилось много
// сейчас нигде не используется
float nump = 1.0 - numShips*0.15;
//Выбираем
if(rand(1001) + 1 < wdmTimeOfLastMerchant)
{
// торговец
wdmTimeOfLastMerchant = 0.0;
wdmCreateMerchantShip(0.8 + rand(10)*0.03);
}
else
{
// проверка выключателя DEBUG-панели
bool encoff = false;
if(CheckAttribute(pchar,"worldmapencountersoff") == 1)
{
encoff = sti(pchar.worldmapencountersoff);
}
if(encoff == false)
{
if(rand(1001) + 1 < wdmTimeOfLastWarring)
{
// два корабля в сражении
wdmTimeOfLastWarring = 0.0;
wdmCreateWarringShips();
}
else
{
if(rand(1001) + 1 < wdmTimeOfLastFollow)
{
// боевой корабль
wdmTimeOfLastFollow = 0.0;
// проверка, есть ли у игрока враги
if(!IsStopMapFollowEncounters())
{
wdmCreateFollowShip(0.8 + rand(10)*0.05);
}
}
}
if(rand(1001) + 1 < wdmTimeOfLastSpecial)
{
// кораблекрушенец или бочка
wdmTimeOfLastSpecial = 0.0;
wdmCreateSpecial(0.05 + rand(10)*0.02);
}
}
}
}
else
{
// если кораблей на карте больше 8
// обнуляем счетчики, чтоб понизить коэффициент вероятностей
wdmTimeOfLastMerchant = 0.0;
wdmTimeOfLastWarring = 0.0;
wdmTimeOfLastFollow = 0.0;
wdmTimeOfLastSpecial = 0.0;
}
}
Дефайны в начале файла ..\PROGRAM\worldmap\worldmap_encgen.c определяют как часто будут спавниться энкаунтеры соответствующей категории:
- WDM_STORM_RATE - штормы.
- WDM_MERCHANTS_RATE - энкаунтеры с признаком EncountersTypes[i].Type = ENCOUNTER_TRADE; (торговцы и торговцы с сопровождением), который устанавливается в ..\PROGRAM\Encounters\Encounters_init.c
- WDM_WARRING_RATE - две группы кораблей, сражающихся между собой.
- WDM_FOLLOW_RATE - энкаунтеры с признаком EncountersTypes[i].Type = ENCOUNTER_WAR; (военные эскадры и пираты).
- WDM_SPECIAL_RATE - энкаунтеры с признаком EncountersTypes[i].Type = ENCOUNTER_SPECIAL; (кораблекрушенец и бочка с ресурсами).
Далее идёт функция wdmShipEncounter(), которая вызывается из обработчика ивента. Она определяет, какой энкаунтер будет создан (и будет ли вообще).
Начинается она проверкой соответствия количества корабликов в структуре глобальной карты, с количеством зарегистрированных в структуре pchar. Это технический нюанс работы энкаунтеров глобальной карты и, как я уже сказал, мы здесь в это влезать не будем. Потом проверка лимита активных энкаунтеров и подсчет вероятностей появления разных групп энкаунтеров с учётом всех коэффициентов. Кроме констант, объявленных в начале файла, есть еще два модификатора:
- dltTime - время жизни кораблика на карте;
- iEncountersRate - частота событий, которая устанавливается игроком при начале новой игры.
Ещё из интересного здесь - аргумент в функции вызова wdmCreateFollowShip(0.8 + rand(10)*0.05);. Это число задаёт скорость корабля на глобалке. У игрока оно равно единице. А кораблики, при помощи рандома, могут быть или быстрее или медленнее нас. Что определяет, сможем ли мы от него доплыть до ближайшего форта или нет.
Наконец, по воле рандома, мы переходим к функциям, которые спавнят тот или иной энкаунтер. Их зачем-то разодрали на четыре отдельные метода, хотя их устройство одинаково. Как пример глянем торговца:
bool wdmCreateMerchantShip(float kSpeed)
{
int i1 = -1;
int i2 = -1;
// генерируем первичные данные для вывода описания энкаунтера,
// когда игрок с ним встречается на глобальной карте,
// а также для определения его поведения (догоняет/убегает/движется к координатам и т.д.)
if(GenerateMapEncounter(WDM_ETYPE_MERCHANT, worldMap.island, &i1, &i2) == false) return false;
// создание кораблика на глобалке
string encID = "";
bool res = wdmCreateMerchantShipByIndex(kSpeed, i1, &encID, "", "", 5+rand(5));
// очистка массива энкаунтеров
ReleaseMapEncounters();
return res;
}
Функция wdmCreateMerchantShipByIndex() это непосредственное создание кораблика на карте. Она уходит в вызовы ядра и в рамках данного урока нас не интересует. Мы рассмотрим ф-цию GenerateMapEncounter(), которая, наконец, переносит нас в папку ..\PROGRAM\Encounters\, которая и является предметом сегодняшнего разговора.
Выше я уже упоминал некоторые параметры энкаунтеров, которые присваиваются им при инициализации. Поскольку мы переходим к генерации реальных данных для энкаунтера, будет правильно сначала рассмотреть эту таблицу, а потом вернуться к этой точке и продолжить рассмотрение дальнейших функций с полным пониманием, о чем идёт речь.
Инициализация
При запуске игры происходит инициализация всех основных модулей. В том числе и энкаунтеров. За эту процедуру отвечает void EncountersInit() в основном файле - ..\PROGRAM\Encounters\Encounters.c
extern void InitEncounters();
void EncountersInit()
{
// подключаем файл Encounters_init.c
if(LoadSegment("Encounters\Encounters_init.c"))
{
// инициализация энкаунтеров
InitEncounters();
UnloadSegment("Encounters\Encounters_init.c");
}
// очистить все энкаунтеры
ReleaseMapEncounters();
}
Справка:Обратите внимание, что на момент компиляции скриптов, функция InitEncounters() еще не определена, так как файл Encounters_init.c не подключен при помощи директивы #include. Он не подключается на постоянной основе потому, что нужен нам всего один раз - при запуске игры. Поэтому он подключается непосредственно перед вызовом функции, что в нём находится, и затем сразу выгружается из памяти.
При этом, чтобы компилятор не ругался, что мы вызываем не объявленную функцию - используется предварительное объявление с ключевым словом extern.
В этой функции находится таблица, куда прописываются все доступные энкаунтеры и их параметры. Точнее сами энкаунтеры перечислены в файле Encounters.h, а здесь они определяются. Выглядит это всё следующим образом:
void InitEncounters()
{
int i;
ref rEnc;
// в цикле устанавливаются значения по умолчанию
// для всех энкаунтеров
for (i=0; i<MAX_ENCOUNTER_TYPES; i++)
{
makeref(rEnc, EncountersTypes[i]);
rEnc.Index = i;
rEnc.Chance = 100; // шанс выпадения
rEnc.Skip = false; // отключение автоматической генерации
rEnc.MinRank = 1; // минимальный уровень героя, на котором появляется энкаунтер
rEnc.MaxRank = 1000;// максимальный уровень
rEnc.Merchant.ShipsMin = 0; rEnc.Merchant.ShipsMax = 0; // количество кораблей
rEnc.War.ShipsMin = 0; rEnc.War.ShipsMax = 0; // от ... до ...
rEnc.Type = ENCOUNTER_TRADE; // тип энкаунтера
}
// квестовым отключена автогенерация
makeref(rEnc, EncountersTypes[ENCOUNTER_TYPE_ALONE]);
rEnc.Chance = 0;
rEnc.Skip = true;
// а дальше идут подобного рода перечисления\
// параметров
makeref(rEnc, EncountersTypes[ENCOUNTER_TYPE_MERCHANT_SMALL]); // какой энкаунтер
rEnc.Type = ENCOUNTER_TRADE; // задаём его тип
rEnc.MinRank = 1; // уровень, когда он начнёт появляться в игре
rEnc.MaxRank = 1000; // уровень на котором перестанет
rEnc.worldMapShip = "sloop"; // моделька на глобальной карте
Enc_AddShips(rEnc, "Merchant", 1, 2); // какие корабли и сколько штук
Enc_ExcludeNation(rEnc, PIRATE); // какая нация не_может быть у кораблей энкаунтера
// и это, кстати, не работает
Enc_AddClasses(rEnc, 1, 4, 7, 0, 0); // какие классы кораблей на каком уровне
Enc_AddClasses(rEnc, 1000,4, 7, 0, 0);
// ...
// и так для каждого из перечисленных в Encounters.h
Trace("Init encounters complete.");
}
Собственно, что тут можно добавить? Система с добавлением разных классов для определённого уровня героя предполагала некую прогрессию от более слабых к сильным (и до сих пор есть функции, которые её вычисляют), но здесь это не используется. От 1 до 1000 уровня для всех энкаунтеров забиты одинаковые классы кораблей.
В остальном, со стартовыми параметрами я вас познакомил, дальше, по ходу рассмотрения функций, вам станет понятно для чего они нужны и как работают.
Генерация начальных параметров
Вернёмся к нашим баранам. Функция GenerateMapEncounter() должна сгенерировать нам первичные параметры энкаунтера, которые игрок увидит столкнувшись с ним на глобальной карте.
// генерация первичной информации об энкаунтере на глобалке
bool GenerateMapEncounter(int iMapEncounterType, string sIslandID, ref iEncounter1, ref iEncounter2)
{
iEncounter1 = -1;
iEncounter2 = -1;
bool bReturn = false;
int iNearIslandNation = PIRATE; // нигде не используется
// какой-то выключатель, который тоже нигде не используется
if (sIslandID != "" && !Island_IsEncountersEnable(sIslandID)) { return false; }
// вызов генерации энкаунтера соотв. типа
switch(iMapEncounterType)
{
case WDM_ETYPE_MERCHANT: // торговец
bReturn = GenerateMapEncounter_Merchant(sIslandID, iEncounter1);
break;
case WDM_ETYPE_FOLLOW: // военный
bReturn = GenerateMapEncounter_War(sIslandID, iEncounter1, GetMainCharacterIndex());
break;
case WDM_ETYPE_WARRING: // 2 сражающихся между собой
bReturn = GenerateMapEncounter_Battle(sIslandID, iEncounter1, iEncounter2);
break;
case WDM_ETYPE_SPECIAL: // бочка или кораблекрушение
bReturn = GenerateMapEncounter_Special(sIslandID, iEncounter1);
break;
}
// если генерация не прошла успешно
if (!bReturn)
{
return false;
}
// поскольку ф-ция универсальная, а в случае энкаунтера типа BATTLE
// генерируется 2 энкаунтера, которые между собой сражаются
// то и проверяться будут сразу два энкаунтера
ref rEncounter1, rEncounter2;
if (iEncounter1 != -1)
{
rEncounter1 = &MapEncounters[iEncounter1];
rEncounter1.GroupName = ENCOUNTER_GROUP + iEncounter1;
// если нация энкаунтера пират, но сам энкаунтер - не пираты
// этот костыль сюда добавили потому что сломали выбор нации
if(sti(rEncounter1.nation) == PIRATE)
{
if(sti(rEncounter1.RealEncounterType) < ENCOUNTER_TYPE_PIRATE_SMALL || sti(rEncounter1.RealEncounterType) > ENCOUNTER_TYPE_PIRATE_LARGE)
{
iEncounter1 = -1;
iEncounter2 = -1;
return false;
}
}
}
if (iEncounter2 != -1)
{
rEncounter2 = &MapEncounters[iEncounter2];
rEncounter2.GroupName = ENCOUNTER_GROUP + iEncounter2;
if(sti(rEncounter2.nation) == PIRATE)
{
if(sti(rEncounter2.RealEncounterType) < ENCOUNTER_TYPE_PIRATE_SMALL || sti(rEncounter2.RealEncounterType) > ENCOUNTER_TYPE_PIRATE_LARGE)
{
iEncounter1 = -1;
iEncounter2 = -1;
return false;
}
}
}
// если оба энкаунтера пустые
if(iEncounter1 != -1 && iEncounter2 != -1)
{
if(GetNationRelation(sti(rEncounter1.nation), sti(rEncounter2.nation)) != RELATION_ENEMY)
{
iEncounter1 = -1;
iEncounter2 = -1;
return false;
}
}
int i1 = iEncounter1;
int i2 = iEncounter2;
return true;
}
Функция может показаться довольно длинной, но из выполняющего реальную работу там только конструкция switch, которая опять дробит выполнение на четыре разных функции. Остальное - это костыли к сломанной системе выбора нации. Дальше вы поймёте почему.
Смотрим на примере торговца:
bool GenerateMapEncounter_Merchant(string sIslandID, ref iEncounter)
{
// поиск свободного слота в таблице энкаунтеров
int iEncounterSlot = FindFreeMapEncounterSlot();
if (iEncounterSlot == -1) return false;
ManualReleaseMapEncounter(iEncounterSlot);
ref rEncounter = &MapEncounters[iEncounterSlot];
// поиск энкаунтера подходящего типа в таблице Encounters_init.c
int iEncounterType = FindMerchantEncounter();
if (iEncounterType == -1) return false;
rEncounter.RealEncounterType = iEncounterType;
// генерация количества кораблей в энкаунтере
GenerateMapEncounter_WriteNumShips(rEncounter, iEncounterType, 8);
// выбор подходящей нации для энкаунтера
int iNation = GetRandomNationForMapEncounter(sIslandID, true);
if (iNation < 0)
{
return false;
}
rEncounter.nation = iNation;
// установка флагов:
// слот таблицы энкаунтеров занят, тип энкаунтера
iEncounter = iEncounterSlot;
rEncounter.bUse = true;
rEncounter.Type = "trade";
// задача, которую будет выполнять кораблик
rEncounter.Task = AITASK_MOVE;
// выбор модели кораблика на глобальной карте
return GenerateMapEncounter_SetMapShipModel(rEncounter);
}
И вот здесь, наконец-то, мы в упор подошли к функциям, которые выполняют что-то реальное, ощутимое в игровом мире. Надеюсь, голова у вас ещё не закипела? (:
Смотрим по порядку. Первым делом ищется свободный слот в динамической таблице энкаунтеров - нас это здесь не интересует. Далее производится поиск энкаунтера подходящего типа. Имеется в виду вот эта часть из Encounters_init.c:
makeref(rEnc, EncountersTypes[ENCOUNTER_TYPE_MERCHANT_SMALL]);
rEnc.Type = ENCOUNTER_TRADE; // вот этот тип
В зависимости от того, какой тип энкаунтера - такой поиск и производится. В нашем случае это торговые.
// поиск торгового энкаунтера
int FindMerchantEncounter()
{
int iTypes[100];
int iNumTypes = 0;
ref rCharacter = GetMainCharacter();
int iCharacterRank = sti(rCharacter.rank);
int iChance = rand(250);
// прогоняем всю таблицу из Encounters_init.c
for (int i=0; i<MAX_ENCOUNTER_TYPES; i++)
{
// в поисках энкаунтеров нужного нам типа
if (sti(EncountersTypes[i].Type) == ENCOUNTER_TRADE)
{
// проверяем флаги исключения из генерации
if (sti(EncountersTypes[i].Skip)) { continue; }
// рандом
if (iChance > sti(EncountersTypes[i].Chance)) continue;
// и необходимый уровень, прописанный в той же таблице
if (sti(EncountersTypes[i].MinRank) <= iCharacterRank && sti(EncountersTypes[i].MaxRank) >= iCharacterRank)
{
iTypes[iNumTypes] = i;
iNumTypes++;
}
}
}
// если не нашли ничего подходящего
if (iNumTypes == 0)
{
return -1;
}
return iTypes[rand(iNumTypes - 1)];
}
Помимо проверки непосредственно типа, здесь также присутствуют:
- проверка атрибута Skip, который исключает энкаунтер из автоматической генерации;
- проверка шанса (рандом), чтобы генерировались разные из числа подходящих (скорее всего, это было добавлено когда улетучилась упомянутая ранее прогрессия, при переносе ВМЛ на движок К3);
- уровень персонажа.
Итак, энкаунтер мы нашли, далее идёт вызов функции GenerateMapEncounter_WriteNumShips(), которая вычисляет количество кораблей.
// вычисление количества кораблей в энкаунтере
bool GenerateMapEncounter_WriteNumShips(ref rEncounter, int iEncounterType, int iMaxShipNum)
{
aref aWar, aMerc;
ref rEnc;
makeref(rEnc, EncountersTypes[iEncounterType]);
makearef(aWar, rEnc.War);
makearef(aMerc, rEnc.Merchant);
int iNumMerchantShips, iNumWarShips;
// значения min/max берутся из таблицы Encounters_init.c
// и рандомом определяется конкретное число
iNumMerchantShips = sti(aMerc.ShipsMin) + rand(sti(aMerc.ShipsMax) - sti(aMerc.ShipsMin));
iNumWarShips = sti(aWar.ShipsMin) + rand(sti(aWar.ShipsMax) - sti(aWar.ShipsMin));
int iTotalShips = iNumMerchantShips + iNumWarShips;
// если кораблей не сгенерировано
if (iTotalShips == 0)
{
return false;
}
// затем количество кораблей в каждой группе (военные/торговые)
// снижается до лимита кораблей в энкаунтере (8)
while (iTotalShips > iMaxShipNum)
{
if (iNumWarShips)
{
iNumWarShips--;
iTotalShips--;
}
if (iTotalShips <= iMaxShipNum)
{
break;
}
if (iNumMerchantShips)
{
iNumMerchantShips--;
iTotalShips--;
}
}
// итоговое количество торговых и военных кораблей
// записывается непосредственно в энкаунтер
if(iNumMerchantShips > 0)
{
rEncounter.NumMerchantShips = iNumMerchantShips;
}
else
{
DeleteAttribute(rEncounter, "NumMerchantShips");
}
if(iNumWarShips > 0)
{
rEncounter.NumWarShips = iNumWarShips;
}
else
{
DeleteAttribute(rEncounter, "NumWarShips");
}
return true;
}
Собственно, ничего неожиданного здесь нет. Просто рандом между значениями min и max, которые были указаны в таблице инициализации:
Enc_AddShips(rEnc, "Merchant", 1, 2); // какие корабли и сколько штук
После этого для энкаунтера выбирается подходящая нация:
// выбор нации энкаунтера
int GetRandomNationForMapEncounter(string sIslandID, bool bMerchant)
{ // boal: метод соверненно не понятен, убрал из него массив НатионСтайт, заменив на 1.0, раз работало - пусть будет как было, но логика чумовая
int iNation = -1;
// если передан остров - смотрим его колонию
if(sIslandID != "")
{
int iIsland = FindIsland(sIslandID);
for(int i = 0; i < MAX_COLONIES; i++)
{
if(colonies[i].island == sIslandID)
{
if (colonies[i].nation != "none")
{
iNation = sti(colonies[i].nation);
break;
}
}
}
}
// дефолтный вес каждой нации
float fEngland = 1.0;
float fFrance = 1.0;
float fSpain = 1.0;
float fHolland = 1.0;
float fPirate = 1.0;
// бонус за ближайшую колонию
if(iNation != -1)
{
switch (iNation )
{
case ENGLAND:
fEngland += 0.2;
break;
case FRANCE:
fFrance += 0.2;
break;
case SPAIN:
fSpain += 0.2;
break;
case HOLLAND:
fHolland += 0.2;
break;
case PIRATE:
fPirate += 0.2;
break;
}
}
// собираем всё в один ряд
float fProbablyNation;
if(bMerchant)
{
fProbablyNation = fEngland + fFrance + fSpain + fHolland;
}
else
{
fProbablyNation = fEngland + fFrance + fSpain + fHolland + fPirate;
}
fProbablyNation = frand(fProbablyNation);
// определяем диапазоны в нашем ряду
// для каждой нации
fFrance = fFrance + fEngland;
fSpain = fFrance + fSpain;
fHolland = fSpain + fHolland;
fPirate = fHolland + fPirate;
// а здесь должны были начаться вычисления итоговой нации
// но вместо этого начинаются баги
if(bMerchant == 0)
{
// франция уехала за свой диапазон, своровав его у испании
if(fProbablyNation >= fFrance && fProbablyNation < fSpain)
{
return FRANCE;
}
// и дальше пошло то же самое
if(fProbablyNation >= fSpain && fProbablyNation < fHolland)
{
return SPAIN;
}
if(fProbablyNation >= fHolland && fProbablyNation < fPirate)
{
return HOLLAND;
}
if(fEngland <= fProbablyNation)
{
return ENGLAND;
}
}
else
{
if (rand(2) == 1) return HOLLAND; // можно подумать, что это попытка в реализм
// ведь Голландских торговцев действительно
// было больше всего
// но на самом деле это костыль
if(fProbablyNation >= fFrance && fProbablyNation < fSpain)
{
return FRANCE;
}
if(fProbablyNation >= fSpain && fProbablyNation < fHolland)
{
return SPAIN;
}
if(fProbablyNation >= fHolland && fProbablyNation < fPirate)
{
return HOLLAND;
}
if(fEngland <= fProbablyNation)
{
return ENGLAND;
}
}
// а сами англичане стали пиратами
// или это еще одна, очень тонкая, попытка в реализм?
return PIRATE;
}
Акеллой была заложена достаточно простая логика, но комментарий Алексуса говорит нам, что он в ней по каким-то причинам не разобрался (может времени небыло, может уставший был - не известно).
В функцию передаётся ближайший к игроку остров (если таковой имеется). Он даёт бонус на генерацию корабля именно этой нации. Затем значения веса для каждой из наций суммируются и рандомом определяется кто победил. Но само вычисление победителя сломали.
Первой в диапазоне стоит Англия. Соответственно, значения от 0.0 до fEngland включительно это Англия. Далее, от fEngland до fFrance - это Франция и так далее.
По факту же, Франция засчитывается на диапазоне Испании, Испания на диапазоне Голландии, Голландия на пиратах, а пираты на диапазоне Англии. Да-да, всё что ниже значения fEngland - засчитывается как пираты, потому что этот диапазон не покрыт if-ами. А Англия засчитывается если выпало ровно значение fPirate или диапазон Франции.
Так как логику сломали, но явно не понимали почему - появилась дюжина костылей. Первый из них - это переменная bMerchant, которая обозначает, что мы генерируем торговца. Как я уже сказал - Голландия вычисляется на диапазоне пиратов. Но диапазон пиратов не суммируется, если мы вычисляем торговцев. И это приводит нас к тому, что на карте нет Голландских торговцев. Так родилась вот эта строка:
if (rand(2) == 1) return HOLLAND;
Тот факт, что нации определяются на диапазонах других наций, ломает еще и логику бонусов за остров. Например, бонус за колонию Испании фактически повышает вероятность спавна Французов и т.д.
Ещё где-то тут должен быть учёт допустимых для энкаунтера наций, которые описываются в таблице инициализации. Но его нет. Вероятно потому, что в энкаунтере с пиратами из-за этой функции не генерировались пираты. Ведь они вычисляются на диапазоне Аглии, которая исключена из пиратских энкаунтеров.
А ещё, поскольку на диапазоне Англии вычисляются пираты, появилось 50 строк костылей в функции GenerateMapEncounter(), которые перепроверяют нацию для непиратских энкаунтеров. Такие дела.
Не будем на этом зацикливаться. Последним пунктом выбирается модель кораблика для глобальной карты при помощи функции GenerateMapEncounter_SetMapShipModel(). Но в текущем виде ГПК эта модель задаётся прямо в таблице инициализации, а эта функция только устанавливает особую модель для квестовых энкаунтеров, поэтому мы в ней ковыряться не будем.
Собственно, на этом предварительная генерация заканчивается. Все остальные данные генерируются после того, как игрок встречается с этим энкаунтером и переходит на боевую карту.
Всё остальное
Всё остальное начинается, как я уже сказал, при переходе на боевую карту. А происходит оно всё прямиком в огромной функции запуска этой самой боевой карты - void SeaLogin(ref Login), которую я сюда даже копировать не буду. Нас интересует конкретный вызов: int iNumFantomShips = Fantom_GenerateEncounterExt(sGName, &oResult, iEncounterType, iNumWarShips, iNumMerchantShips);. Это функция, которая генерирует капитанов кораблей и, собственно, их корабли. Капитаны нас здесь не интересуют, потому что это затянется еще на пяток листов А4, перейдём сразу к кораблям:
// подбор классов кораблей в энкаунтере от уровня ГГ
bool Encounter_GetClassesFromRank(int iEncounter, int iRank, ref rMClassMin, ref rMClassMax, ref rWClassMin, ref rWClassMax)
{
// массив типов энкаунтеров из Encounters.h
ref rEnc = &EncountersTypes[iEncounter];
// очистка возвращаемых данных
rMClassMin = 0; rMClassMax = 0;
rWClassMin = 0; rWClassMax = 0;
string sRank = "Rank." + iRank;
// если нету готовых параметров для этого уровня
// то вычисляем их
if (!CheckAttribute(rEnc, sRank))
{
if (CheckAttribute(rEnc, "Rank"))
{
int iLastRank = -1;
int iBestRank = 1000;
aref aRanks; makearef(aRanks, rEnc.Rank);
int iNumRanks = GetAttributesNum(aRanks);
// перебираем прописанные соотношения классов к уровню
for (int i=0; i<iNumRanks; i++)
{
aref aRank = GetAttributeN(aRanks, i);
string sName = GetAttributeName(aRank);
int iCurRank = sti(sName);
// находим запись, ближайшую к текущему уровню ГГ
if (abs(iRank - iCurRank) < iBestRank)
{
iBestRank = abs(iRank - iCurRank);
iLastRank = iCurRank;
}
}
if (iLastRank == -1) { return false; }
sRank = "Rank." + iLastRank;
}
else
{
return false;
}
}
// и копируем из неё данные о классах кораблей
rMClassMin = rEnc.(sRank).1;
rMClassMax = rEnc.(sRank).0;
rWClassMin = rEnc.(sRank).3;
rWClassMax = rEnc.(sRank).2;
return true;
}
Вся эта функция - это вычисление прогрессии, которая должна была закладываться в таблице инициализации и которая умножилась на ноль при переносе ВМЛ. А всем последующим модмейкерам, очевидно, это было не интересно.
Далее, подобранные в данной функции диапазоны классов кораблей, используются в подборе корабля для фантома:
// диапазон доступных для энкаунтера кораблей
for (i=SHIP_TARTANE; i<=SHIP_MANOWAR; i++)
{
object rShip = GetShipByType(i);
if (!checkAttribute(rship, "class"))
{
trace ("bad ship is: " + rship.name);
}
int iClass = MakeInt(rShip.Class);
if (iClass > iClassMin) { continue; } // проверки соответствия входящим параметрам
if (iClass < iClassMax) { continue; }
if (sti(rShip.CanEncounter) != true) { continue; } // исключение уникальных
if (sti(rShip.Type.(sShipType)) != true) { continue; } // подходящие по типу (торговые/военные)
// заносим подходящие в список
iShips[iShipsNum] = i;
iShipsNum++;
}
if (iShipsNum==0)
{
Trace("Can't find ship type '" + sShipType + "' with ClassMin = " + iClassMin + " and ClassMax = " + iClassMax);
return INVALID_SHIP_TYPE;
}
// выбор рандомом из итогового списка
int iBaseShipType = iShips[rand(iShipsNum - 1)];
На этом вся информация, которая относится к игровому представлению энкаунтеров, заканчивается. Единственное, что ещё имеет к этому отношение - сообщение, при встрече на глобальной карте. Эти тексты задаются в функции void wdmRecalcReloadToSea() - можете найти её поиском.