Caesar III: game loop

Если бы меня спросили какая часть технической реализации игры Цезарь мне интересна больше других, я бы вспомнил о расчете одного «дня» городской жизни. Отдельные компоненты математической модели города тоже интересны в реализации, но эти «шестеренки» будут крутиться только в сборе. Большая часть игры проходит внутри «игрового цикла», в котором проводятся вычисления параметров компонентов, выполняются перемещения игровых объектов, создаются новые события и объекты, если вам интересно узнать как была устроена симуляция города в одной из лучших игр 1998 года — добро пожаловать под кат. Описания, псевдокод и схемы помогут Вам лучше узнать об используемых алгоритмах.1d4e4285872b4171bb9679b453ae74a6.jpgОбсчет одного «дня» в городе авторы игры разбили на несколько шагов, основные из которых приведены ниже, упрощенный код самой функции можно найти под спойлером:

cd162496af16993577ca5b949eac0b9f.png

Каждые 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; } Религия fb7e81576b00967bce347e1688f33edf.jpg

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

5bc578a5f1253d008a3a6a3b5020b494.jpg

Расчет настроения богов и условия гнева\благословения 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); } } } } } } Настроения в городе 9371120f538748e8bd4b01b9e5a1866c.png

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

b4f5c3692af1472f97a0e6ba3668e23d.jpeg

Расчет настроения и степени миграции в городе 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; } Фестивали 8dc8a9c8a8ed4a51935f3e86a2490cf4.jpg

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

b2f5937b1ee84554b6b513973196deb9.jpeg

Расчет влияния фестиваля на настроение в городе 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; } Выплата подати императору f26d811c569f4f86aa772d9c4d805828.jpeg

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

0f8d9c0c02fe4d80bed27d58be9cc2a2.jpeg

Расчет имперской четвертины за последний год 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; } Случайные события 8c11d134212a48d392509f9187444c44.png

Случайные события. В игре доступно 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; } } } Здоровье жителей a7c0a0426f344890aa90cc839e000f48.jpg24036d9279c246a286f10a91b590dcb7.jpg

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

f4e10ec206464309a4cb656314499026.jpeg

Вычисление здоровья и вероятности эпидемии в городе 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 & 0×3F; 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); } }

} Сбор налогов c199d47d0cca4759a8dd91d6a46b51bb.jpg

На первых порах город живет за счет поступающих налогов, хотя они достаточно скромные. Сборщик налогов должен так же как например, доктор из клиники, проходить рядом с жилыми домами с определенной периодичностью. Если жилой дом бесперебойно посещается сборщиками налогов, то он каждый месяц будет платить налог.

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

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

Что такое 200-процентный налог, это (вероятно) желание разработчиков сэкономить на операции приведения к меньшему целому, в этом блоке

collectedPatricians = adjustWithPercentage ( cityinfo.monthlyCollectedTaxFromPatricians / 2, cityinfo.taxpercentage); кода видно, что собранные налоги делятся на 2. Чтобы не получить ситуацию, когда дом может заплатить больше положенного был введен налог * 2, а при вычислениях мы всегда будем получать значение меньшее или равное правильному.

На более высоких уровнях развития города, он вполне может жить только за счет налогов171ec84ad63e414c926d43e432e5b54e.jpg80399520bebc49138cfe2aa52ca6563e.jpg

af25e99f422243d8942685286a51e93c.jpeg

Вычисление полученных за месяц налогов 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); } Потребление продуктов f3f5ed2a70bb497182b298b1e598facd.jpg

Люди едят Х количества пищи, независимо от того сколько у них видов еды. Съедаемое количество еды зависит только от количества человек проживаемых в доме (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; } Производство товаров ba60b47efd66449ebd9cc3b1a06f6ce0.png

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

Подсчет рейтинга благосостояния ccf0b683430345a08719705f13a640e9.jpgРейтинг благосостояния в игре является самым «тяжелым» для подъема, с одной стороны любой проступок со стороны правителя ведет к его снижению, с другой — максимальный прирост уровня составляет 2 пункта, т.е. при соблюдении всех правил отметку в 50 пунктов город достигнет минимум через 25 лет, без учета штрафов и премий.38025148673a4000bc4242ef08d2f481.jpeg

Подсчет уровня благосостояния города 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 are below 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 (); } Структуры данных игры 9c5467a7ff2a4ab29acc2fc7a09194d6.jpg

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

© Habrahabr.ru