Caesar III: game loop

Якщо б мене запитали яка частина технічної реалізації гри Цезар цікава мені більше за інших, я б згадав про розрахунку одного «дня» міського життя. Окремі компоненти математичної моделі міста теж цікаві в реалізації, але ці «шестерінки» будуть крутитися тільки в зборі. Велика частина гри проходить всередині «ігрового циклу», в якому проводяться обчислення параметрів компонентів, виконуються переміщення ігрових об'єктів, створюються нові події та об'єкти, якщо вам цікаво дізнатися, як була влаштована симуляція міста в одній з кращих ігор 1998 року — ласкаво просимо під кат. Описи, псевдокод й схеми допоможуть Вам краще дізнатися про використовуваних алгоритмів.




Обрахування одного «дня» в місті автори гри розбили на кілька кроків, основні з яких наведено нижче, спрощений код самої функції можна знайти під спойлером:



Кожні 50 тиків починається новий день (16 днів в ігровому місяці), для якого обчислюється ще кілька функцій, які
не вимагають такої частої обробки, а саме:
  • у перший і восьмий дні місяця розраховується рівень щастя городян і рівень злочинності у місті.
  • при зміні місяці виконується перевірка можливості проведення фестивалю, сплата податків, зарплат, обчислення ймовірностей випадкових подій і ще багато чого.
  • при зміні року виконується збереження параметрів минулого року, оновлення рейтингів.


Етапи розрахунку параметрів міста1. Розрахунок настрою:
— богів
— аборигенів
2. Оновлення параметрів вторгнення військ Цезаря
3. Розрахунок переміщення груп об'єктів
4. Збір інформації по коморах
5. Оновлення даних по доступним сервісів для будинків
6. Оновлення параметрів складів
7. Оновлення даних радника по населенню і поставок пшениці з риму
8. Оновлення споживання товарів в майстернях і матеріалів у видобувних підприємствах
9. Оновлення шляхів до доків
10. Розрахунок виробництва товарів в майстернях
11. Розрахунок доступності дороги в Рим
12. Оновлення населення будинків
13. Розрахунок появи бездомних з перенаселених будинків
14. Розрахунок розподілу робітників на підприємствах, підрахунок безробітних, працюючих підприємств
15. Оновлення області покриття фонтанів і резервуарів
16. Оновлення доступу до води для будинків
17. Оновлення стану груп об'єктів
18. Розрахунок появи городян з обслуговуючих будівель
19. Розрахунок поява торговців
20. Підрахунок типів і кількості будівель у місті, підрахунок покриття об'єктів культури
21. Розрахунок розподілу скарбниці міста між сенатом і форумами
22. Розрахунок зменшення параметрів культури в будинках
23. Розрахунок убування сервісів в будинках
24. Розрахунок впливу будівель на бажаність землі
25. Оновлення рівня будинків
26. Видалення з карти будівель позначених для знесення
27. Оновлення параметрів палаючих руїн
28. Оновлення статусу будівель навколо пожеж
29. Створення протестуючих жителів
30. Розрахунок параметрів збору податків
31. Оновлення рівня розваг у будинках


Етапи розрахунку параметрів міста (код)
void gameLoop()
{
while( game.run )
{
gametime.ticks++;
switch ( gametime.ticks )
{
case 1: calculateGodHappiness(1);
case 2: changeBackgroundMusic();
case 3: minimap_redraw = 1;
case 4: tick_updateCaesarInvasion();
case 5: tick_updateFormations(0);
case 6: tick_checkNativeLand();
case 7: determineRoadNetworkIds();
case 8: gatherGranaryStorageInfo();
case 9: ???
case 10: updateHighestInUseBuildingId();
case 11: ???
case 12: buildingDecayHousesCovered();
case 16: tick_resource_recalculateStock();
case 17: updateAdvisorFoodAndSupplyRomeWheat();
case 18: tick_updateCityInfoWorkshopRawMaterialsstored();
case 19: docksDetermineWaterAccess();
case 20: tick_updateIndustryProduction();
case 21: tick_checkPathingAccessToRome();
case 22: updatePopulationInHouses();
case 23: population();
case 24: evictPeopleFromOvercrowdedHouses();

case 25: calculateWorkersNeededPerCategory();
calculateUnemployment();
setBuildingWorkerPercentage();
setBuildingNumWorkersWater();
setBuildingNumWorkers();

case 27: recalculateReservoirAndFountainAccess();
case 28: gametick_updateHouseWaterAccess();
case 29: updateFormations(1);
case 30: minimap_redraw = 1;
case 31: generateWalkersForBuildings();
case 32: generateTraders();

case 33: countBuildingTypes();
calculateCultureCoverage();

case 34: distributeTreasuryOverForumsAndSenates();
case 35: decayService_culture();
case 36: determineHousingServicesForEvolve();

case 37: calculateDesirabilityOfBuildings();
calculateDesirabilityOfTerrain();

case 38: calculateBuildingDesirability();
case 39: evolveDevolveHouses();
case 40: clearDeletedBuildings();
case 43: updateBurningRuin();
case 44: updateCrimeFireDamage();
case 45: generateCriminal();
case 46: updateDoubleWheatProduction();
case 47:
case 48: decayService_taxCollector();
case 49: gatherEntertainmentInfo();
}

if( gametime.ticks >= 50 )
{
gametime.ticks = 0;
doGameDayTick();
}

renderCity();
}
}


Настання нового дня
void doGameDayTick()
{
++gametime.totalDays;
++gametime.day;

if ( gametime_day > 15 )
{
gametime.day = 0;
cityinfo.newcomersThisMonth = 0;
++cityinfo.monthsSinceFestival;
monthHandle();
++gametime.month;
if ( gametime_month <= 11 ) { updateRatings(0); }
else { startNewYear(); }
recordMonthlyPopulation();
holdFestival(); 
}

if ( !gametime.day || gametime.day == 8 )
calculateCityHappinessAndCrime(); 
}


Настання нового місяця
void monthHandle()
{
calculateHealthRate();
handleRandomEvents();
collectMonthlyTaxes();
payMonthlyWages();
payMonthlyInterest();
payMonthlySalary();
housesConsumeMonthlyFood();
handleDistantBattleEvent();
handleInvasionEvent();
checkRequestsEvent();
checkDemandChangesEvent();
checkPriceChangesEvent();
decreaseMonthsLeftToGovernAfterWin();
tickMonth_updateLegionMorale();
playerMessages_updateMessageDelay();
determineGraphicIdsForRoads();
determineGraphicIdsForWater(0, 0, setting_map_width - 1, setting_map_height - 1);
calculateOpenGroundCitizen();
sortAndCompactPlayerMessages();
}


Настання нового року
void startNewYear()
{
gametime.month = 0;
handleExpandEmpireEvent();
++gametime.year;

gametick_requestBirthsDeaths_calculatehousingtypes();
copyFinanceTaxesToLastYear();
copyFinanceWagesToLastYear();
copyFinanceImportExportToLastYear();
copyFinanceConstructionToLastYear();
copyFinanceInterestToLastYear();
copyFinanceSalaryToLastYear();
copyFinanceSundriesToLastYear();
calculateAndPayTribute();
resetTradeAmounts();
tick_updateFireSpreadDirection();

updateRatings(1);
cityinfo.blessingNeptuneDoubleTradeactive = 0;
}



Релігія



Спочатку римляни були язичниками, поклонялися грецьким і в меншій мірі етруським богам. Пізніше міфологічний період змінився захопленням язичницькими культами. Держава, взявши на себе організацію і проведення ритуалів, створило офіційну релігію, яка змінила попередні уявлення про богів. Релігія в житті людей завжди мало велике значення, і комп'ютерні моделі не уникли людських забобонів, автори гри сильно утрируют поняття прихильності божества, зводячи його до трьом станам покарав-нейтральний благословив, але наявність штрафів і премій робить обчислення менш передбачуваними, також як і наявність елемента випадковості при виборі божества.



Розрахунок настрою богів і умови гніву\благословення
void calculateGodHappiness(int includeBlessingsAndCurses)
{
maxTemples = 0;
maxGod = 10;
minTemples = 100000;
minGod = 10;
cityinfo.maxHappinessCeres = pctReligionCoverageCeres;
cityinfo.maxHappinessNeptune = pctReligionCoverageNeptune;
cityinfo.maxHappinessMercury = pctReligionCoverageMercury;
cityinfo.maxHappinessMars = pctReligionCoverageMars;
cityinfo.maxHappinessVenus = pctReligionCoverageVenus;
for ( i = 0; i < 5; i++)
{
if ( i )
{
switch ( i )
{
case 1:
numTemples = numLargeTemplesNeptune + numSmallTemplesNeptune;
break;
case 2:
numTemples = numLargeTemplesMercury + numSmallTemplesMercury;
break;
case 3:
numTemples = numLargeTemplesMars + numSmallTemplesMars;
break;
case 4:
numTemples = numLargeTemplesVenus + numSmallTemplesVenus;
break;
}
}
else
{
numTemples = numLargeTemplesCeres + numSmallTemplesCeres;
}

if ( numTemples >= maxTemples )
{
if ( numTemples == maxTemples )
maxGod = 10;
else
maxGod = i + 1;
maxTemples = numTemples;
}

if ( numTemples <= minTemples )
{
if ( numTemples == minTemples )
minGod = 10;
else
minGod = i + 1;
minTemples = numTemples;
}
}

for ( j = 0; j < 5; j++)
{
monthsGodSinceFestival = cityinfo.monthsGodSinceFestival[j];
if ( monthsGodSinceFestival > 40 )
monthsGodSinceFestival = 40;
cityinfo.maxGodHappiness[j] += 12;
cityinfo.maxGodHappiness[j] -= monthsGodSinceFestival;
}

if( maxGod )
{
if( maxGod < 5 )
{
if ( cityinfo.monthsGodSinceFestival[maxGod + 3] >= 50 )
cityinfo.monthsGodSinceFestival[maxGod + 3] = 100;
else
cityinfo.monthsGodSinceFestival[maxGod + 3] += 50;
}
}

if ( minGod )
{
if ( minGod < 5 )
cityinfo.monthsGodSinceFestival[minGod + 3] -= 25;
}

if ( cityinfo.population >= 100 )
{
if ( cityinfo.population >= 200 )
{
if ( cityinfo.population >= 300 )
{
if ( cityinfo.population >= 400 )
{
if ( cityinfo.population >= 500 )
min = 0;
else
min = 10;
}
else
{
min = 20;
}
}
else
{
min = 30;
}
}
else
{
min = 40;
}
}
else
{
min = 50;
}

for ( k = 0; k < 5; ++k )
{
if( cityinfo.maxGodHappiness[k] > 100 )
cityinfo.maxGodHappiness[k] = 100;
if( cityinfo.maxHappinessCeres[k] < min )
cityinfo.maxGodHappiness[k] = min;
}

if ( includeBlessingsAndCurses )
{
for ( l = 0; l < 5; ++l )
{
if ( cityinfo.godHappiness[l] <= cityinfo.maxGodHappiness[l] )
{
if ( cityinfo.godHappiness[l] < cityinfo.maxGodHappiness[l] )
++cityinfo.godHappiness[l];
}
else
{
--cityinfo.godHappiness[l];
} 
}

for ( m = 0; m < 5; m++)
{
if( cityinfo.godHappiness[m] > 50 )
cityinfo.godSmallCurseDone[m] = 0;
if ( cityinfo.godHappiness[m] < 50 )
cityinfo.godBlessingDone[m] = 0;
}

god = random_7f_1 & 7;
if ( god <= 4 )
{
if ( cityinfo.godHappiness[god] < 50 )
{
if ( cityinfo.godHappiness[god] < 40 )
{
if ( cityinfo.godHappiness[god] < 20 )
{
if ( cityinfo.godHappiness[god] < 10 )
cityinfo.numBoltsGod[god] += 5;
else
cityinfo.numBoltsGod[god] += 2;
}
else
{
++cityinfo.numBoltsGod[god];
}
}
}
else
{
cityinfo.numBoltsGod[god] = 0;
}
if ( cityinfo.numBoltsGod[god] >= 50 )
cityinfo.numBoltsGod[god] = 50;
}
if ( !gametime.day )
{
for ( n = 0; n < 5; ++n )
++cityinfo.monthsGodSinceFestival[n];

if ( god > 4 )
{
if( determineAngriestGod() )
god = cityinfo.religionAngryGod - 1;
}

if ( setting.godsOn )
{
if ( god <= 4 )
{
if( cityinfo.godHappiness[god] < 100 || cityinfo.godBlessingDone[god] )
{
if ( cityinfo.numBoltsGod[god] < 20
|| cityinfo.godSmallCurseDone[god]
|| cityinfo.monthsGodSinceFestival[god] <= 3 )
{
if ( cityinfo.numBoltsGod[god] >= 50
&& cityinfo.monthsGodsSinceFestival[ god ] > 3 )
{
cityinfo.numBoltsGod[god] = 0;
cityinfo.godHappiness[god] += 30;
message.usePopup = 1;
if ( god ) // large curse
{
switch ( god )
{
case God_Neptune:
if ( cityinfo.numOpenSeaTradeRoutes <= 0 )
{
postMessageToPlayer(42, 0, 0);
return;
}
postMessageToPlayer(81, 0, 0);
neptuneSinkAllShips();
cityinfo.seaTradeProblemDuration = 80;
cityinfo.godCurseNeptuneSankShips= 1;
break;

case God_Mercury:
postMessageToPlayer(43, 0, 0);
removeGoodsFromStorageForMercury(1);
break;

case God_Mars:
if ( largeCurseMarsCurseFort() )
{
postMessageToPlayer(82, 0, 0);
startLocalUprisingFromMars();
}
else
{
postMessageToPlayer(44, 0, 0);
}
break;
case God_Venus:
postMessageToPlayer(45, 0, 0);
setCrimeRiskForAllHouses(40);
increaseSentiment(-10);
if( cityinfo.healthRate < 80 )
{
if ( cityinfo.healthRate < 60 )
changeHealthRate(-20);
else
changeHealthRate(-40);
}
else
{
changeHealthRate(-50);
}
cityinfo.godCurseVenusActive = 1;
alculateCityHappinessAndCrime();
break;
}
}
else
{
postMessageToPlayer(41, 0, 0);
ceresWitherCrops(1);
}
}
}
else
{ // small curse
cityinfo.godSmallCurseDone[ god] = 1;
cityinfo.numBoltsCeres[god] = 0;
cityinfo.godHappiness[god] += 12;
message.usePopup = 1;
if ( god )
{
switch ( god )
{
case God_Neptune:
postMessageToPlayer(92, 0, 0);
neptuneSinkAllShips();
cityinfo.godCurseNeptuneSankShips = 1;
break;
case God_Mercury:
postMessageToPlayer(93, 0, 0);
removeGoodsFromStorageForMercury(0);
break;
case God_Mars:
if ( startLocalUprisingFromMars() )
postMessageToPlayer(94, 0, 0);
else
postMessageToPlayer(44, 0, 0);
break;
case God_Venus:
postMessageToPlayer(95, 0, 0);
setCrimeRiskForAllHouses(50);
increaseSentiment(-5);
hangeHealthRate(-10);
calculateCityHappinessAndCrime();
break;
}
}
else
{
postMessageToPlayer(91, 0, 0);
ceresWitherCrops(0);
}
}
}
else
{
cityinfo.godBlessingDone[god] = 1;
message_usePopup = 1;
if ( god )
{
switch ( god )
{
case God_Neptune:
postMessageToPlayer(97, 0, 0);
cityinfo.blessingNeptuneDoubleTradeactive = 1;
break;
case God_Mercury:
postMessageToPlayer(98, 0, 0);
smallBlessingMercuryFillGranary();
break;
case God_Mars:
postMessageToPlayer(99, 0, 0);
cityinfo_blessingMarsEnemiesToKill = 10;
break;
case God_Venus:
postMessageToPlayer(100, 0, 0);
increaseSentiment(25);
break;
}
}
else // ceres
{
postMessageToPlayer(96, 0, 0);
ceresBlessing();
}
}
}
minHappiness = 100;
for ( ii = 0; ii < 5; ++ii )
{
if ( cityinfo.godHappiness[ii] < minHappiness )
minHappiness = cityinfo.godHappiness[ii];
}
if ( cityinfo.godAngryMessageDelay )
{
--cityinfo_godAngryMessageDelay;
}
else
{
if ( minHappiness < 30 )
{
cityinfo.godAngryMessageDelay = 20;
if ( minHappiness >= 10 )
postMessageToPlayer(55, 0, 0);
else
postMessageToPlayer(101, 0, 0);
}
}
}
}
}
}



Настрої в місті



Самі мешканці реагують на різницю зарплат між містом і Римом, різноманітність продуктів
у місті, рівень податків і число нетрів. Даний параметр зберігається для кожного будинку
і не змінюється незалежно від сусідніх будинків.



Розрахунок настрою і ступеня міграції в місті
void calculateCityHappinessAndCrime()
{
totalPop = calculatePeopleInHousingTypes();
if ( totalPop < cityinfo.population )
removePeopleFromCensus(ciid, cityinfo.population - totalPop);

sentimentContributionTents = 0;
sentimentContributionFood = 0;
sentimentContributionWages = 0;
sentimentContributionTaxes = taxrate_happiness_factor[ cityinfo.taxpercentage ];

diffWage = cityinfo.wages - cityinfo.wagesRome;

switch( diffWage )
{
>= 7: sentimentContributionWages = 4;
>= 4: sentimentContributionWages = 3;
>= 1: sentimentContributionWages = 2;
== 1: sentimentContributionWages = 1;
}

if ( diffWage < 0 )
{
sentimentContributionWages = -diffWage / 2;
}

switch( cityinfo.unemploymentPercentage )
{
> 25: sentimentContributionEmployment = -3;
> 17: sentimentContributionEmployment = -2;
> 10: sentimentContributionEmployment = -1;
< 5: sentimentContributionEmployment = 1;
}

if( cityinfo.populationSentiment_includetents > 0 )
{
tentPenaltyIfLessTents = getHappinessPenaltyForTentDwellers();
cityinfo.populationSentiment_includetents = 0;
}
else
{
tentPenaltyIfLessTents = 0;
cityinfo.populationSentiment_includetents = 1;
}

housesNeedingFood = 0;
housesCalculated = 0;
totalSentimentContributionFood = 0;
totalTentPenalty = 0;
for( building in city.buildings )
{
if ( building.inUse == 1 )
{
if ( building.houseSize )
{
if ( building.house_population )
{
if ( cityinfo.population >= 300 )
{
building.house_happiness += sentimentContributionTaxes;
building.house_happiness += sentimentContributionWages;
building.house_happiness += sentimentContributionEmployment;
++housesCalculated;
sentimentContributionFood = 0;
sentimentContributionTents = 0;

if ( model.houses_foodtypes[ building.level ] > 0 ) // needs food: >= shack
{
++housesNeedingFood;

sentimentContributionFood = building.houseNumFoods - building.houseHaveFoods;
++totalSentimentContributionFood;
}
else // tent dwellers
{
sentimentContributionTents = tentPenaltyIfLessTents;
totalTentPenalty += tentPenaltyIfLessTents;
}

building.house_happiness += sentimentContributionFood;
building.house_happiness += sentimentContributionTents;
}
else
{
sentimentContributionFood = 0;
sentimentContributionEmployment = 0;
sentimentContributionTaxes = 0;
sentimentContributionWages = 0;
sentimentContributionTents = 0;

if ( cityinfo.population >= 200 )
building.house_happiness = 50;
else
building.house_happiness = 60;
}
}
else
{
building.house_happiness = 60;
}
}
}
}

if ( housesNeedingFood )
sentimentContributionFood = totalSentimentContributionFood / housesNeedingFood;

if ( housesCalculated )
sentimentContributionTents = totalTentPenalty / housesCalculated;

totalHappiness = 0;
totalHouses = 0;

for ( building in city.buildings )
{
if( building.inUse == 1 && building.houseSize && building.house_population )
{ 
++totalHouses;
totalHappiness += building.happiness;
}
}

if ( totalHouses > 0 )
cityinfo.citySentiment = totalHappiness / totalHouses;
else
cityinfo.citySentiment = 60;

cityinfo.emigrationCause = 0;

worstSentiment = 0;
if( sentimentContributionFood < 0 )
{
worstSentiment = sentimentContributionFood;
cityinfo.emigrationCause = 1;
}

if ( sentimentContributionEmployment < worstSentiment )
{
worstSentiment = sentimentContributionEmployment;
cityinfo.emigrationCause = 2;
}

if ( sentimentContributionTaxes < worstSentiment )
{
worstSentiment = sentimentContributionTaxes;
cityinfo.emigrationCause = 3;
}

if ( sentimentContributionWages < worstSentiment )
{
worstSentiment = sentimentContributionWages;
cityinfo.emigrationCause = 4;
}

if ( sentimentContributionTents < worstSentiment )
cityinfo.emigrationCause = 5;

cityinfo.citySentimentLastTime = cityinfo_citySentiment;
}



Фестивалі



Значне збільшення настрої в місті дає тільки перший фестиваль за повні 12 місяців, другий і наступні тільки половину. Зроблено це для того, щоб у багатому місті не було можливості підняти настрій тільки за рахунок фестивалів. Підготовка до самого фестивалю теж займає певний час, що накладає обмеження на кількість проведених за рік фестивалів.



Розрахунок впливу фестивалю на настрій в місті
void holdFestival()
{
--cityinfo.monthsSinceFirstFestival;
--cityinfo.monthsSinceSecondFestival;

if ( cityinfo.plannedFestival_size <= 0 )
return;

--cityinfo.plannedFestival_monthsTogo;

if( cityinfo.plannedFestival_monthsTogo > 0 )
return;

if ( cityinfo.monthsSinceFirstFestival > 0 )
{
if ( cityinfo.monthsSinceSecondFestival <= 0 )
{
cityinfo.monthsSinceSecondFestival = 12;
switch ( cityinfo.plannedFestival_size )
{
case smallFestival:
increaseSentiment(2);
break;
case middleFestival:
increaseSentiment(3);
break;
case bigFestival:
increaseSentiment(5);
break;
}
}
}
else
{
cityinfo.monthsSinceFirstFestival = 12;
switch ( cityinf._plannedFestival_size )
{
case smallFestival:
increaseSentiment(7);
break;
case middleFestival:
increaseSentiment(9);
break;
case bigFestival:
increaseSentiment(12);
break;
}
}

cityinfo.monthsSinceFestival = 1;
switch ( cityinfo.plannedFestival_size )
{
case smallFestival:
postMessageToPlayer(38, 0, 0);
break;
case middleFestival:
postMessageToPlayer(39, 0, 0);
break;
case bigFestival:
postMessageToPlayer(40, 0, 0);
break;
}
cityinfo.plannedFestival_size = 0;
cityinfo.plannedFestival_monthsTogo = 0;
}



Виплата податі імператору



Виплата данини імператору. Кількість грошей, яка в кінці року потрібно виплатити з казни
залежить від прибутку міста і від кількості проживаючих людей. Перший фактор означає виплату чверті
отриманих за рік грошей, але не менше певної суми, яка залежить від поточного населення. Якщо ж місто
не може виплатити цих грошей, то правитель розплачується зниженням прихильності імператора, причому враховуються і минулі
року невиплат, так що невдоволення накопичується при тривалих неуплатах данини.



Розрахунок імперської четвертини за останній рік
void calculateAndPayTribute()
{
cityinfo.finance_donated_lastyear = cityinfo.finance_donated_thisyear;
cityinfo.finance_donated_thisyear = 0;

cityinfo.tributeNotPaid = 0;

income = cityinfo.finance_donated_lastyear
+ cityinfo.finance_taxes_lastyear
+ cityinfo.finance_exports_lastyear

expenses = cityinfo.finance_sundries_lastyear
+ cityinfo.finance_salary_lastyear
+ cityinfo.finance_interest_lastyear
+ cityinfo.finance_construction_lastyear
+ cityinfo.finance_wages_lastyear
+ cityinfo.finance_imports_lastyear

if ( cityinfo.treasury > 0 )
{
switch( cityinfo.population )
{
> 5000: cityinfo.finance_tribute_lastyear = 500;
> 3000: cityinfo.finance_tribute_lastyear = 400;
> 2000: cityinfo.finance_tribute_lastyear = 300;
> 1001: cityinfo.finance_tribute_lastyear = 225;
> 501: cityinfo.finance_tribute_lastyear = 150;
> 0: cityinfo.finance_tribute_lastyear = 50;
}

if ( income > expenses )
{
cityinfo.tributeNotPaidYears = 0;
realTribute = adjustWithPercentage(income - expenses, 25);
if ( realTribute > cityinfo.finance_tribute_lastyear )
cityinfo.finance_tribute_lastyear = realTribute;
}
}
else
{
cityinfo.tributeNotPaid = 1;
++cityinfo.tributeNotPaidYears;
cityinfo.finance_tribute_lastyear = 0;
}

cityinfo.treasury -= cityinfo.finance_tribute_lastyear;
expenses += cityinfo.finance_tribute_lastyear;

calculateTributeThisYear();

cityinfo.finance_balance_lastyear = cityinfo.treasury;
cityinfo.finance_totalIncome_lastyear = income;
cityinfo.finance_totalExpenses_lastyear = expenses;
}



Випадкові події



Випадкові події. У грі доступно 7 випадкових подій, за їх виникнення відповідає набір прапорів, які можна змінювати в редакторі місій. Випадкова подія виникає, якщо генератор викинув його тип і воно дозволено в сценарії. Розробники зробили наступні типи: пониження/підвищення зарплати в Римі, проблеми з торгівлею морської або наземної, отруєння колодязів, обвалення затоплення шахт і кар'єрів і глиняних ям. Землетрус задається в редакторі, має час і точку виникнення, поширюється випадково за чотирма напрямками.

Випадкові події
void handleRandomEvents()
{
event = randomEvent.probability[random_7f_1];
if ( event > 0 )
{
switch ( event )
{
case 1:
if ( scn_event_raiseWages )
{
if ( cityinfo.wagesRome < 45 )
{
cityinfo.wagesRome += (random_7f_2 & 3) + 1;
if ( cityinfo.wagesRome > 45 )
cityinfo.wagesRome = 45;
message_usePopup = 1;
postMessageToPlayer(68, 0, 0);
}
}
break;
case 2:
if ( scn_event_lowerWages )
{
if ( cityinfo.wagesRome > 5 )
{
cityinfo.wagesRome -= (random_7f_2 & 3) + 1;
message_usePopup = 1;
postMessageToPlayer(69, 0, 0);
}
}
break;
case 3:
if ( scn_event_landTradeProblem )
{
if ( cityinfo.numOpenLandTradeRoutes > 0 )
{
cityinfo.landTradeProblemDuration = 48;
message_usePopup = 1;
if ( scn_climate == Climate_Desert )
postMessageToPlayer(65, 0, 0);
else
postMessageToPlayer(67, 0, 0);
}
}
break;
case 4:
if ( scn_event_seaTradeProblem )
{
if ( cityinfo.numOpenSeaTradeRoutes > 0 )
{
cityinfo.seaTradeProblemDuration = 48;
message_usePopup = 1;
postMessageToPlayer(66, 0, 0);
}
}
break;
case 5:
if ( scn_event_contaminatedWater )
{
if ( cityinfo.population > 200 )
{
if ( cityinfo.healthRate <= 80 )
{
if ( cityinfo.healthRate <= 60 )
changeHealthRate(-25);
else
changeHealthRate(-40);
}
else
{
changeHealthRate(-50);
}
message_usePopup = 1;
postMessageToPlayer(70, 0, 0);
}
}
break;
case 6:
if ( scn_event_ironMineCollapse )
{
gridOffsetIronmine = destroyFirstBuildingOfType(B_IronMine);
if ( gridOffsetIronmine )
{
message_usePopup = 1;
postMessageToPlayer(71, 0, gridOffsetIronmine);
}
}
break;
case 7:
if ( scn_event_clayPitFlooded )
{
gridOffsetClaypit = destroyFirstBuildingOfType(B_ClayPit);
if ( gridOffsetClaypit )
{
message_usePopup = 1;
postMessageToPlayer(72, 0, gridOffsetClaypit);
}
}
break;
}
}
}



Здоров'я жителів



Практикуючі лікарі порівняно пізно з'явилися в Римі. Аж до II ст. до н. е.., а в малозабезпечених верствах суспільства і багато пізніше римляни лікувалися у навчених життєвим досвідом своїх родичів нехитрими засобами, які передавалися з покоління в покоління. Ця народна медицина не чуждалась і примітивної магії. Були складені керівництва по землеробству, мають цілий ряд вказівок, як лікувати людей і тварин, явно наслідуючи народних засобів.
У грі клініки та госпіталі надають одні і тіж послуги, але госпіталі також потрібні для будинків високого рівня для продовження росту. Здоров'я жителів є одним з основних показників процвітання міста: епідемії можуть викошувати цілі квартали і з збільшенням населення міста число людей померлих під час епідемії буде тільки рости.



Обчислення здоров'я та ймовірності епідемії в місті
void calculateHealthRate()
{
population = 0;
populationWithDoctors = 0;
if ( cityinfo.population < 200 )
{
cityinfo.healthRate = 50;
cityinfo.calculatedTargetHealthRate = 50;
return;
}

for( building in city.buildings )
{
if ( building.inUse == 1 && building.houseSize > 0 &&
building.house_population > 0 )
{
population += building.house_population;
if ( building.hasClinicService )
populationWithDoctors += building.house_population;
else
populationWithDoctors += building.house_population / 4;
}
}

cityinfo.calculatedTargetHealthRate = getPercentage(populationWithDoctors, population);
cityinfo.healthRate += sign( cityinfo.healthRate - cityinfo.calculatedTargetHealthRate ) * 2;
cityinfo.healthRate = bound( 0, cityinfo.healthRate, 100 );

if ( cityinfo.healthRate >= 40 )
return;

pandemicChance = 40 - cityinfo.healthRate;
goodHealthPeople = random_7f_1 & 0x3F;
if ( cityinfo.godCurseVenusActive )
goodHealthPeople = 0;

cityinfo.godCurseVenusActive = 0;

if ( goodHealthPeople > pandemicChance )
return;

howPeopleCanDie = adjustWithPercentage(populationWithDoctors, (random_7f_1 & 3) + 7);
if ( howPeopleCanDie > 0 )
{
howPeopleCanDie = howPeopleCanDie - cityinfo.numHospitalWorkers;
changeHealthRate(10);

if( howPeopleCanDie > 0 )
{
if ( cityinfo.numHospitalWorkers > 0 )
postMessageToPlayer(103, 0, 0);
else
postMessageToPlayer(104, 0, 0);

for( building in city.buildings )
{
if ( building.inUse == 1 && building.houseSize > 0
&& building.house_population > 0 && !building.hasClinicService )
{
howPeopleCanDie -= building.house_population;
collapseBuildingOnFire(j, 1);
if ( howPeopleCanDie <= 0 )
return;
}
}
}
else
{
postMessageToPlayer(102, 0, 0);
}
}


}



Збір податків



На перших порах місто живе за рахунок надходження податків, хоча вони досить скромні. Збирач податків повинен так само як наприклад, лікар з клініки, проходити поруч з житловими будинками з певною періодичністю. Якщо житловий будинок безперебійно відвідується збирачами податків, то він кожен місяць буде платити податок.

То скільки житловий будинок буде платити в місяць, залежить від:
— процентної ставки, встановленої у радника з фінансів (регулюється від 0 до 25 %);
— кількості людей, що проживають в будинку, на момент збору податків (тобто при зміні місяці);
— рівня розвитку будинки (в ігровому файлі «c3_model.txt»дані по житлових будинках, 20-а колонка — це число є 200 процентним податком на місяць з однієї людини, що проживає в даному рівні розвитку житлового будинку).

Проаналізувавши функцію можна зробити висновок, що плавне підняття податків ніяк не відрізняється від швидкого зміни. Податки впливають на настрій ваших жителів, але немає сенсу підвищувати зарплату своїх робітників більш ніж на 8 одиниць, по відношенню до зарплати яку платить Рим.

Що таке 200-відсотковий податок, це (ймовірно) бажання розробників заощадити на операції приведення до меншого цілого, в цьому блоці
collectedPatricians = adjustWithPercentage(
cityinfo.monthlyCollectedTaxFrompatricians / 2,
cityinfo.taxpercentage );


коду видно, що зібрані податки діляться на 2. Щоб не отримати ситуацію, коли будинок може заплатити більше встановленого був введений податок * 2, а при обчисленнях ми завжди будемо отримувати значення менше або рівне правильному.

На більш високих рівнях розвитку міста, він цілком може жити тільки за рахунок податків




Обчислення отриманих за місяць податків
void __cdecl fun_collectMonthlyTaxes()
{
cityinfo.numPlebsTaxed = 0;
cityinfo.numPatriciansTaxed = 0;
cityinfo.numPlebsNotTaxed = 0;
cityinfo.numPatriciansNotTaxed = 0;
cityinfo.monthlyUncollectedTaxFromplebs = 0;
cityinfo.monthlyCollectedTaxFromplebs = 0;
cityinfo.monthlyUncollectedTaxFrompatricians = 0;
cityinfo.monthlyCollectedTaxFrompatricians = 0;

for ( i = 0; i < 20; ++i )
cityinfo.societyGraph[ i ] = 0;

for ( house in city.houses )
{
isPatrician = house.level >= 12;
trm = adjustWithPercentage(
model_houses.tax[ house.level ],
difficulty.moneypct[setting.difficulty] );

cityinfo.societyGraph[ house.level ] += house.population;
if (house.taxcollector > 0 )
{
if ( isPatrician )
cityinfo.numPatriciansTaxed += house.population;
else
cityinfo.numPlebsTaxed += house.population;

tax = house.population * trm;
house.taxIncomeThisYear += tax;

if ( isPatrician )
cityinfo.monthlyCollectedTaxFrompatricians += tax;
else
cityinfo.monthlyCollectedTaxFromplebs += tax;
}
else
{
if ( isPatrician )
cityinfo.numPatriciansNotTaxed += house.population;
else
cityinfo.numPlebsNotTaxed += house.population;

if ( isPatrician )
cityinfo.monthlyUncollectedTaxFrompatricians += house.population * trm;
else
cityinfo.monthlyUncollectedTaxFromplebs += house.population * trm;
}
}

collectedPatricians = adjustWithPercentage(
cityinfo.monthlyCollectedTaxFrompatricians / 2,
cityinfo.taxpercentage );

cityinfo.yearlyTaxFromPatricians += collectedPatricians;

collectedPatricians2 = collectedPatricians;

collectedPlebs = adjustWithPercentage(
cityinfo.monthlyCollectedTaxFromplebs / 2,
cityinfo.taxpercentage );
cityinfo.yearlyTaxFromPlebs += collectedPlebs;
totalCollectedTax = collectedPlebs + collectedPatricians2;

cityinfo.yearlyUncollectedTaxFrompatricians += adjustWithPercentage( cityinfo.monthlyUncollectedTaxFrompatricians/ 2,
cityinfo.taxpercentage);

cityinfo.yearlyUncollectedTaxFromplebs += adjustWithPercentage( cityinfo.monthlyUncollectedTaxFromplebs / 2,
cityinfo.taxpercentage);
cityinfo.treasury += totalCollectedTax;

cityinfo.percentagePlebsRegisteredfortax = getPercentage( cityinfo.numPlebsTaxed,
cityinfo.numPlebsNotTaxed + cityinfo.numPlebsTaxed );

cityinfo.percentagePatriciansRegisteredfortax = getPercentage( cityinfo.numPatriciansTaxed,
cityinfo.numPatriciansNotTaxed + cityinfo.numPatriciansTaxed );

cityinfo.percentageRegisteredFortax = getPercentage( cityinfo.numPlebsTaxed + cityinfo_numPatriciansTaxed,
cityinfo.numPlebsNotTaxed + cityinfo.numPlebsTaxed
+ cityinfo.numPatriciansNotTaxed + cityinfo.numPatriciansTaxed );
}



Споживання продуктів



Люди їдять Х кількості їжі, незалежно від того скільки у них видів їжі. Съедаемое кількість їжі залежить тільки від кількості осіб проживаючих в будинку (10 людина з'їдає 5 одиниць їжі в місяць) Наприклад: є житловий будинок 20-го рівня, повністю заселений тобто в ньому проживає 200 чоловік їм необхідно 3 види їжі. Вони в місяць будуть з'їдати загальна кількість їжі рівне 200/10*5 = 100 одиницям, ці 100 одиниць розподіляться між трьома необхідними видами їжі, скоріше всього поровно, тобто за 100/3 = 33

Обчислення споживання для будинків
void housesConsumeMonthlyFood()
{
gatherFoodInformation();
cityinfo.foodTypesEaten = 0;
totalConsumed = 0;

for ( building in city.houses )
{
numTypes = model_houses.foodtypes[ building.level ];
foodToConsumePerType = adjustWithPercentage( building.population, 50);

if ( numTypes > 1 )
foodToConsumePerType /= numTypes;

building.houseNumFoods = 0;

if ( scn_romeSuppliesWheat )
{
cityinfo.foodTypesEaten = 1;
cityinfo.foodTypesAvailable = 1;
building.foodstocks[0] = foodToConsumePerType;
building.houseNumFoods = 1;
}
else
{
if ( numTypes > 0 )
{
for ( j = 0; ; ++j )
{
if ( j < 4 )
{
if (building.foodstocks[j] < foodToConsumePerType )
{
if ( building.foodstocks[j] )
{
building.foodstocks[j] = 0;
++building.houseNumFoods;
totalConsumed += foodToConsumePerType;
}
}
else
{
building.foodstocks[j] -= foodToConsumePerType;
++building.houseNumFoods;
totalConsumed += foodToConsumePerType;
}
if ( building.houseNumFoods > cityinfo.foodTypesEaten )
cityinfo.foodTypesEaten = building.houseNumFoods;

if ( building.houseNumFoods < numTypes )
continue;
}
break;
}
}
}
}
cityinfo.foodConsumedLastMonth = totalConsumed;
cityinfo_foodStoredLastMonth = cityinfo_foodStoredSoFarThisMonth;
cityinfo_foodStoredSoFarThisMonth = 0;
}



Виробництво товарів



Іноді для виробництва товарів місту потрібно імпортувати матеріали. Вісім майстерень працюють стабільно з одним складом, ще дві з перебоями: імпорт, залежить від кількості майстерень та від кількості матеріалів, які може надати торговий партнер. Експорт можна чисельно регулювати, а імпортується сировина пропорційно запитом майстерень. Тому при невеликій кількості майстерень склад просто не буде закуповувати сировину про запас.

Підрахунок рейтингу добробуту

Рейтинг добробуту в грі є самим «важким» для підйому, з одного боку будь проступок з боку правителя веде до його зниження, з іншого — максимальний приріст рівня становить 2 пункту, тобто при дотриманні всіх правил позначку в 50 пунктів місто досягне мінімум через 25 років, без урахування штрафів і премій.



Підрахунок рівня добробуту міста
void updateProsperityRating()
{
labor = 0;
if ( cityinfo.unemploymentPercentage >= 5 )
{
if ( cityinfo.unemploymentPercentage >= 15 )
labor = -1; // -1 Unemployment rate is above 15%
}
else
{
labor = 1; // +1 Less than 5% unemployment
}
if ( cityinfo.finance_construction_lastyear + cityinfo.treasury <= cityinfo.treasury_lastyear_prosperity )
increase = labor - 1; // -1 Losing money
else
increase = labor + 5; // +5 Making a profit

cityinfo.treasury_lastyear_prosperity = cityinfo.treasury;
if ( cityinfo.foodTypesEaten >= 2 )// == grand insula or better
++increase; // +1 There is at least one Grand Insula or better
avgWage = cityinfo.wageRatePaid_lastYear / 12;
if ( avgWage <= cityinfo.wagesRome + 1 )
{
if ( avgWage < cityinfo.wagesRome )
--increase; // -1 Your wages below are Rome's
}
else
{
++increase; // You pay at least 2 Dn more than Rome's wage
}
poor = getPercentage(cityinfo_peopleInTentsAndShacks, cityinfo_population);
rich = getPercentage(cityinfo_peopleInVillasAndPalaces, cityinfo_population);

if ( poor > 30 )
--increase;

if ( rich > 10 )
++increase; // +1 10% or more of your population lives in villas

if ( cityinfo.tributeNotPaid )
--increase;

if ( cityinfo_hippodromeShows > 0 )
++increase; // +1 Active Hippodrome

cityinfo_prosperityRating += increase;

if ( cityinfo.prosperityRating > cityinfo.maxProsperity )
cityinfo.prosperityRating = cityinfo.maxProsperity;

if ( cityinfo.prosperityRating < 0 )
cityinfo.prosperityRating = 0;

if ( cityinfo.prosperityRating > 100 )
cityinfo.prosperityRating = 100;
setProsperityRatingExplanation();
}



Структури даних ігри



Caesar III оперує тільки статичними масивами, тому кількість будівель, людей, об'єктів, груп відомо заздалегідь. Так, наприклад, кількість будівель і відображених городян не може бути більше 2000, кількість груп об'єктів у місті (вовки, вівці, легіони, протестувальники) не перевищує 50. Такі жорсткі обмеження були накладені через необхідність працювати з ОЗП менше 32Мб, з яких половина була зайнята архівом з текстурами. Нижче я наведу опис текстур з тими полями, фізичний зміст яких вдалося відновити.

(Walker) Опис нерухомого об'єкта
struct Walker
{
int gridOffset; //зміщення на карті (y * mapWidth + x)
char inUse; //ця запис активна
short nextIdOnSameTile; //наступний об'єкт на цьому тайлі
unsigned char actionState; //поточний стан об'єкта (йде, б'ється, сидить, чекає)
int tradeCityId; //поле для торговця, з якого міста прибув
int direction; //напрям руху
int buildingId; //номер будинку, куди прямує об'єкта

unsigned char y; //позиція всередині тайла
unsigned char x;

unsigned char byte_7FA360; //dst_x ???
unsigned char byte_7FA361; //dst_y ???

int progressOnTile; //зміщення відносно центру тайла, задіюється при повороті карти

int tilePosition_y; //абсолютне зміщення на карті
int tilePosition_x; 

int destination_x; //точка призначення
int destination_y;

WalkerType type; //тип об'єкта 
int word_7FA344;
char byte_7FA34C;
char speed; //швидкість переміщення
char byte_7FA3A6;

int state; //попередній стан об'єкта
short baseWorkingBuildingId; //поле для обслуговуючого персоналу, базова будівля 
short formationId; //номер групи, в яку входить об'єкт
short word_7FA346;
char byte_7FA39B;
short word_7FA366;
short tradeCaravanNextId; //ідентифікатор наступного об'єкта в каравані, актуально для торговців і торговки з ринку
short itemCollecting;
char byte_7FA341;
short migrantDestinationHome; //номер будинку, куди прямує мешканець
short word_7FA374;
short destinationpathId; //номер шляху, на який можна перейти, якщо буде затор
char byte_7FA376;
char lastDirection; //попереднє напрямок руху
short word_7FA3B0;
short wlk_ID_mm;
short word_7FA3B4;
short word_7FA3B6;
short word_7FA372;
short word_7FA35E;

char cartPusherGoodType; //тип товару у носильника
char byte_7FA39C;
char byte_7FA39D;
char byte_7FA393;
char reachedLastStep; //прапор останнього тайла (шлях завершений 0/1)
char maxLevelOrRiskSeen; //прапор для префекта, що поруч пожежа або ворог (0\1) 
char byte_7FA3B8;
char byte_7FA342;
char byte_7FA3A5;
char byte_7FA3A2;
char isBoat; //прапор човни
char byte_7FA34D;
char byte_7FA39F;
char byte_7FA3A7;
char byte_7FA3A9;

short word_7FA384;
short wlk_ID_pp; //номер предка цього об'єкта, використовується для трупів, щоб знати хто був убитий
char migrantNumPeopleCarried; //кількість жителів у візку мігранта
char mood; ///настрій
char byte_7FA389;
char byte_7FA3A3;
char byte_7FA370;
char ruler; //прапор для групи, що цей об'єкт є прикладом для руху 
char simpleDirection; //можна використовувати землю для руху (0 - дороги, 1 - земля і дороги)
char byte_7FA39A;
char byte_7FA3B9;
char at_dest_x; //прапор наближення до кінцевого тайлу
char at_dest_y;
short word_7FA3BA;
short word_7FA3BC;
char prevActionState; //попереднє стан обхекта
short destinationPathCurrent; //вибраний шлях для руху
};


(Building) Опис нерухомого об'єкта
struct Building
{
BuildingType type; //тип будівлі
int storageId; //номер складу (склад, комора, док)
int x; //розташування на карті
int y;

unsigned char inUse; //прапор активності
int house_crimeRisk; //поле використовується будинком, рівень невдоволення
int house_size; // (будинок) розмір в тайлах
int house_population; //(будинок) населення
int walkerServiceAccess; //наявність робочої сили (0-100)
int laborCategory; //клас будівлі (медицина, освіта і тд)
int word_94BDAC[2];
char byte_94BDB8;
int level_resourceId; //рівень дому або необхідні ресурси (фабрика)
int grow_value_house_foodstocks[8]; //(будинок) запаси товарів
short house_roomForPeople; //(будинок) число вільних місць 
short haveRomeroad; //доступність краю карти
short house_maxPopEver; //(будинок) максимум населення
short noContactWithRome; //час без доступу до дорозі
char enter_x; //точка входу
char enter_y;
short walkerId; //номер жителя/групи який асоційований з цим будинком
short laborSeekerId; //номер рекрутера, який обслуговує виробництво
short immigrantId; // (будинок) номер поселенця, який йде до цього будинку
short towerBallistaId; // (вежа) номер балісти, яка стоїть на вежі
char walkerSpawnDelay; // (виробництво) час між створенням об'єктів
char byte_94BD6C;
char hasFountain; // прапор розміщення фонтану поряд з будівлею 
char waterDep; // (фонтан, лазні) прапор доступності резервуара з водою
short warehouse_prevStorage; //(склад) попередній склад
short warehouse_nextStorage; //(склад) наступний склад (використовується торговцями)
short industry_unitsStored; // (фабрика) скільки матеріалів на складі
char house_hasWell; // (будинок) доступ до криниці
short num_workers; // (фабрика) скільки присутній робочих
short fireRisk; //ризик пожежі
short damageRisk; // ризик обвалення
short industry_outputGood; // (фабрика) скільки товарів на складі
short house_theater_amphi_wine; // рівень сервісу акторів
short house_amphiGlad_colo; //(будинок) рівень сервісу гладіаторів
short house_coloLion_hippo; // (будинок) рівень сервісу колісниць
short house_school_library; //(будинок)рівень доступності шкіл/бібліотек
short house_academy_barber; // (будинок) рівень доступності академій/перукаря
short granary_capacity[4]; // комору - запаси товарів
short house_wheat; // (будинок) запаси пшениці 
short gridOffset; // зміщення на карті міста (тайлах) 
short wharf_hasBoat_house_evolveStatusdesir; //рибацька пристань - скільки човен набрала риби/ будинок - прапор невдоволення
short house_pottery; // (будинок) кількість посуду
short house_oil; // (будинок) кількість масла
short house_furniture; // (будинок) кількість меблів
short house_wine; // (будинок) кількість вина
short house_vegetables; // (будинок) кількість овочів
short size; // розмір в тайлах
short formationId; // номер групи, яка приписана до цього будинку
short placedSequenceNumber; //номер шматочка в складних будівлях (форт, іподром) 
char byte_always0; //???
short cityId; //номер міста, до якого належить будинок (привіт з Цезар 2)
short workersEffectivity; //баф на ефективність виробництва
short burningRuinStep; // анімація для гарячих руїн
char house_bathhouse_dock_numships_entert_days; 
char byte_94BDBB;
char haveProblems; //номер проблеми з будівлею
char house_entertainment; // (будинок) якість розваг
char house_numGods; // (будинок) якість релігії
char house_education; // якість навчання
char house_clinic; // (будинок) якість охорони здоров'я
char house_hospital_entert_days2; //(патриції) скільки днів з останнього обслуговування хірургом
char house_mercury; //(будинок) прапор обслуговування богами
char house_neptune;
char house_mars;
char house_venus;
char byte_94BDB9;
char hasRoadAccess; //прапор доступу до дорозі
char haveRoadnet; //прапор доступності сенату
char house_isMerged; //прапор об'єднання із сусідніми будинками
char desirability; //якість території (-50 до + 100) 
char adjacentToWater; //знаходиться поруч з водою
char byte_94BD84;
char byte_94BD85;
char house_health; //рівень здоров'я вдома
char house_ceres; //
char house_taxcollector; //рівень обслуговування сборзиком податків
char byte_94BD7D;
};


(EmpireObject) Опис об'єкта на глобальній карті
struct EmpireObject
{
char inUse; //цей запис
char type; //тип (місто, торговець, межі, війська)
char currentAnimationIndex; //індекс анімації
__int16 xCoord; //розташування на карті
__int16 yCoord;
__int16 width;
__int16 height;
__int16 graphicID; //перша текстура
__int16 graphicID_exp; ///друга текстура
char distBattleTravelMonths; // (віддалена битва) через скільки місяців війська прийдуть у місто гравця
__int16 xCoord_exp; //координати для другої текстури
__int16 yCoord_exp; 
char cityType; //тип міста, римський, ворожий, віддалений
char cityNameId; //ім'я міста
char tradeRouteId; //номер торгового маршруту до міста гравця 
char tradeRouteOpen; //статус торгівлі 
__int16 tradeCostToOpen[10]; 
char citySells[16]; //які товари продає місто
char ownerCityIndex; //прапор, що це місто гравця 
char f990D29[10];
char cityBuys[16]; //які товари купує місто
char invasionPathId; //номер нападу
char invasionYears; //кількість років до нападу, використовується для повідомлень про напади
__int16 trade40; 
__int16 trade25;
__int16 trade15;
};


(TradeRoute) Опис торгового марщрута
struct TradeRoute
{
char inUse; //запис активна
char cityType; //початок маршруту
char cityNameId; //кінець маршруту
char routeId[16]; 
char isOpen; //маршрут відкрито 
char buysFlag[16]; //що купуємо
char sellsFlag[16]; //що продаємо
char sellsFlag_wine; //на маршруті продається якісне вино
__int16 costToOpen; //ціна за відкриття
__int16 unknown10; 
__int16 walkerEntryDelay; //затримка торгівлі в днях
__int16 unknown0; 
__int16 empireObjectId; //іконка торговця
char isSeaTrade; //прапор морської торгівлі, використовується для вибору текстур відтворення 
__int16 walkerId1; //ідентифікатори торговців
__int16 walkerId2;
__int16 walkerId3;
int quotas[16]; //квоти на товари
};


Масиви об'єктів
Walker walkers[1000]; //рухомі об'єкти на мапі міста
Building buildings[2000]; //будівлі на карті міста 
Formation formations[50]; //групи об'єктів
EmpireObject empireObjects[100]; //об'єкти на карті імперії
ModelHouse model_houses[20]; //характеристики будинку
Storage storages[200]; //параметри комор і складів
TradeRoute tradeRoutes[200]; //торговельні маршрути
CityInfo city_inform[8]; //привіт з Цезар 2 (там можна було керувати кількома провінціями) 



Подяки
На хвилі хабраэффекта останньої статті про математичної моделі міста гра отримала «зелене світло» в стиме , також наша команда отримала велику кількість пожертвувань на IndieGoGo.com. Величезна подяка хабрасообществу за підтримку ремейка.

Порадившись з шанувальниками гри Caesar III, ми вирішили призупинити внесення змін і зосередитися на виправлення багів і балансу, щоб гілка v0.4 виглядала і гралася як оригінал. З помітних змін залишилося тільки масштабування карти і деяка невідповідність логіки окремих юнітів.

До ремейку приєднався художник Дмитро Плотніков, який малює новий арт

Велике спасибі SkidanovAlex, Ununtrium, Bick і всім хто допоміг з перекладом тексту на indiegogo.
Окрема подяка MennyCalavera і Anastasia Smolskaya за переклади статей 1 і 2.

Подивитися зміни і додаткову інформацію по ремейку можна на сторінці проекту. Там же можна скачати останні складання і написати нам про знайдені баги.



Спасибі за приділений час

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.