help with ranged bombardment

pandamoose

Chieftain
Joined
Jan 5, 2013
Messages
31
Hello forums,

I'm wondering if there's any way to edit xml files to change the range of city bombardment. Essentially I want to make it so that catapults can attack (which I've done with iairrange) and bombard city defenses from 2 squares away.

Thanks in advance
 
Using AirRange on a land unit is the wrong way to go about this. There isn't a way to get them to attack farther away without using a modcomp such as DCM.
 
Like stated above the xml version works although it is very trashy(Mainly because it is really op).
But if you want to do it go to UnitInfos. Find the iAirRange, AirCombatLimit(Something like that), and iAirDamage. From there it is pretty self explanatory.
 
I've made modifications to make it not so OP. Mostly, I was looking to make the combat more realistic that catapults don't just die when they use their collateral damage attacks. To balance it out, I've made modifications on the cost of catapults as well as their power so that they can attack from a longer range without simply wreaking havoc, along with the addition of new horse units to counter catapults.

Either way, if not through XML, do any of you know what I can edit to allow for a long ranged city bombardment? I know what I'm doing as far as programming goes, even python. I simply haven't looked at the civ code yet and don't want to browse through thousands of lines of code to change one thing.
 
I've made modifications to make it not so OP. Mostly, I was looking to make the combat more realistic that catapults don't just die when they use their collateral damage attacks. To balance it out, I've made modifications on the cost of catapults as well as their power so that they can attack from a longer range without simply wreaking havoc, along with the addition of new horse units to counter catapults.

Either way, if not through XML, do any of you know what I can edit to allow for a long ranged city bombardment? I know what I'm doing as far as programming goes, even python. I simply haven't looked at the civ code yet and don't want to browse through thousands of lines of code to change one thing.

Look at the DCM (Dale's Combat Mod). That has a C++ component called Ranged Bombardment which does exactly what you are asking.
 
its not OP because of strenght or damage. its OP because the AI doesnt use it. The same goes for DCM unfortunately. It would be one of the coolest things in BtS though :(

Just to get it out there, the AI does use it although they suck at it.:lol:
 
its not OP because of strenght or damage. its OP because the AI doesnt use it. The same goes for DCM unfortunately. It would be one of the coolest things in BtS though :(

If the OP knows what he is doing (which he says he does) that is fixable.
 
I took a look at the AI code. There is a function called AI_rangeAttack() that makes the AI use... range attacks. However, this function is only called in one place - AI_anyAttack(). It looks like it just needs to be added into some of the other attack functions like AI_cityAttack(), AI_stackAttackCity(), and AI_leaveAttack(). It should probably be added to some defensive functions too so that units guarding cities will check if they can range attack.

edit: Of course depending on your combat odds you might just want to regular attack a city instead of range attack, so maybe that is why they didn't put it into the city attack functions.
 
Just using the xml method they will occasionally fire at you.

how do you set the settings, coz when I tried, I could bombard, but nomatter which setup I put the AI in, he wouldnt attack. Even if I gave him 30 arty and walked around his units or bombarded them. Not a single respons.
Did you change/add UnitAI, coz I didnt, and maybe giving them fighterplaneAI would make them react, since they get AirRange.

Do you have Chipotle enabled? If you do, can you ctrl+z and see what UnitAI the bombarding unit had?

And do you use any mods? even bug or bull or is it "vanilla" BtS?
 
Why not just place 10 arty at each corner of the world, together with one target board to fire at.
Then go to worldbuilder and assign a different Unit AI for each of them.
Then see what happens next.
 
how do you set the settings, coz when I tried, I could bombard, but nomatter which setup I put the AI in, he wouldnt attack. Even if I gave him 30 arty and walked around his units or bombarded them. Not a single respons.
Did you change/add UnitAI, coz I didnt, and maybe giving them fighterplaneAI would make them react, since they get AirRange.

Do you have Chipotle enabled? If you do, can you ctrl+z and see what UnitAI the bombarding unit had?

And do you use any mods? even bug or bull or is it "vanilla" BtS?

sorry for the late response. I'm not using any mods that have been posted here. I'm just trying to modify the game myself for personal use so that it's more balanced as far as logical warfare goes. From my personal playthroughs, I've found that the AI seems to know exactly what you're doing and where all your units are, giving an unfair advantage to them. While I can deal with this, I recently started playing with my wife who isn't so great at the whole strategy thing. Thus I'm making a more streamlined battle system.

To answer your question on how to make the ai attack, I simply used the unitai's for counter and they seem to attack just fine. I can't fully answer everything yet because I'm still testing these mods. I've been changing a lot of stuff recently including a full overhaul of the tech tree, additional resources, buildings, units, combat types, etc. Until I get this whole thing fully working, I can't say for sure how well some of my changes will work out, and it will take quite a bit of balancing.

For those of you that are curious as to the reason behind the ranged bombardment and how it affects battle mechanics, it's like this:

I've created new battle types: mounted-ranged, mounted-melee, elephant, and made each unit type more straightforward as to what they are strong against. Units now upgrade in a more tree-like manner, each with 4 ranks (for example hunter -> archer -> longbowman -> greatbowman, and with the option of archers upgrading towards a crossbow -> marksman branch). As such, units are as follows:

hunters upgrade towards archery which now can no longer upgrade ridiculous amounts of city defense and take out infantry. archers have a ranged bombardment attack against a single unit at range 1. They're ideal for holding out a city or taking out archers trying to hold the city.

hunters can also upgrade towards a crossbow branch that makes them additionally strong against melee units, but have no bombard attack.

warriors upgrade to swordsmen for city attack

spearmen upgrade to halberdiers for anti-horse attacks. All melee attackers attacking a stack will always target spearmen and mounted spearmen first. spearmen get a first strike.

mounted archers are strong vs siege and are immune to first strike with flanking on siege and high withdrawl rate.

mounted spearmen get a first strike and are strong against melee (they balance out spearmen, so mounted spearmen are made to meet spearmen first in battle)

mounted swordsmen get flanking on archery units and also get a flanking attack on siege.

gunpowder units start at muskets up to infantry. Then they have a choice of going into paratroopers then anti tank/air, or going straight infantry for better attack, but no anti-mech bonuses.

helicopters are strong vs tanks, while tanks are strong vs gunpowder.

ships can now attack along the coastline.
 
ok I think I missed something - so can we get AI to beneficially use range bombardment?

I did some crude testing, but it seems like it. Donno how well it will actually use it in a game, but giving an AI a couple of mobile arty range 3 will actually make it fire ranged attack, and then move it. If its in a city, it will fire from there and stay put. but it will fire. and again the turn after (iirc DCM only fired once?)

I've been looking at the cvunit.cpp to see if I can Randomize damage (also for air, coz it always annoyed me that damage was constant (only for airstrikes as interception works a different way). Maybe even make siege bombard randomized too (?) so its not allways a sure hit. Imho that would be interesting gameplay (maybe a catapult need 2 shot to lower defense, maybe 4 or even 6)

I'm also looking for the visibility issue (units can only bombard "unblocked" in own visibility zone).

Any help would be much appreciated :
(Line 11729 - 11962 in cvUnit.cpp)

Spoiler :
Code:
CvUnit* CvUnit::airStrikeTarget(const CvPlot* pPlot) const
{
	CvUnit* pDefender;

	pDefender = pPlot->getBestDefender(NO_PLAYER, getOwnerINLINE(), this, true);

	if (pDefender != NULL)
	{
		if (!pDefender->isDead())
		{
			if (pDefender->canDefend())
			{
				return pDefender;
			}
		}
	}

	return NULL;
}


bool CvUnit::canAirStrike(const CvPlot* pPlot) const
{
	if (getDomainType() != DOMAIN_AIR)
	{
		return false;
	}

	if (!canAirAttack())
	{
		return false;
	}

	if (pPlot == plot())
	{
		return false;
	}

	if (!pPlot->isVisible(getTeam(), false))
	{
		return false;
	}

	if (plotDistance(getX_INLINE(), getY_INLINE(), pPlot->getX_INLINE(), pPlot->getY_INLINE()) > airRange())
	{
		return false;
	}

	if (airStrikeTarget(pPlot) == NULL)
	{
		return false;
	}

	return true;
}


bool CvUnit::airStrike(CvPlot* pPlot)
{
	if (!canAirStrike(pPlot))
	{
		return false;
	}

	if (interceptTest(pPlot))
	{
		return false;
	}

	CvUnit* pDefender = airStrikeTarget(pPlot);

	FAssert(pDefender != NULL);
	FAssert(pDefender->canDefend());

	setReconPlot(pPlot);

	setMadeAttack(true);
	changeMoves(GC.getMOVE_DENOMINATOR());

	int iDamage = airCombatDamage(pDefender);

	int iUnitDamage = std::max(pDefender->getDamage(), std::min((pDefender->getDamage() + iDamage), airCombatLimit()));

	CvWString szBuffer = gDLL->getText("TXT_KEY_MISC_YOU_ARE_ATTACKED_BY_AIR", pDefender->getNameKey(), getNameKey(), -(((iUnitDamage - pDefender->getDamage()) * 100) / pDefender->maxHitPoints()));
	gDLL->getInterfaceIFace()->addMessage(pDefender->getOwnerINLINE(), false, GC.getEVENT_MESSAGE_TIME(), szBuffer, "AS2D_AIR_ATTACK", MESSAGE_TYPE_INFO, getButton(), (ColorTypes)GC.getInfoTypeForString("COLOR_RED"), pPlot->getX_INLINE(), pPlot->getY_INLINE(), true, true);

	szBuffer = gDLL->getText("TXT_KEY_MISC_YOU_ATTACK_BY_AIR", getNameKey(), pDefender->getNameKey(), -(((iUnitDamage - pDefender->getDamage()) * 100) / pDefender->maxHitPoints()));
	gDLL->getInterfaceIFace()->addMessage(getOwnerINLINE(), true, GC.getEVENT_MESSAGE_TIME(), szBuffer, "AS2D_AIR_ATTACKED", MESSAGE_TYPE_INFO, pDefender->getButton(), (ColorTypes)GC.getInfoTypeForString("COLOR_GREEN"), pPlot->getX_INLINE(), pPlot->getY_INLINE());

	collateralCombat(pPlot, pDefender);

	pDefender->setDamage(iUnitDamage, getOwnerINLINE());

	return true;
}

bool CvUnit::canRangeStrike() const
{
	if (getDomainType() == DOMAIN_AIR)
	{
		return false;
	}

	if (airRange() <= 0)
	{
		return false;
	}

	if (airBaseCombatStr() <= 0)
	{
		return false;
	}

	if (!canFight())
	{
		return false;
	}

	if (isMadeAttack() && !isBlitz())
	{
		return false;
	}

	if (!canMove() && getMoves() > 0)
	{
		return false;
	}

	return true;
}

bool CvUnit::canRangeStrikeAt(const CvPlot* pPlot, int iX, int iY) const
{
	if (!canRangeStrike())
	{
		return false;
	}

	CvPlot* pTargetPlot = GC.getMapINLINE().plotINLINE(iX, iY);

	if (NULL == pTargetPlot)
	{
		return false;
	}

	if (!pPlot->isVisible(getTeam(), false))
	{
		return false;
	}

	if (plotDistance(pPlot->getX_INLINE(), pPlot->getY_INLINE(), pTargetPlot->getX_INLINE(), pTargetPlot->getY_INLINE()) > airRange())
	{
		return false;
	}

	CvUnit* pDefender = airStrikeTarget(pTargetPlot);
	if (NULL == pDefender)
	{
		return false;
	}

	if (!pPlot->canSeePlot(pTargetPlot, getTeam(), airRange(), getFacingDirection(true)))
	{
		return false;
	}
	
	return true;
}


bool CvUnit::rangeStrike(int iX, int iY)
{
	CvUnit* pDefender;
	CvWString szBuffer;
	int iUnitDamage;
	int iDamage;

	CvPlot* pPlot = GC.getMapINLINE().plot(iX, iY);
	if (NULL == pPlot)
	{
		return false;
	}

	if (!canRangeStrikeAt(pPlot, iX, iY))
	{
		return false;
	}

	pDefender = airStrikeTarget(pPlot);

	FAssert(pDefender != NULL);
	FAssert(pDefender->canDefend());

	if (GC.getDefineINT("RANGED_ATTACKS_USE_MOVES") == 0)
	{
		setMadeAttack(true);
	}
	changeMoves(GC.getMOVE_DENOMINATOR());

	iDamage = rangeCombatDamage(pDefender);

	iUnitDamage = std::max(pDefender->getDamage(), std::min((pDefender->getDamage() + iDamage), airCombatLimit()));

	szBuffer = gDLL->getText("TXT_KEY_MISC_YOU_ARE_ATTACKED_BY_AIR", pDefender->getNameKey(), getNameKey(), -(((iUnitDamage - pDefender->getDamage()) * 100) / pDefender->maxHitPoints()));
	//red icon over attacking unit
	gDLL->getInterfaceIFace()->addMessage(pDefender->getOwnerINLINE(), false, GC.getEVENT_MESSAGE_TIME(), szBuffer, "AS2D_COMBAT", MESSAGE_TYPE_INFO, getButton(), (ColorTypes)GC.getInfoTypeForString("COLOR_RED"), this->getX_INLINE(), this->getY_INLINE(), true, true);
	//white icon over defending unit
	gDLL->getInterfaceIFace()->addMessage(pDefender->getOwnerINLINE(), false, 0, L"", "AS2D_COMBAT", MESSAGE_TYPE_DISPLAY_ONLY, pDefender->getButton(), (ColorTypes)GC.getInfoTypeForString("COLOR_WHITE"), pDefender->getX_INLINE(), pDefender->getY_INLINE(), true, true);

	szBuffer = gDLL->getText("TXT_KEY_MISC_YOU_ATTACK_BY_AIR", getNameKey(), pDefender->getNameKey(), -(((iUnitDamage - pDefender->getDamage()) * 100) / pDefender->maxHitPoints()));
	gDLL->getInterfaceIFace()->addMessage(getOwnerINLINE(), true, GC.getEVENT_MESSAGE_TIME(), szBuffer, "AS2D_COMBAT", MESSAGE_TYPE_INFO, pDefender->getButton(), (ColorTypes)GC.getInfoTypeForString("COLOR_GREEN"), pPlot->getX_INLINE(), pPlot->getY_INLINE());

	collateralCombat(pPlot, pDefender);

	//set damage but don't update entity damage visibility
	pDefender->setDamage(iUnitDamage, getOwnerINLINE(), false);

	if (pPlot->isActiveVisible(false))
	{
		// Range strike entity mission
		CvMissionDefinition kDefiniton;
		kDefiniton.setMissionTime(GC.getMissionInfo(MISSION_RANGE_ATTACK).getTime() * gDLL->getSecsPerTurn());
		kDefiniton.setMissionType(MISSION_RANGE_ATTACK);
		kDefiniton.setPlot(pDefender->plot());
		kDefiniton.setUnit(BATTLE_UNIT_ATTACKER, this);
		kDefiniton.setUnit(BATTLE_UNIT_DEFENDER, pDefender);
		gDLL->getEntityIFace()->AddMission(&kDefiniton);

		//delay death
		pDefender->getGroup()->setMissionTimer(GC.getMissionInfo(MISSION_RANGE_ATTACK).getTime());
	}

	return true;
}
 
I did some crude testing, but it seems like it. Donno how well it will actually use it in a game, but giving an AI a couple of mobile arty range 3 will actually make it fire ranged attack, and then move it. If its in a city, it will fire from there and stay put. but it will fire. and again the turn after (iirc DCM only fired once?)

I've been looking at the cvunit.cpp to see if I can Randomize damage (also for air, coz it always annoyed me that damage was constant (only for airstrikes as interception works a different way). Maybe even make siege bombard randomized too (?) so its not allways a sure hit. Imho that would be interesting gameplay (maybe a catapult need 2 shot to lower defense, maybe 4 or even 6)

I'm also looking for the visibility issue (units can only bombard "unblocked" in own visibility zone).

Any help would be much appreciated :
(Line 11729 - 11962 in cvUnit.cpp)

Spoiler :
Code:
CvUnit* CvUnit::airStrikeTarget(const CvPlot* pPlot) const
{
	CvUnit* pDefender;

	pDefender = pPlot->getBestDefender(NO_PLAYER, getOwnerINLINE(), this, true);

	if (pDefender != NULL)
	{
		if (!pDefender->isDead())
		{
			if (pDefender->canDefend())
			{
				return pDefender;
			}
		}
	}

	return NULL;
}


bool CvUnit::canAirStrike(const CvPlot* pPlot) const
{
	if (getDomainType() != DOMAIN_AIR)
	{
		return false;
	}

	if (!canAirAttack())
	{
		return false;
	}

	if (pPlot == plot())
	{
		return false;
	}

	if (!pPlot->isVisible(getTeam(), false))
	{
		return false;
	}

	if (plotDistance(getX_INLINE(), getY_INLINE(), pPlot->getX_INLINE(), pPlot->getY_INLINE()) > airRange())
	{
		return false;
	}

	if (airStrikeTarget(pPlot) == NULL)
	{
		return false;
	}

	return true;
}


bool CvUnit::airStrike(CvPlot* pPlot)
{
	if (!canAirStrike(pPlot))
	{
		return false;
	}

	if (interceptTest(pPlot))
	{
		return false;
	}

	CvUnit* pDefender = airStrikeTarget(pPlot);

	FAssert(pDefender != NULL);
	FAssert(pDefender->canDefend());

	setReconPlot(pPlot);

	setMadeAttack(true);
	changeMoves(GC.getMOVE_DENOMINATOR());

	int iDamage = airCombatDamage(pDefender);

	int iUnitDamage = std::max(pDefender->getDamage(), std::min((pDefender->getDamage() + iDamage), airCombatLimit()));

	CvWString szBuffer = gDLL->getText("TXT_KEY_MISC_YOU_ARE_ATTACKED_BY_AIR", pDefender->getNameKey(), getNameKey(), -(((iUnitDamage - pDefender->getDamage()) * 100) / pDefender->maxHitPoints()));
	gDLL->getInterfaceIFace()->addMessage(pDefender->getOwnerINLINE(), false, GC.getEVENT_MESSAGE_TIME(), szBuffer, "AS2D_AIR_ATTACK", MESSAGE_TYPE_INFO, getButton(), (ColorTypes)GC.getInfoTypeForString("COLOR_RED"), pPlot->getX_INLINE(), pPlot->getY_INLINE(), true, true);

	szBuffer = gDLL->getText("TXT_KEY_MISC_YOU_ATTACK_BY_AIR", getNameKey(), pDefender->getNameKey(), -(((iUnitDamage - pDefender->getDamage()) * 100) / pDefender->maxHitPoints()));
	gDLL->getInterfaceIFace()->addMessage(getOwnerINLINE(), true, GC.getEVENT_MESSAGE_TIME(), szBuffer, "AS2D_AIR_ATTACKED", MESSAGE_TYPE_INFO, pDefender->getButton(), (ColorTypes)GC.getInfoTypeForString("COLOR_GREEN"), pPlot->getX_INLINE(), pPlot->getY_INLINE());

	collateralCombat(pPlot, pDefender);

	pDefender->setDamage(iUnitDamage, getOwnerINLINE());

	return true;
}

bool CvUnit::canRangeStrike() const
{
	if (getDomainType() == DOMAIN_AIR)
	{
		return false;
	}

	if (airRange() <= 0)
	{
		return false;
	}

	if (airBaseCombatStr() <= 0)
	{
		return false;
	}

	if (!canFight())
	{
		return false;
	}

	if (isMadeAttack() && !isBlitz())
	{
		return false;
	}

	if (!canMove() && getMoves() > 0)
	{
		return false;
	}

	return true;
}

bool CvUnit::canRangeStrikeAt(const CvPlot* pPlot, int iX, int iY) const
{
	if (!canRangeStrike())
	{
		return false;
	}

	CvPlot* pTargetPlot = GC.getMapINLINE().plotINLINE(iX, iY);

	if (NULL == pTargetPlot)
	{
		return false;
	}

	if (!pPlot->isVisible(getTeam(), false))
	{
		return false;
	}

	if (plotDistance(pPlot->getX_INLINE(), pPlot->getY_INLINE(), pTargetPlot->getX_INLINE(), pTargetPlot->getY_INLINE()) > airRange())
	{
		return false;
	}

	CvUnit* pDefender = airStrikeTarget(pTargetPlot);
	if (NULL == pDefender)
	{
		return false;
	}

	if (!pPlot->canSeePlot(pTargetPlot, getTeam(), airRange(), getFacingDirection(true)))
	{
		return false;
	}
	
	return true;
}


bool CvUnit::rangeStrike(int iX, int iY)
{
	CvUnit* pDefender;
	CvWString szBuffer;
	int iUnitDamage;
	int iDamage;

	CvPlot* pPlot = GC.getMapINLINE().plot(iX, iY);
	if (NULL == pPlot)
	{
		return false;
	}

	if (!canRangeStrikeAt(pPlot, iX, iY))
	{
		return false;
	}

	pDefender = airStrikeTarget(pPlot);

	FAssert(pDefender != NULL);
	FAssert(pDefender->canDefend());

	if (GC.getDefineINT("RANGED_ATTACKS_USE_MOVES") == 0)
	{
		setMadeAttack(true);
	}
	changeMoves(GC.getMOVE_DENOMINATOR());

	iDamage = rangeCombatDamage(pDefender);

	iUnitDamage = std::max(pDefender->getDamage(), std::min((pDefender->getDamage() + iDamage), airCombatLimit()));

	szBuffer = gDLL->getText("TXT_KEY_MISC_YOU_ARE_ATTACKED_BY_AIR", pDefender->getNameKey(), getNameKey(), -(((iUnitDamage - pDefender->getDamage()) * 100) / pDefender->maxHitPoints()));
	//red icon over attacking unit
	gDLL->getInterfaceIFace()->addMessage(pDefender->getOwnerINLINE(), false, GC.getEVENT_MESSAGE_TIME(), szBuffer, "AS2D_COMBAT", MESSAGE_TYPE_INFO, getButton(), (ColorTypes)GC.getInfoTypeForString("COLOR_RED"), this->getX_INLINE(), this->getY_INLINE(), true, true);
	//white icon over defending unit
	gDLL->getInterfaceIFace()->addMessage(pDefender->getOwnerINLINE(), false, 0, L"", "AS2D_COMBAT", MESSAGE_TYPE_DISPLAY_ONLY, pDefender->getButton(), (ColorTypes)GC.getInfoTypeForString("COLOR_WHITE"), pDefender->getX_INLINE(), pDefender->getY_INLINE(), true, true);

	szBuffer = gDLL->getText("TXT_KEY_MISC_YOU_ATTACK_BY_AIR", getNameKey(), pDefender->getNameKey(), -(((iUnitDamage - pDefender->getDamage()) * 100) / pDefender->maxHitPoints()));
	gDLL->getInterfaceIFace()->addMessage(getOwnerINLINE(), true, GC.getEVENT_MESSAGE_TIME(), szBuffer, "AS2D_COMBAT", MESSAGE_TYPE_INFO, pDefender->getButton(), (ColorTypes)GC.getInfoTypeForString("COLOR_GREEN"), pPlot->getX_INLINE(), pPlot->getY_INLINE());

	collateralCombat(pPlot, pDefender);

	//set damage but don't update entity damage visibility
	pDefender->setDamage(iUnitDamage, getOwnerINLINE(), false);

	if (pPlot->isActiveVisible(false))
	{
		// Range strike entity mission
		CvMissionDefinition kDefiniton;
		kDefiniton.setMissionTime(GC.getMissionInfo(MISSION_RANGE_ATTACK).getTime() * gDLL->getSecsPerTurn());
		kDefiniton.setMissionType(MISSION_RANGE_ATTACK);
		kDefiniton.setPlot(pDefender->plot());
		kDefiniton.setUnit(BATTLE_UNIT_ATTACKER, this);
		kDefiniton.setUnit(BATTLE_UNIT_DEFENDER, pDefender);
		gDLL->getEntityIFace()->AddMission(&kDefiniton);

		//delay death
		pDefender->getGroup()->setMissionTimer(GC.getMissionInfo(MISSION_RANGE_ATTACK).getTime());
	}

	return true;
}

a few things you can try:

1. play around with the "int iDamage" and "int iUnitDamage" under bool CvUnit::airstrike

you can add a random number in these lines to play around with random damage

2. to fix the visibility issue, i believe you can get rid of these lines:
Code:
	if (!pPlot->isVisible(getTeam(), false))
	{
		return false;
	}

or if you don't want to change the code, just have it return true.

This is just guessing without looking too much into the code.


Edit: Also, I have my modifications complete to the point of playability. I've been testing balance, and I must say that the ranged bombardment thing actually works now. In my game, the catapults would come in and attack any units they saw through bombardment instead of straight up attack. I had to scout them out with my horse units to kill them. Archers generally stayed within city walls, 1 range bombing any units that came within range of the city. They kinda acted like the natural city's defensive attacks in civ5.

As far as the balance of the visibility issue goes, i kinda like the way it worked out. First off, it doesn't make too much sense that a catapult would be able to launch rocks through a forest into a city or over a hill into a city, but that's just the irl situation. The reason I liked it in civilization is that this gives a reason to save a lot of forests, making the eco-friendly upgrade path more usable. I've found that the way civ was before pushed me to chop down forests pretty early mostly due to my happiness growing high enough that my city would benefit too greatly from working those tiles before lumbermills. most of my cities went for commerce production early with cottages to boost my research rate.

The way I have modded the game, civilization happiness now grows at a steady pace instead of getting a quick +4 happiness boost to all cities when plantation is discovered. I have made all resources provide happiness bonuses only to the city working it, and those resources then also gain a happiness bonus from additional buildings.

Either way, my point is using this method, forests and hills have also become a tactical strategy, especially for plotting a defensive location for your city.
 
Top Bottom