Энкаунтеры

Содержание:
Энкаунтеры в море несколько сложнее остальных, так как состоят из двух больших систем - это энкаунтеры на глобальной карте и их последующий перенос на боевую.
В этом уроке мы не_будем копаться в деталях обработки координат, динамических таблиц и событий глобальной карты, а также не_будем разбирать квестовые и сухопутные энкаунтеры - всё это отдельные большие темы. Здесь мы разберём непосредственно генерацию энкаунтеров и работу с их параметрами с точки зрения влияния на игровой процесс и баланс.

Точка начала

Кораблики на глобальной карте начинаются с ивента #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() - можете найти её поиском.

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