warzone2100/src/combat.cpp

509 lines
16 KiB
C++

/*
This file is part of Warzone 2100.
Copyright (C) 1999-2004 Eidos Interactive
Copyright (C) 2005-2013 Warzone 2100 Project
Warzone 2100 is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
Warzone 2100 is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Warzone 2100; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
/**
* @file combat.c
*
* Combat mechanics routines.
*
*/
#include "lib/framework/frame.h"
#include "lib/netplay/netplay.h"
#include "action.h"
#include "cluster.h"
#include "combat.h"
#include "difficulty.h"
#include "geometry.h"
#include "mapgrid.h"
#include "projectile.h"
#include "random.h"
#include "qtscript.h"
/* Fire a weapon at something */
bool combFire(WEAPON *psWeap, BASE_OBJECT *psAttacker, BASE_OBJECT *psTarget, int weapon_slot)
{
WEAPON_STATS *psStats;
UDWORD firePause;
SDWORD longRange;
int compIndex;
CHECK_OBJECT(psAttacker);
CHECK_OBJECT(psTarget);
ASSERT(psWeap != NULL, "Invalid weapon pointer");
/* Don't shoot if the weapon_slot of a vtol is empty */
if (psAttacker->type == OBJ_DROID && isVtolDroid(((DROID *)psAttacker))
&& psWeap->usedAmmo >= getNumAttackRuns(((DROID *)psAttacker), weapon_slot))
{
objTrace(psAttacker->id, "VTOL slot %d is empty", weapon_slot);
return false;
}
/* Get the stats for the weapon */
compIndex = psWeap->nStat;
ASSERT_OR_RETURN( false , compIndex < numWeaponStats, "Invalid range referenced for numWeaponStats, %d > %d", compIndex, numWeaponStats);
psStats = asWeaponStats + compIndex;
// check valid weapon/prop combination
if (!validTarget(psAttacker, psTarget, weapon_slot))
{
return false;
}
unsigned fireTime = gameTime - deltaGameTime + 1; // Can fire earliest at the start of the tick.
// See if reloadable weapon.
if (psStats->upgrade[psAttacker->player].reloadTime)
{
unsigned reloadTime = psWeap->lastFired + weaponReloadTime(psStats, psAttacker->player);
if (psWeap->ammo == 0) // Out of ammo?
{
fireTime = std::max(fireTime, reloadTime); // Have to wait for weapon to reload before firing.
if (gameTime < fireTime)
{
return false;
}
}
if (reloadTime <= fireTime)
{
//reset the ammo level
psWeap->ammo = psStats->upgrade[psAttacker->player].numRounds;
}
}
/* See when the weapon last fired to control it's rate of fire */
firePause = weaponFirePause(psStats, psAttacker->player);
firePause = std::max(firePause, 1u); // Don't shoot infinitely many shots at once.
fireTime = std::max(fireTime, psWeap->lastFired + firePause);
if (gameTime < fireTime)
{
/* Too soon to fire again */
return false;
}
if (psTarget->visible[psAttacker->player] != UBYTE_MAX)
{
// Can't see it - can't hit it
objTrace(psAttacker->id, "combFire(%u[%s]->%u): Object has no indirect sight of target", psAttacker->id, getName(psStats), psTarget->id);
return false;
}
/* Check we can hit the target */
bool tall = (psAttacker->type == OBJ_DROID && isVtolDroid((DROID *)psAttacker))
|| (psAttacker->type == OBJ_STRUCTURE && ((STRUCTURE *)psAttacker)->pStructureType->height > 1);
if (proj_Direct(psStats) && !lineOfFire(psAttacker, psTarget, weapon_slot, tall))
{
// Can't see the target - can't hit it with direct fire
objTrace(psAttacker->id, "combFire(%u[%s]->%u): No direct line of sight to target",
psAttacker->id, objInfo(psAttacker), psTarget->id);
return false;
}
Vector3i deltaPos = psTarget->pos - psAttacker->pos;
// if the turret doesn't turn, check if the attacker is in alignment with the target
if (psAttacker->type == OBJ_DROID && !psStats->rotate)
{
uint16_t targetDir = iAtan2(removeZ(deltaPos));
int dirDiff = abs(angleDelta(targetDir - psAttacker->rot.direction));
if (dirDiff > FIXED_TURRET_DIR)
{
return false;
}
}
/* Now see if the target is in range - also check not too near */
int dist = iHypot(removeZ(deltaPos));
longRange = proj_GetLongRange(psStats, psAttacker->player);
int min_angle = 0;
// Calculate angle for indirect shots
if (!proj_Direct(psStats) && dist > 0)
{
min_angle = arcOfFire(psAttacker,psTarget,weapon_slot,true);
// prevent extremely steep shots
min_angle = std::min(min_angle, DEG(PROJ_ULTIMATE_PITCH));
// adjust maximum range of unit if forced to shoot very steep
if (min_angle > DEG(PROJ_MAX_PITCH))
{
//do not allow increase of max range though
if (iSin(2*min_angle) < iSin(2*DEG(PROJ_MAX_PITCH))) // If PROJ_MAX_PITCH == 45, then always iSin(2*min_angle) <= iSin(2*DEG(PROJ_MAX_PITCH)), and the test is redundant.
{
longRange = longRange * iSin(2*min_angle) / iSin(2*DEG(PROJ_MAX_PITCH));
}
}
}
int baseHitChance = 0;
if (dist <= longRange && dist >= psStats->upgrade[psAttacker->player].minRange)
{
// get weapon chance to hit in the long range
baseHitChance = weaponLongHit(psStats,psAttacker->player);
// adapt for height adjusted artillery shots
if (min_angle > DEG(PROJ_MAX_PITCH))
{
baseHitChance = baseHitChance * iCos(min_angle) / iCos(DEG(PROJ_MAX_PITCH));
}
}
else
{
/* Out of range */
objTrace(psAttacker->id, "combFire(%u[%s]->%u): Out of range", psAttacker->id, getName(psStats), psTarget->id);
return false;
}
// apply experience accuracy modifiers to the base
//hit chance, not to the final hit chance
int resultHitChance = baseHitChance;
// add the attacker's experience
if (psAttacker->type == OBJ_DROID)
{
SDWORD level = getDroidEffectiveLevel((DROID *) psAttacker);
// increase total accuracy by EXP_ACCURACY_BONUS % for each experience level
resultHitChance += EXP_ACCURACY_BONUS * level * baseHitChance / 100;
}
// subtract the defender's experience
if (psTarget->type == OBJ_DROID)
{
SDWORD level = getDroidEffectiveLevel((DROID *) psTarget);
// decrease weapon accuracy by EXP_ACCURACY_BONUS % for each experience level
resultHitChance -= EXP_ACCURACY_BONUS * level * baseHitChance / 100;
}
if (psAttacker->type == OBJ_DROID && ((DROID *)psAttacker)->sMove.Status != MOVEINACTIVE
&& !psStats->fireOnMove)
{
return false; // Can't fire while moving
}
/* -------!!! From that point we are sure that we are firing !!!------- */
/* note when the weapon fired */
psWeap->lastFired = fireTime;
/* reduce ammo if salvo */
if (psStats->upgrade[psAttacker->player].reloadTime)
{
psWeap->ammo--;
}
// increment the shots counter
psWeap->shotsFired++;
// predicted X,Y offset per sec
Vector3i predict = psTarget->pos;
// Target prediction
if (isDroid(psTarget) && castDroid(psTarget)->sMove.bumpTime == 0)
{
DROID *psDroid = castDroid(psTarget);
int32_t flightTime;
if (proj_Direct(psStats) || dist <= psStats->upgrade[psAttacker->player].minRange)
{
flightTime = dist * GAME_TICKS_PER_SEC / psStats->flightSpeed;
}
else
{
int32_t vXY, vZ; // Unused, we just want the flight time.
flightTime = projCalcIndirectVelocities(dist, deltaPos.z, psStats->flightSpeed, &vXY, &vZ, min_angle);
}
if (psTarget->lastHitWeapon == WSC_EMP)
{
int empTime = EMP_DISABLE_TIME - (gameTime - psTarget->timeLastHit);
CLIP(empTime, 0, EMP_DISABLE_TIME);
if (empTime >= EMP_DISABLE_TIME * 9/10)
{
flightTime = 0; /* Just hit. Assume they'll get hit again */
}
else
{
flightTime = MAX(0, flightTime - empTime);
}
}
predict += Vector3i(iSinCosR(psDroid->sMove.moveDir, psDroid->sMove.speed*flightTime / GAME_TICKS_PER_SEC), 0);
if (!isFlying(psDroid))
{
predict.z = map_Height(removeZ(predict)); // Predict that the object will be on the ground.
}
}
/* Fire off the bullet to the miss location. The miss is only visible if the player owns the target. (Why? - Per) */
// What bVisible really does is to make the projectile audible even if it misses you. Since the target is NULL, proj_SendProjectile can't check if it was fired at you.
bool bVisibleAnyway = psTarget->player == selectedPlayer;
// see if we were lucky to hit the target
bool isHit = gameRand(100) <= resultHitChance;
if (isHit)
{
/* Kerrrbaaang !!!!! a hit */
objTrace(psAttacker->id, "combFire: [%s]->%u: resultHitChance=%d, visibility=%d", getName(psStats), psTarget->id, resultHitChance, (int)psTarget->visible[psAttacker->player]);
syncDebug("hit=(%d,%d,%d)", predict.x, predict.y, predict.z);
}
else /* Deal with a missed shot */
{
const int minOffset = 5;
int missDist = 2 * (100 - resultHitChance) + minOffset;
Vector3i miss = Vector3i(iSinCosR(gameRand(DEG(360)), missDist), 0);
predict += miss;
psTarget = NULL; // Missed the target, so don't expect to hit it.
objTrace(psAttacker->id, "combFire: Missed shot by (%4d,%4d)", miss.x, miss.y);
syncDebug("miss=(%d,%d,%d)", predict.x, predict.y, predict.z);
}
// Make sure we don't pass any negative or out of bounds numbers to proj_SendProjectile
CLIP(predict.x, 0, world_coord(mapWidth - 1));
CLIP(predict.y, 0, world_coord(mapHeight - 1));
proj_SendProjectileAngled(psWeap, psAttacker, psAttacker->player, predict, psTarget, bVisibleAnyway, weapon_slot, min_angle, fireTime);
return true;
}
/*checks through the target players list of structures and droids to see
if any support a counter battery sensor*/
void counterBatteryFire(BASE_OBJECT *psAttacker, BASE_OBJECT *psTarget)
{
/*if a null target is passed in ignore - this will be the case when a 'miss'
projectile is sent - we may have to cater for these at some point*/
// also ignore cases where you attack your own player
// Also ignore cases where there are already 1000 missiles heading towards the attacker.
if (psTarget == NULL
|| (psAttacker != NULL && psAttacker->player == psTarget->player)
|| aiObjectIsProbablyDoomed(psAttacker))
{
return;
}
CHECK_OBJECT(psTarget);
for (BASE_OBJECT *psViewer = apsSensorList[0]; psViewer; psViewer = psViewer->psNextFunc)
{
if (aiCheckAlliances(psTarget->player, psViewer->player))
{
if ((psViewer->type == OBJ_STRUCTURE && !structCBSensor((STRUCTURE *)psViewer))
|| (psViewer->type == OBJ_DROID && !cbSensorDroid((DROID *)psViewer)))
{
continue;
}
const int sensorRange = objSensorRange(psViewer);
// Check sensor distance from target
const int xDiff = psViewer->pos.x - psTarget->pos.x;
const int yDiff = psViewer->pos.y - psTarget->pos.y;
if (xDiff * xDiff + yDiff * yDiff < sensorRange * sensorRange)
{
// Inform viewer of target
if (psViewer->type == OBJ_DROID)
{
orderDroidObj((DROID *)psViewer, DORDER_OBSERVE, psAttacker, ModeImmediate);
}
else if (psViewer->type == OBJ_STRUCTURE)
{
((STRUCTURE *)psViewer)->psTarget[0] = psAttacker;
}
}
}
}
}
int objArmour(BASE_OBJECT *psObj, WEAPON_CLASS weaponClass)
{
int armour = 0;
if (psObj->type == OBJ_DROID)
{
armour = ((DROID *)psObj)->armour[weaponClass];
}
else if (psObj->type == OBJ_STRUCTURE && weaponClass == WC_KINETIC && ((STRUCTURE *)psObj)->status != SS_BEING_BUILT)
{
armour = ((STRUCTURE *)psObj)->pStructureType->upgrade[psObj->player].armour;
}
else if (psObj->type == OBJ_STRUCTURE && weaponClass == WC_HEAT && ((STRUCTURE *)psObj)->status != SS_BEING_BUILT)
{
armour = ((STRUCTURE *)psObj)->pStructureType->upgrade[psObj->player].thermal;
}
else if (psObj->type == OBJ_FEATURE && weaponClass == WC_KINETIC)
{
armour = ((FEATURE *)psObj)->psStats->armourValue;
}
return armour;
}
/* Deals damage to an object
* \param psObj object to deal damage to
* \param damage amount of damage to deal
* \param weaponClass the class of the weapon that deals the damage
* \param weaponSubClass the subclass of the weapon that deals the damage
* \return < 0 when the dealt damage destroys the object, > 0 when the object survives
*/
int32_t objDamage(BASE_OBJECT *psObj, unsigned damage, unsigned originalhp, WEAPON_CLASS weaponClass, WEAPON_SUBCLASS weaponSubClass, bool isDamagePerSecond)
{
int actualDamage, level = 1, lastHit = psObj->timeLastHit;
int armour = objArmour(psObj, weaponClass);
// If the previous hit was by an EMP cannon and this one is not:
// don't reset the weapon class and hit time
// (Giel: I guess we need this to determine when the EMP-"shock" is over)
if (psObj->lastHitWeapon != WSC_EMP || weaponSubClass == WSC_EMP)
{
psObj->timeLastHit = gameTime;
psObj->lastHitWeapon = weaponSubClass;
}
// EMP cannons do no damage, if we are one return now
if (weaponSubClass == WSC_EMP)
{
return 0;
}
// apply game difficulty setting
damage = modifyForDifficultyLevel(damage, psObj->player != selectedPlayer);
if (psObj->type == OBJ_STRUCTURE || psObj->type == OBJ_DROID)
{
// Force sending messages, even if messages were turned off, since a non-synchronised script will execute here. (Aaargh!)
bool bMultiMessagesBackup = bMultiMessages;
bMultiMessages = bMultiPlayer;
clustObjectAttacked((BASE_OBJECT *)psObj);
triggerEventAttacked(psObj, g_pProjLastAttacker, lastHit);
bMultiMessages = bMultiMessagesBackup;
}
debug(LOG_ATTACK, "objDamage(%d): body %d armour %d damage: %d", psObj->id, psObj->body, armour, damage);
if (psObj->type == OBJ_DROID)
{
DROID *psDroid = (DROID *)psObj;
// Retrieve highest, applicable, experience level
level = getDroidEffectiveLevel(psDroid);
}
// Reduce damage taken by EXP_REDUCE_DAMAGE % for each experience level
actualDamage = (damage * (100 - EXP_REDUCE_DAMAGE * level)) / 100;
// You always do at least a third of the experience modified damage
actualDamage = MAX(actualDamage - armour, actualDamage / 3);
// And at least MIN_WEAPON_DAMAGE points
actualDamage = MAX(actualDamage, MIN_WEAPON_DAMAGE);
if (isDamagePerSecond)
{
int deltaDamageRate = actualDamage - psObj->periodicalDamage;
if (deltaDamageRate <= 0)
{
return 0; // Did this much damage already, this tick, so don't do more.
}
actualDamage = gameTimeAdjustedAverage(deltaDamageRate);
psObj->periodicalDamage += deltaDamageRate;
}
objTrace(psObj->id, "objDamage: Penetrated %d", actualDamage);
syncDebug("damage%u dam%u,o%u,wc%d.%d,ar%d,lev%d,aDam%d,isDps%d", psObj->id, damage, originalhp, weaponClass, weaponSubClass, armour, level, actualDamage, isDamagePerSecond);
// for some odd reason, we have 0 hitpoints.
if (!originalhp)
{
ASSERT(originalhp, "original hitpoints are 0 ?");
return -65536; // it is dead
}
// If the shell did sufficient damage to destroy the object, deal with it and return
if (actualDamage >= psObj->body)
{
return -(int64_t)65536 * psObj->body / originalhp;
}
// Subtract the dealt damage from the droid's remaining body points
psObj->body -= actualDamage;
syncDebugObject(psObj, 'D');
return (int64_t)65536 * actualDamage / originalhp;
}
/* Guesses how damage a shot might do.
* \param psObj object that might be hit
* \param damage amount of damage to deal
* \param weaponClass the class of the weapon that deals the damage
* \param weaponSubClass the subclass of the weapon that deals the damage
* \return guess at amount of damage
*/
unsigned int objGuessFutureDamage(WEAPON_STATS *psStats, unsigned int player, BASE_OBJECT *psTarget)
{
unsigned int damage;
int actualDamage, armour, level = 1;
if (psTarget == NULL)
return 0; // Hard to destroy the ground. The armour on the mud is very strong and blocks all damage.
damage = calcDamage(weaponDamage(psStats, player), psStats->weaponEffect, psTarget);
// EMP cannons do no damage, if we are one return now
if (psStats->weaponSubClass == WSC_EMP)
{
return 0;
}
// apply game difficulty setting
damage = modifyForDifficultyLevel(damage, psTarget->player != selectedPlayer);
armour = objArmour(psTarget, psStats->weaponClass);
if (psTarget->type == OBJ_DROID)
{
DROID *psDroid = (DROID *)psTarget;
// Retrieve highest, applicable, experience level
level = getDroidEffectiveLevel(psDroid);
}
//debug(LOG_ATTACK, "objGuessFutureDamage(%d): body %d armour %d damage: %d", psObj->id, psObj->body, armour, damage);
// Reduce damage taken by EXP_REDUCE_DAMAGE % for each experience level
actualDamage = (damage * (100 - EXP_REDUCE_DAMAGE * level)) / 100;
// You always do at least a third of the experience modified damage
actualDamage = MAX(actualDamage - armour, actualDamage / 3);
// And at least MIN_WEAPON_DAMAGE points
actualDamage = MAX(actualDamage, MIN_WEAPON_DAMAGE);
//objTrace(psObj->id, "objGuessFutureDamage: Would penetrate %d", actualDamage);
return actualDamage;
}