If I were asked which part of the technical implementation of Caesar interests me most, I'd recall the calculation of a single “day” of city life. The individual components of the city's mathematical model are interesting in their own right, but those “cogs” only spin when assembled together. Most of the game runs inside the “game loop”, where component parameters are computed, game objects move, and new events and objects are created. If you'd like to know how the city simulation worked in one of the best games of 1998 — read on.
The authors split the computation of one “day” into several steps, the main ones listed below; simplified code of the function itself is under the spoiler:
Every 50 ticks a new day begins (16 days in a game month), for which several more functions are computed that don't need such frequent processing, namely:
on the 1st and 8th day of the month, citizen happiness and the city's crime level are calculated; on a month change the game checks whether a festival can be held, pays taxes and wages, computes the probability of random events and much more; on a year change it stores the past year's parameters and updates ratings.
City parameter steps. 1. Mood: gods, natives. 2. Update Caesar's invasion parameters. 3. Move groups of objects. 4. Gather granary info. 5. Update available services for houses. 6. Update warehouse parameters. 7. Update the population advisor and Rome's wheat supply. 8. Update goods consumption in workshops and raw materials in extractive industries. 9. Update paths to docks. 10. Compute goods production. 11. Road accessibility to Rome. 12. Update house populations. 13. Homeless from overcrowded houses. 14. Distribute workers, count the unemployed and active enterprises. 15. Fountain/reservoir coverage. 16. Water access for houses. 17. State of object groups. 18. Spawn citizens from service buildings. 19. Spawn traders. 20. Count building types and culture coverage. 21. Distribute the treasury between senate and forums. 22. Decay of culture in houses. 23. Decay of services in houses. 24. Building influence on land desirability. 25. Update house levels. 26. Remove demolished buildings. 27. Burning-ruin parameters. 28. Status of buildings around fires. 29. Create protesters. 30. Tax-collection parameters. 31. Entertainment level in houses.
Show code — Game-day steps (code)
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; }Religion
At first the Romans were pagans, worshipping Greek and, to a lesser extent, Etruscan gods. The game heavily exaggerates the notion of a deity's favour, reducing it to three states — cursed / neutral / blessed — but penalties and bonuses, along with the randomness in choosing a deity, make the computations less predictable.
Show code — calculateGodHappiness
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); } } } } } }City sentiment
The residents react to the wage gap between the city and Rome, the variety of food, the tax level and the number of slums. This parameter is stored per house and doesn't change depending on neighbouring houses.
Show code — calculateCityHappinessAndCrime
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; }Festivals
A significant boost to city sentiment comes only from the first festival in a full 12 months; the second and later ones give only half. This prevents a rich city from raising sentiment through festivals alone. Preparing for a festival also takes time, which limits how many can be held per year.
Show code — holdFestival
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; }Tribute to the emperor
Tribute to the emperor. The sum the treasury must pay at year's end depends on the city's profit and population. The first factor means paying a quarter of the year's earnings, but no less than an amount tied to the current population. If the city can't pay, the ruler pays with a drop in the emperor's favour — and past years of non-payment count too, so resentment accumulates over long arrears.
Show code — calculateAndPayTribute
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; }Random events
Random events. The game has 7 random events; a set of flags editable in the mission editor controls their occurrence. An event fires if the generator rolls its type and it's allowed in the scenario. The developers made: lowering/raising wages in Rome, sea or land trade problems, poisoning of wells, mine collapses and flooding of quarries and clay pits. An earthquake is set in the editor, has a time and origin point, and spreads randomly in four directions.
Show code — handleRandomEvents
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; } } }Citizens' health
Practising doctors appeared in Rome relatively late. In the game, clinics and hospitals provide the same services, but hospitals are also needed by high-level houses to keep growing. Citizens' health is a key indicator of prosperity: epidemics can mow down whole districts, and as the population grows the number of deaths in an epidemic only rises.
Show code — calculateHealthRate
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); } } }Tax collection
In the early days the city lives off incoming taxes, modest as they are. The tax collector, like the doctor from a clinic, must pass near residential houses periodically. If a house is regularly visited by tax collectors, it pays tax every month.
How much a house pays per month depends on:
— the rate set by the finance advisor (0 to 25%); — the number of people living in the house at the moment of collection (i.e. at the month change); — the house's development level (in c3_model.txt, the 20th column is the 200% monthly tax per person at that house level).
Analysing the function, you can conclude that gradually raising taxes is no different from changing them quickly. Taxes affect your residents' mood, but there's no point paying your workers more than 8 units above Rome's wage.
What is a “200% tax”? It's (probably) the developers' wish to save on a round-down-to-integer operation, in this block:
collectedPatricians = adjustWithPercentage( cityinfo.monthlyCollectedTaxFromPatricians / 2, cityinfo.taxpercentage );
the collected taxes are divided by 2. To avoid a house paying more than it should, a tax × 2 was introduced, so the computation always yields a value less than or equal to the correct one.
At higher city development levels, the city can live entirely off taxes:
Show code — collectMonthlyTaxes
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 ); }Food consumption
People eat X amount of food regardless of how many food types they have. The amount eaten depends only on the number of people in the house (10 people eat 5 units per month). For example: a fully populated level-20 house has 200 residents who need 3 food types. Per month they eat 200/10×5 = 100 units, split among the three required types, most likely evenly — about 100/3 = 33 each.
Show code — housesConsumeMonthlyFood
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; }Goods production
Sometimes the city needs to import materials to produce goods. Eight workshops run stably off one warehouse, two more intermittently: imports depend on the number of workshops and on how much material the trade partner can supply. Exports can be tuned numerically, while raw materials are imported in proportion to workshop demand. So with few workshops the warehouse simply won't stockpile raw materials.
Prosperity rating
The prosperity rating is the “heaviest” to raise: any misstep by the ruler lowers it, while the maximum gain per step is 2 points — so, following all the rules, a city reaches 50 points in at least 25 years, not counting penalties and bonuses.
Show code — updateProsperityRating
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(); }Game data structures
Caesar III operates only on static arrays, so the number of buildings, people, objects and groups is known in advance. For example, buildings and displayed citizens can't exceed 2000, and object groups in the city (wolves, sheep, legions, protesters) don't exceed 50. These hard limits were imposed by the need to work within under 32 MB of RAM, half of which was taken by the texture archive. Below are the structures with the fields whose meaning I managed to restore.
Show code — Game data structures (Walker / Building / EmpireObject / TradeRoute)
(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 (там можно было управлять несколькими провинциями)Acknowledgements
On the wave of the last article's traffic, the game got a “green light” on Steam, and our team received many donations on IndieGoGo.com. Huge thanks to the community for supporting the remake.
After consulting with Caesar III fans, we decided to pause adding changes and focus on fixing bugs and balance, so the v0.4 branch would look and play like the original. The notable remaining changes are map scaling and some inconsistencies in individual units' logic.
Artist Dmitry Plotnikov, who draws new art, joined the remake.
Thanks to SkidanovAlex, Ununtrium, Bick and everyone who helped translate the text on indiegogo. Many thanks to MennyCalavera for support and help promoting the remake. Special thanks to Anastasia Smolskaya for translating articles 1 and 2.
You can see the changes and more info about the game on the project page, where you can also download the latest builds and report bugs.
P.S. This article wouldn't have been possible without the help of Bianca van Schaik, who spent a great deal of time and effort restoring the original's sources.