1
0
mirror of https://github.com/vcmi/vcmi.git synced 2024-12-16 10:19:47 +02:00
vcmi/server/processors/NewTurnProcessor.cpp
Ivan Savenko 3dd4fa2528 Reduce usage of pointers to VLC entities
Final goal (of multiple PR's) is to remove all remaining pointers from
serializeable game state, and replace them with either identifiers or
with shared/unique pointers.

CGTownInstance::town and CGHeroInstance::type members have been removed.
Now this data is computed dynamically using subID member.

VLC entity of a town can now be accessed via following methods:
- getFactionID() returns ID of a faction
- getFaction() returns pointer to a faction
- getTown() returns pointer to a town

VLC entity of a hero can now be accessed via following methods:
- getHeroTypeID() returns ID of a hero
- getHeroClassID() returns ID of a hero class
- getHeroType() returns pointer to a hero
- getHeroClass() returns pointer to a hero class
2024-10-10 12:28:08 +00:00

686 lines
21 KiB
C++

/*
* NewTurnProcessor.cpp, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#include "StdInc.h"
#include "NewTurnProcessor.h"
#include "HeroPoolProcessor.h"
#include "../CGameHandler.h"
#include "../../lib/CPlayerState.h"
#include "../../lib/IGameSettings.h"
#include "../../lib/StartInfo.h"
#include "../../lib/TerrainHandler.h"
#include "../../lib/entities/building/CBuilding.h"
#include "../../lib/entities/faction/CTownHandler.h"
#include "../../lib/gameState/CGameState.h"
#include "../../lib/gameState/SThievesGuildInfo.h"
#include "../../lib/mapObjects/CGHeroInstance.h"
#include "../../lib/mapObjects/CGTownInstance.h"
#include "../../lib/mapObjects/IOwnableObject.h"
#include "../../lib/mapping/CMap.h"
#include "../../lib/networkPacks/PacksForClient.h"
#include "../../lib/networkPacks/StackLocation.h"
#include "../../lib/pathfinder/TurnInfo.h"
#include "../../lib/texts/CGeneralTextHandler.h"
#include <vstd/RNG.h>
NewTurnProcessor::NewTurnProcessor(CGameHandler * gameHandler)
:gameHandler(gameHandler)
{
}
void NewTurnProcessor::handleTimeEvents(PlayerColor color)
{
for (auto const & event : gameHandler->gameState()->map->events)
{
if (!event.occursToday(gameHandler->gameState()->day))
continue;
if (!event.affectsPlayer(color, gameHandler->getPlayerState(color)->isHuman()))
continue;
InfoWindow iw;
iw.player = color;
iw.text = event.message;
//give resources
if (!event.resources.empty())
{
gameHandler->giveResources(color, event.resources);
for (GameResID i : GameResID::ALL_RESOURCES())
if (event.resources[i])
iw.components.emplace_back(ComponentType::RESOURCE, i, event.resources[i]);
}
gameHandler->sendAndApply(iw); //show dialog
}
}
void NewTurnProcessor::handleTownEvents(const CGTownInstance * town)
{
for (auto const & event : town->events)
{
if (!event.occursToday(gameHandler->gameState()->day))
continue;
PlayerColor player = town->getOwner();
if (!event.affectsPlayer(player, gameHandler->getPlayerState(player)->isHuman()))
continue;
// dialog
InfoWindow iw;
iw.player = player;
iw.text = event.message;
if (event.resources.nonZero())
{
gameHandler->giveResources(player, event.resources);
for (GameResID i : GameResID::ALL_RESOURCES())
if (event.resources[i])
iw.components.emplace_back(ComponentType::RESOURCE, i, event.resources[i]);
}
for (auto & i : event.buildings)
{
// Only perform action if:
// 1. Building exists in town (don't attempt to build Lvl 5 guild in Fortress
// 2. Building was not built yet
// othervice, silently ignore / skip it
if (town->getTown()->buildings.count(i) && !town->hasBuilt(i))
{
gameHandler->buildStructure(town->id, i, true);
iw.components.emplace_back(ComponentType::BUILDING, BuildingTypeUniqueID(town->getFactionID(), i));
}
}
if (!event.creatures.empty())
{
SetAvailableCreatures sac;
sac.tid = town->id;
sac.creatures = town->creatures;
for (si32 i=0;i<event.creatures.size();i++) //creature growths
{
if (i < town->creatures.size() && !town->creatures.at(i).second.empty() && event.creatures.at(i) > 0)//there is dwelling
{
sac.creatures[i].first += event.creatures.at(i);
iw.components.emplace_back(ComponentType::CREATURE, town->creatures.at(i).second.back(), event.creatures.at(i));
}
}
}
gameHandler->sendAndApply(iw); //show dialog
}
}
void NewTurnProcessor::onPlayerTurnStarted(PlayerColor which)
{
const auto * playerState = gameHandler->gameState()->getPlayerState(which);
handleTimeEvents(which);
for (const auto * t : playerState->getTowns())
handleTownEvents(t);
for (const auto * t : playerState->getTowns())
{
//garrison hero first - consistent with original H3 Mana Vortex and Battle Scholar Academy levelup windows order
if (t->garrisonHero != nullptr)
gameHandler->objectVisited(t, t->garrisonHero);
if (t->visitingHero != nullptr)
gameHandler->objectVisited(t, t->visitingHero);
}
}
void NewTurnProcessor::onPlayerTurnEnded(PlayerColor which)
{
const auto * playerState = gameHandler->gameState()->getPlayerState(which);
assert(playerState->status == EPlayerStatus::INGAME);
if (playerState->getTowns().empty())
{
DaysWithoutTown pack;
pack.player = which;
pack.daysWithoutCastle = playerState->daysWithoutCastle.value_or(0) + 1;
gameHandler->sendAndApply(pack);
}
else
{
if (playerState->daysWithoutCastle.has_value())
{
DaysWithoutTown pack;
pack.player = which;
pack.daysWithoutCastle = std::nullopt;
gameHandler->sendAndApply(pack);
}
}
// check for 7 days without castle
gameHandler->checkVictoryLossConditionsForPlayer(which);
bool newWeek = gameHandler->getDate(Date::DAY_OF_WEEK) == 7; // end of 7th day
if (newWeek) //new heroes in tavern
gameHandler->heroPool->onNewWeek(which);
}
ResourceSet NewTurnProcessor::generatePlayerIncome(PlayerColor playerID, bool newWeek)
{
const auto & playerSettings = gameHandler->getPlayerSettings(playerID);
const PlayerState & state = gameHandler->gameState()->players.at(playerID);
ResourceSet income;
for (const auto & town : state.getTowns())
{
if (newWeek && town->hasBuilt(BuildingSubID::TREASURY))
{
//give 10% of starting gold
income[EGameResID::GOLD] += state.resources[EGameResID::GOLD] / 10;
}
//give resources if there's a Mystic Pond
if (newWeek && town->hasBuilt(BuildingSubID::MYSTIC_POND))
{
static constexpr std::array rareResources = {
GameResID::MERCURY,
GameResID::SULFUR,
GameResID::CRYSTAL,
GameResID::GEMS
};
auto resID = *RandomGeneratorUtil::nextItem(rareResources, gameHandler->getRandomGenerator());
int resVal = gameHandler->getRandomGenerator().nextInt(1, 4);
income[resID] += resVal;
gameHandler->setObjPropertyValue(town->id, ObjProperty::BONUS_VALUE_FIRST, resID);
gameHandler->setObjPropertyValue(town->id, ObjProperty::BONUS_VALUE_SECOND, resVal);
}
}
for (GameResID k = GameResID::WOOD; k < GameResID::COUNT; k++)
{
income += state.valOfBonuses(BonusType::RESOURCES_CONSTANT_BOOST, BonusSubtypeID(k));
income += state.valOfBonuses(BonusType::RESOURCES_TOWN_MULTIPLYING_BOOST, BonusSubtypeID(k)) * state.getTowns().size();
}
if(newWeek) //weekly crystal generation if 1 or more crystal dragons in any hero army or town garrison
{
bool hasCrystalGenCreature = false;
for (const auto & hero : state.getHeroes())
for(auto stack : hero->stacks)
if(stack.second->hasBonusOfType(BonusType::SPECIAL_CRYSTAL_GENERATION))
hasCrystalGenCreature = true;
for(const auto & town : state.getTowns())
for(auto stack : town->stacks)
if(stack.second->hasBonusOfType(BonusType::SPECIAL_CRYSTAL_GENERATION))
hasCrystalGenCreature = true;
if(hasCrystalGenCreature)
income[EGameResID::CRYSTAL] += 3;
}
TResources incomeHandicapped = income;
incomeHandicapped.applyHandicap(playerSettings->handicap.percentIncome);
for (auto obj : state.getOwnedObjects())
incomeHandicapped += obj->asOwnable()->dailyIncome();
return incomeHandicapped;
}
SetAvailableCreatures NewTurnProcessor::generateTownGrowth(const CGTownInstance * t, EWeekType weekType, CreatureID creatureWeek, bool firstDay)
{
SetAvailableCreatures sac;
PlayerColor player = t->tempOwner;
sac.tid = t->id;
sac.creatures = t->creatures;
for (int k=0; k < t->getTown()->creatures.size(); k++)
{
if (t->creatures.at(k).second.empty())
continue;
uint32_t creaturesBefore = t->creatures.at(k).first;
uint32_t creatureGrowth = 0;
const CCreature *cre = t->creatures.at(k).second.back().toCreature();
if (firstDay)
{
creatureGrowth = cre->getGrowth();
}
else
{
creatureGrowth = t->creatureGrowth(k);
//Deity of fire week - upgrade both imps and upgrades
if (weekType == EWeekType::DEITYOFFIRE && vstd::contains(t->creatures.at(k).second, creatureWeek))
creatureGrowth += 15;
//bonus week, effect applies only to identical creatures
if (weekType == EWeekType::BONUS_GROWTH && cre->getId() == creatureWeek)
creatureGrowth += 5;
}
// Neutral towns have halved creature growth
if (!player.isValidPlayer())
creatureGrowth /= 2;
uint32_t resultingCreatures = 0;
if (weekType == EWeekType::PLAGUE)
resultingCreatures = creaturesBefore / 2;
else if (weekType == EWeekType::DOUBLE_GROWTH && vstd::contains(t->creatures.at(k).second, creatureWeek))
resultingCreatures = (creaturesBefore + creatureGrowth) * 2;
else
resultingCreatures = creaturesBefore + creatureGrowth;
sac.creatures.at(k).first = resultingCreatures;
}
return sac;
}
void NewTurnProcessor::updateNeutralTownGarrison(const CGTownInstance * t, int currentWeek) const
{
assert(t);
assert(!t->getOwner().isValidPlayer());
constexpr int randomRollsCounts = 3; // H3 makes around 3 random rolls to make simple bell curve distribution
constexpr int upgradeChance = 5; // Chance for a unit to get an upgrade
constexpr int growthChanceFort = 80; // Chance for growth to occur in towns with fort built
constexpr int growthChanceVillage = 40; // Chance for growth to occur in towns without fort
const auto & takeFromAvailable = [this, t](CreatureID creatureID)
{
int tierToSubstract = -1;
for (int i = 0; i < t->getTown()->creatures.size(); ++i)
if (vstd::contains(t->getTown()->creatures[i], creatureID))
tierToSubstract = i;
if (tierToSubstract == -1)
return; // impossible?
int creaturesAvailable = t->creatures[tierToSubstract].first;
int creaturesRecruited = creatureID.toCreature()->getGrowth();
int creaturesLeft = std::max(0, creaturesAvailable - creaturesRecruited);
if (creaturesLeft != creaturesAvailable)
{
SetAvailableCreatures sac;
sac.tid = t->id;
sac.creatures = t->creatures;
sac.creatures[tierToSubstract].first = creaturesLeft;
gameHandler->sendAndApply(sac);
}
};
int growthChance = t->hasFort() ? growthChanceFort : growthChanceVillage;
int growthRoll = gameHandler->getRandomGenerator().nextInt(0, 99);
if (growthRoll >= growthChance)
return;
int tierRoll = 0;
for(int i = 0; i < randomRollsCounts; ++i)
tierRoll += gameHandler->getRandomGenerator().nextInt(0, currentWeek);
// NOTE: determined by observing H3 games, might not match H3 100%
int tierToGrow = std::clamp(tierRoll / randomRollsCounts, 0, 6) + 1;
bool upgradeUnit = gameHandler->getRandomGenerator().nextInt(0, 99) < upgradeChance;
// Check if town garrison already has unit of specified tier
for(const auto & slot : t->Slots())
{
const auto * creature = slot.second->type;
if (creature->getFactionID() != t->getFactionID())
continue;
if (creature->getLevel() != tierToGrow)
continue;
StackLocation stackLocation(t, slot.first);
gameHandler->changeStackCount(stackLocation, creature->getGrowth(), false);
takeFromAvailable(creature->getGrowth());
if (upgradeUnit && !creature->upgrades.empty())
{
CreatureID upgraded = *RandomGeneratorUtil::nextItem(creature->upgrades, gameHandler->getRandomGenerator());
gameHandler->changeStackType(stackLocation, upgraded.toCreature());
}
else
gameHandler->changeStackType(stackLocation, creature);
return;
}
// No existing creatures in garrison, but we have a free slot we can use
SlotID freeSlotID = t->getFreeSlot();
if (freeSlotID.validSlot())
{
for (auto const & tierVector : t->getTown()->creatures)
{
CreatureID baseCreature = tierVector.at(0);
if (baseCreature.toEntity(VLC)->getLevel() != tierToGrow)
continue;
StackLocation stackLocation(t, freeSlotID);
if (upgradeUnit && !baseCreature.toCreature()->upgrades.empty())
{
CreatureID upgraded = *RandomGeneratorUtil::nextItem(baseCreature.toCreature()->upgrades, gameHandler->getRandomGenerator());
gameHandler->insertNewStack(stackLocation, upgraded.toCreature(), upgraded.toCreature()->getGrowth());
takeFromAvailable(upgraded.toCreature()->getGrowth());
}
else
{
gameHandler->insertNewStack(stackLocation, baseCreature.toCreature(), baseCreature.toCreature()->getGrowth());
takeFromAvailable(baseCreature.toCreature()->getGrowth());
}
return;
}
}
}
RumorState NewTurnProcessor::pickNewRumor()
{
RumorState newRumor;
static const std::vector<RumorState::ERumorType> rumorTypes = {RumorState::TYPE_MAP, RumorState::TYPE_SPECIAL, RumorState::TYPE_RAND, RumorState::TYPE_RAND};
std::vector<RumorState::ERumorTypeSpecial> sRumorTypes = {
RumorState::RUMOR_OBELISKS, RumorState::RUMOR_ARTIFACTS, RumorState::RUMOR_ARMY, RumorState::RUMOR_INCOME};
if(gameHandler->gameState()->map->grailPos.valid()) // Grail should always be on map, but I had related crash I didn't manage to reproduce
sRumorTypes.push_back(RumorState::RUMOR_GRAIL);
int rumorId = -1;
int rumorExtra = -1;
auto & rand = gameHandler->getRandomGenerator();
newRumor.type = *RandomGeneratorUtil::nextItem(rumorTypes, rand);
do
{
switch(newRumor.type)
{
case RumorState::TYPE_SPECIAL:
{
SThievesGuildInfo tgi;
gameHandler->gameState()->obtainPlayersStats(tgi, 20);
rumorId = *RandomGeneratorUtil::nextItem(sRumorTypes, rand);
if(rumorId == RumorState::RUMOR_GRAIL)
{
rumorExtra = gameHandler->gameState()->getTile(gameHandler->gameState()->map->grailPos)->terType->getIndex();
break;
}
std::vector<PlayerColor> players = {};
switch(rumorId)
{
case RumorState::RUMOR_OBELISKS:
players = tgi.obelisks[0];
break;
case RumorState::RUMOR_ARTIFACTS:
players = tgi.artifacts[0];
break;
case RumorState::RUMOR_ARMY:
players = tgi.army[0];
break;
case RumorState::RUMOR_INCOME:
players = tgi.income[0];
break;
}
rumorExtra = RandomGeneratorUtil::nextItem(players, rand)->getNum();
break;
}
case RumorState::TYPE_MAP:
// Makes sure that map rumors only used if there enough rumors too choose from
if(!gameHandler->gameState()->map->rumors.empty() && (gameHandler->gameState()->map->rumors.size() > 1 || !gameHandler->gameState()->currentRumor.last.count(RumorState::TYPE_MAP)))
{
rumorId = rand.nextInt(gameHandler->gameState()->map->rumors.size() - 1);
break;
}
else
newRumor.type = RumorState::TYPE_RAND;
[[fallthrough]];
case RumorState::TYPE_RAND:
auto vector = VLC->generaltexth->findStringsWithPrefix("core.randtvrn");
rumorId = rand.nextInt((int)vector.size() - 1);
break;
}
}
while(!newRumor.update(rumorId, rumorExtra));
return newRumor;
}
std::tuple<EWeekType, CreatureID> NewTurnProcessor::pickWeekType(bool newMonth)
{
for (const CGTownInstance *t : gameHandler->gameState()->map->towns)
{
if (t->hasBuilt(BuildingID::GRAIL, ETownType::INFERNO))
return { EWeekType::DEITYOFFIRE, CreatureID::IMP };
}
if(!gameHandler->getSettings().getBoolean(EGameSettings::CREATURES_ALLOW_RANDOM_SPECIAL_WEEKS))
return { EWeekType::NORMAL, CreatureID::NONE};
int monthType = gameHandler->getRandomGenerator().nextInt(99);
if (newMonth) //new month
{
if (monthType < 40) //double growth
{
if (gameHandler->getSettings().getBoolean(EGameSettings::CREATURES_ALLOW_ALL_FOR_DOUBLE_MONTH))
{
CreatureID creatureID = VLC->creh->pickRandomMonster(gameHandler->getRandomGenerator());
return { EWeekType::DOUBLE_GROWTH, creatureID};
}
else if (VLC->creh->doubledCreatures.size())
{
CreatureID creatureID = *RandomGeneratorUtil::nextItem(VLC->creh->doubledCreatures, gameHandler->getRandomGenerator());
return { EWeekType::DOUBLE_GROWTH, creatureID};
}
else
{
gameHandler->complain("Cannot find creature that can be spawned!");
return { EWeekType::NORMAL, CreatureID::NONE};
}
}
if (monthType < 50)
return { EWeekType::PLAGUE, CreatureID::NONE};
return { EWeekType::NORMAL, CreatureID::NONE};
}
else //it's a week, but not full month
{
if (monthType < 25)
{
std::pair<int, CreatureID> newMonster(54, CreatureID());
do
{
newMonster.second = VLC->creh->pickRandomMonster(gameHandler->getRandomGenerator());
} while (VLC->creh->objects[newMonster.second] &&
(*VLC->townh)[VLC->creatures()->getById(newMonster.second)->getFactionID()]->town == nullptr); // find first non neutral creature
return { EWeekType::BONUS_GROWTH, newMonster.second};
}
return { EWeekType::NORMAL, CreatureID::NONE};
}
}
std::vector<SetMana> NewTurnProcessor::updateHeroesManaPoints()
{
std::vector<SetMana> result;
for (auto & elem : gameHandler->gameState()->players)
{
for (CGHeroInstance *h : elem.second.getHeroes())
{
int32_t newMana = h->getManaNewTurn();
if (newMana != h->mana)
result.emplace_back(h->id, newMana, true);
}
}
return result;
}
std::vector<SetMovePoints> NewTurnProcessor::updateHeroesMovementPoints()
{
std::vector<SetMovePoints> result;
for (auto & elem : gameHandler->gameState()->players)
{
for (CGHeroInstance *h : elem.second.getHeroes())
{
auto ti = std::make_unique<TurnInfo>(h, 1);
// NOTE: this code executed when bonuses of previous day not yet updated (this happen in NewTurn::applyGs). See issue 2356
int32_t newMovementPoints = h->movementPointsLimitCached(gameHandler->gameState()->map->getTile(h->visitablePos()).terType->isLand(), ti.get());
if (newMovementPoints != h->movementPointsRemaining())
result.emplace_back(h->id, newMovementPoints, true);
}
}
return result;
}
InfoWindow NewTurnProcessor::createInfoWindow(EWeekType weekType, CreatureID creatureWeek, bool newMonth)
{
InfoWindow iw;
switch (weekType)
{
case EWeekType::DOUBLE_GROWTH:
iw.text.appendLocalString(EMetaText::ARRAY_TXT, 131);
iw.text.replaceNameSingular(creatureWeek);
iw.text.replaceNameSingular(creatureWeek);
break;
case EWeekType::PLAGUE:
iw.text.appendLocalString(EMetaText::ARRAY_TXT, 132);
break;
case EWeekType::BONUS_GROWTH:
iw.text.appendLocalString(EMetaText::ARRAY_TXT, 134);
iw.text.replaceNameSingular(creatureWeek);
iw.text.replaceNameSingular(creatureWeek);
break;
case EWeekType::DEITYOFFIRE:
iw.text.appendLocalString(EMetaText::ARRAY_TXT, 135);
iw.text.replaceNameSingular(CreatureID::IMP); //%s imp
iw.text.replaceNameSingular(CreatureID::IMP); //%s imp
iw.text.replacePositiveNumber(15);//%+d 15
iw.text.replaceNameSingular(CreatureID::FAMILIAR); //%s familiar
iw.text.replacePositiveNumber(15);//%+d 15
break;
default:
if (newMonth)
{
iw.text.appendLocalString(EMetaText::ARRAY_TXT, (130));
iw.text.replaceLocalString(EMetaText::ARRAY_TXT, gameHandler->getRandomGenerator().nextInt(32, 41));
}
else
{
iw.text.appendLocalString(EMetaText::ARRAY_TXT, (133));
iw.text.replaceLocalString(EMetaText::ARRAY_TXT, gameHandler->getRandomGenerator().nextInt(43, 57));
}
}
return iw;
}
NewTurn NewTurnProcessor::generateNewTurnPack()
{
NewTurn n;
n.specialWeek = EWeekType::FIRST_WEEK;
n.creatureid = CreatureID::NONE;
n.day = gameHandler->gameState()->day + 1;
bool firstTurn = !gameHandler->getDate(Date::DAY);
bool newWeek = gameHandler->getDate(Date::DAY_OF_WEEK) == 7; //day numbers are confusing, as day was not yet switched
bool newMonth = gameHandler->getDate(Date::DAY_OF_MONTH) == 28;
if (!firstTurn)
{
for (const auto & player : gameHandler->gameState()->players)
n.playerIncome[player.first] = generatePlayerIncome(player.first, newWeek);
}
if (newWeek && !firstTurn)
{
auto [specialWeek, creatureID] = pickWeekType(newMonth);
n.specialWeek = specialWeek;
n.creatureid = creatureID;
}
n.heroesMana = updateHeroesManaPoints();
n.heroesMovement = updateHeroesMovementPoints();
if (newWeek)
{
for (CGTownInstance *t : gameHandler->gameState()->map->towns)
n.availableCreatures.push_back(generateTownGrowth(t, n.specialWeek, n.creatureid, firstTurn));
}
if (newWeek)
n.newRumor = pickNewRumor();
if (newWeek)
{
//new week info popup
if (n.specialWeek != EWeekType::FIRST_WEEK)
n.newWeekNotification = createInfoWindow(n.specialWeek, n.creatureid, newMonth);
}
return n;
}
void NewTurnProcessor::onNewTurn()
{
NewTurn n = generateNewTurnPack();
bool firstTurn = !gameHandler->getDate(Date::DAY);
bool newWeek = gameHandler->getDate(Date::DAY_OF_WEEK) == 7; //day numbers are confusing, as day was not yet switched
bool newMonth = gameHandler->getDate(Date::DAY_OF_MONTH) == 28;
gameHandler->sendAndApply(n);
if (newWeek)
{
for (CGTownInstance *t : gameHandler->gameState()->map->towns)
if (t->hasBuilt(BuildingSubID::PORTAL_OF_SUMMONING))
gameHandler->setPortalDwelling(t, true, (n.specialWeek == EWeekType::PLAGUE ? true : false)); //set creatures for Portal of Summoning
}
if (newWeek && !firstTurn)
{
for (CGTownInstance *t : gameHandler->gameState()->map->towns)
{
if (!t->getOwner().isValidPlayer())
updateNeutralTownGarrison(t, 1 + gameHandler->getDate(Date::DAY) / 7);
}
}
//spawn wandering monsters
if (newMonth && (n.specialWeek == EWeekType::DOUBLE_GROWTH || n.specialWeek == EWeekType::DEITYOFFIRE))
{
gameHandler->spawnWanderingMonsters(n.creatureid);
}
logGlobal->trace("Info about turn %d has been sent!", n.day);
}