/** * @file * @brief Reaction fire code * * Note that in a turn-based game, reaction fire is a design bug in the first place, * causing several logic problems. But we want it and we need it, so we'll have to * work around those problems. * * Imagine the following situations: * One of your soldiers is standing next to a house by a street. There is an alien down * the street with a gun in snap shot mode (8 TUs). The soldier steps out on street, trying * to cross it (and entering the line of sight of that alien). After 4 more paces or spending * 8 TUs, your soldier is shot by the alien. Sounds reasonable? Fine. That's the basic idea * of reaction fire. * * Now assume you have 5 soldiers next to that house. They all step out on the street. Nothing * happens because they all just entered the line of sight of the alien. The first soldier * starts walking and gets shot after 4 paces like above. The second soldier will walk 7 paces * unharmed and get shot after 8 paces and the third soldier will be shot after 12 paces/24 TUs. * This is because when the alien shoots at one target, all his other targets will receive a * bonus equal to the amount of TUs the alien used for his shot. Still sounds reasonable? Fine. * * A slight modification: only one of the 5 soldiers steps out and gets shot after 4 paces. * Then the 2nd steps out and gets shot after 4 paces as well. Likewise the 3rd soldier. * That's because the soldiers hidden behind the house are not among the targets of the alien * and thus don't receive the bonus when the alien shoots, * * There is also a problem at end of turn. Imagine your sniper is set to react with an aimed * shot (18 Tus). An alien steps into his line of sight and fires two snap shots, totaling * 16 TUs. Then the alien decides to do nothing for the rest of his round. You would love to * see your sniper do his aimed shot, right ? But reaction fire rules don't allow that. * On the other hand: if you (were stupid enough to) move one of your soldiers with his last * 2 TUs out from cover and into the sight of an alien on RF with all his TUs still available, * your soldier will receive no RF, due to the same RF rules. You love that, right ? * * Reaction fire is involved in the following situations: * 1. G_ReactionFireOnMovement() * calls G_ReactionFireCheckExecution() * calls G_ReactionFireTryToShoot() * calls G_ReactionFireTargetsUpdateAll() * 2. G_ClientShoot() * calls G_ReactionFirePreShot() * calls G_ReactionFireTargetsUpdateAll() * calls G_ReactionFireTryToShoot() * calls G_ReactionFirePostShot() * calls G_ReactionFireCheckExecution() * calls G_ReactionFireTryToShoot() * 3. G_ClientEndRound() * calls G_ReactionFireOnEndTurn() * calls G_ReactionFireTryToShoot() * calls G_ReactionFireReset() */ /* Copyright (C) 2002-2013 UFO: Alien Invasion. This program 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. This program 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 this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ #include "g_reaction.h" #include "g_actor.h" #include "g_client.h" #include "g_combat.h" #include "g_edicts.h" #include "g_match.h" #include "g_vis.h" #define MAX_RF_TARGETS 10 #define MAX_RF_DATA 128 #define DEBUG_RF 0 /** @brief A single relation between a shooter and his target. */ class ReactionFireTarget { public: Edict const *target; int triggerTUs; /* the amount of TUS of the target(!) at which the reaction takes place */ }; #define RF_NO_ENTNUM -1 /** @brief A list of relations between a shooter and all his targets. */ class ReactionFireTargetList { public: int entnum; int count; ReactionFireTarget targets[MAX_RF_TARGETS]; ReactionFireTargetList () { init(); } inline void init (void) { entnum = RF_NO_ENTNUM; count = 0; } inline void reset (void) { count = 0; } }; /** @brief A table with all the relations between all shooters and all their targets. */ class ReactionFireTargets { public: void init(); void add(const Edict* shooter, const Edict* target, const int tusForShot); void remove(const Edict* shooter, const Edict* target); bool hasExpired(const Edict* shooter, const Edict* target, const int tusTarget); int getTriggerTUs(const Edict* shooter, const Edict* target); void advance(const Edict* shooter, const int tusShot); void reset(); void notifyClientMove(const Edict* target, int step, bool startMove); void notifyClientOnStep(const Edict* target, int step); void create(const Edict* shooter); void resetTargetList(const Edict* shooter); void notifyClientOnShot(const Edict* target, int step); private: ReactionFireTargetList rfData[MAX_RF_DATA]; ReactionFireTargetList* find (const Edict* shooter); }; static ReactionFireTargets rft; /** * @brief Initialize the reaction fire table for all entities. */ void ReactionFireTargets::init (void) { for (int i = 0; i < MAX_RF_DATA; i++) { rfData[i].init(); } } /** * @brief Reset the target count in the reaction fire table for all entities. */ void ReactionFireTargets::reset (void) { for (int i = 0; i < MAX_RF_DATA; i++) { rfData[i].reset(); } } void ReactionFireTargets::notifyClientOnStep (const Edict* target, int step) { for (int i = 0; i < MAX_RF_DATA; i++) { ReactionFireTargetList *rfts = &rfData[i]; if (rfts->entnum == RF_NO_ENTNUM) continue; const Edict* shooter = G_EdictsGetByNum(rfts->entnum); for (int j = 0; j < rfts->count; j++) { ReactionFireTarget& t = rfts->targets[j]; if (t.target != target) continue; const int tus = std::max(0, target->TU - t.triggerTUs); G_EventReactionFireTargetUpdate(*shooter, *target, tus, step); } } } void ReactionFireTargets::notifyClientOnShot (const Edict* target, int tusTarget) { for (int i = 0; i < MAX_RF_DATA; i++) { ReactionFireTargetList* rfts = &rfData[i]; if (rfts->entnum == RF_NO_ENTNUM) continue; const Edict* shooter = G_EdictsGetByNum(rfts->entnum); for (int j = 0; j < rfts->count; j++) { ReactionFireTarget& t = rfts->targets[j]; if (t.target != target) continue; const int tus = std::max(0, target->TU - tusTarget - t.triggerTUs); G_EventReactionFireTargetUpdate(*shooter, *target, tus, MAX_ROUTE); } } } void ReactionFireTargets::notifyClientMove (const Edict* target, int step, bool startMove) { for (int i = 0; i < MAX_RF_DATA; i++) { ReactionFireTargetList *rfts = &rfData[i]; if (rfts->entnum == RF_NO_ENTNUM) continue; const Edict* shooter = G_EdictsGetByNum(rfts->entnum); for (int j = 0; j < rfts->count; j++) { if (rfts->targets[j].target != target) continue; if (startMove) { const int tus = target->TU - rfts->targets[j].triggerTUs; G_EventReactionFireAddTarget(*shooter, *target, tus, step); } else { G_EventReactionFireRemoveTarget(*shooter, *target, step); } } } } /** * @brief Find the given edict's table of reaction fire targets. * @param[in] shooter The reaction firing actor */ ReactionFireTargetList* ReactionFireTargets::find (const Edict* shooter) { for (int i = 0; i < MAX_RF_DATA; i++) { ReactionFireTargetList *rfts = &rfData[i]; if (rfts->entnum == shooter->number) { return rfts; } } return nullptr; } /** * @brief Create a table of reaction fire targets for the given edict. * @param[in] shooter The reaction firing actor */ void ReactionFireTargets::create (const Edict* shooter) { ReactionFireTargetList *rfts = find(shooter); if (rfts) gi.Error("Entity already has rfData"); for (int i = 0; i < MAX_RF_DATA; i++) { ReactionFireTargetList& data = rfData[i]; if (data.entnum != RF_NO_ENTNUM) continue; data.entnum = shooter->number; return; } gi.Error("Not enough rfData"); } /** * @brief Add a reaction fire target for the given shooter. * @param[in] shooter The reaction firing actor * @param[in] target The potential reaction fire victim * @param[in] tusForShot The TUs needed for the shot */ void ReactionFireTargets::add (const Edict* shooter, const Edict* target, const int tusForShot) { int i; ReactionFireTargetList *rfts = find(shooter); assert(rfts); assert(target); for (i = 0; i < rfts->count; i++) { /* check if shooter already knows that target */ if (rfts->targets[i].target == target) return; } if (i >= MAX_RF_TARGETS) return; rfts->targets[i].target = target; rfts->targets[i].triggerTUs = target->TU - tusForShot; rfts->count++; G_EventReactionFireAddTarget(*shooter, *target, tusForShot, target->moveinfo.steps - 1); #if DEBUG_RF if (!(G_IsAlien(shooter) || G_IsCivilian(shooter))) Com_Printf("S%i: added\n", shooter->number); #endif } /** * @brief Remove a reaction fire target for the given shooter. * @param[in] shooter The reaction firing actor * @param[in] target The potential reaction fire victim */ void ReactionFireTargets::remove (const Edict* shooter, const Edict* target) { ReactionFireTargetList *rfts = find(shooter); assert(rfts); assert(target); for (int i = 0; i < rfts->count; i++) { ReactionFireTarget &t = rfts->targets[i]; if (t.target != target) continue; /* not the last one? */ if (i != rfts->count - 1) { t.target = rfts->targets[rfts->count - 1].target; t.triggerTUs = rfts->targets[rfts->count - 1].triggerTUs; } rfts->count--; G_EventReactionFireRemoveTarget(*shooter, *target, target->moveinfo.steps - 1); #if DEBUG_RF if (!(G_IsAlien(shooter) || G_IsCivilian(shooter))) Com_Printf("S%i: removed\n", shooter->number); #endif } } void ReactionFireTargets::resetTargetList (const Edict* shooter) { ReactionFireTargetList* rfts = find(shooter); for (int i = rfts->count - 1; i >= 0; --i) remove(shooter, rfts->targets[i].target); rfts->reset(); } /** * @brief Check if the given shooter is ready to reaction fire at the given target. * @param[in] shooter The reaction firing actor * @param[in] target The potential reaction fire victim * @return The TUs the target will need to reach until the RF shot goes off. */ int ReactionFireTargets::getTriggerTUs (const Edict* shooter, const Edict* target) { const ReactionFireTargetList *rfts = find(shooter); if (!rfts) return -2; /* the shooter doesn't aim at anything */ assert(target); for (int i = 0; i < rfts->count; i++) { const ReactionFireTarget &t = rfts->targets[i]; if (t.target == target) return t.triggerTUs; } return -1; /* the shooter doesn't aim at this target */ } /** * @brief Check if the given shooter is ready to reaction fire at the given target. * @param[in] shooter The reaction firing actor * @param[in] target The potential reaction fire victim * @param[in] tusTarget The TUs the target will need for the shot, 0 for just moving */ bool ReactionFireTargets::hasExpired (const Edict* shooter, const Edict* target, const int tusTarget) { const ReactionFireTargetList *rfts = find(shooter); if (!rfts) return false; /* the shooter doesn't aim at anything */ assert(target); for (int i = 0; i < rfts->count; i++) { const ReactionFireTarget &t = rfts->targets[i]; if (t.target == target) return t.triggerTUs >= target->TU - tusTarget; } return false; /* the shooter doesn't aim at this target */ } /** * @brief Increase the triggertime for the next RF shot for all targets of the shooter (after a reaction fire). * @param[in] shooter The reaction firing actor * @param[in] tusShot The TUs the shooter will need for the next shot */ void ReactionFireTargets::advance (const Edict* shooter, const int tusShot) { ReactionFireTargetList *rfts = find(shooter); assert(rfts); for (int i = 0; i < rfts->count; i++) { ReactionFireTarget &t = rfts->targets[i]; t.triggerTUs -= tusShot; } } /** * @brief free function to initialize the reaction fire table for all entities. */ void G_ReactionFireTargetsInit (void) { rft.init(); } /** * @brief free function to create a table of reaction fire targets for the given edict. * @param[in] shooter The reaction firing actor */ void G_ReactionFireTargetsCreate (const Edict* shooter) { rft.create(shooter); } class ReactionFire { private: bool isEnemy(const Edict* shooter, const Edict* target) const; bool canReact(Edict* shooter, const Edict* target) const; bool canSee(const Edict* shooter, const Edict* target) const; bool shoot(Edict* shooter, const pos3_t at, shoot_types_t type, fireDefIndex_t firemode); bool isPossible(Edict* shooter, const Edict* target) const; public: void notifyClientOnStep(const Edict* target, int step); bool checkExecution(const Edict* target); void updateAllTargets(const Edict* target); bool tryToShoot(Edict* shooter, const Edict* target); bool isInWeaponRange(const Edict* shooter, const Edict* target, const fireDef_t* fd) const; const fireDef_t* getFireDef(const Edict* shooter) const; void resetTargets(const Edict* shooter); void notifyClientOnShot(const Edict* target, int tusTarget); }; static ReactionFire rf; /** * @brief Get the fireDef for the RF settings of the shooter. * @param[in] shooter The reaction firing actor * @return nullptr if something is wrong */ const fireDef_t* ReactionFire::getFireDef (const Edict* shooter) const { const FiremodeSettings *fmSetting = &shooter->chr.RFmode; if (!fmSetting->isSaneFiremode()) return nullptr; const Item* weapon = shooter->getHandItem(fmSetting->getHand()); if (weapon && weapon->ammoDef() && weapon->isWeapon() && !weapon->mustReload()) { const fireDef_t* fdArray = weapon->getFiredefs(); if (fdArray == nullptr) return nullptr; const fireDefIndex_t fmIdx = fmSetting->getFmIdx(); return &fdArray[fmIdx]; } return nullptr; } bool ReactionFire::isInWeaponRange (const Edict* shooter, const Edict* target, const fireDef_t* fd) const { assert(fd); return fd->range >= VectorDist(shooter->origin, target->origin); } /** * @brief Get the weapon firing TUs of the item in the hand of the shooter. * @return -1 if no firedef was found for the item or the reaction fire mode is not activated. * @param[in] shooter The reaction firing actor * @param[in] target The target to check reaction fire for (e.g. check whether the weapon that was marked for * using in reaction fire situations can handle the distance between the shooter and the target) */ static int G_ReactionFireGetTUsForItem (const Edict* shooter, const Edict* target) { const fireDef_t* fd = rf.getFireDef(shooter); if (!fd) return -1; const int tus = G_ActorGetModifiedTimeForFiredef(shooter, fd, true); if (tus <= shooter->TU && rf.isInWeaponRange(shooter, target, fd)) { return tus; } return -1; } /** * @brief Checks if the currently selected firemode is usable with the defined weapon. * @param[in] actor The actor to check the firemode for. */ static bool G_ActorHasWorkingFireModeSet (const Edict* actor) { const FiremodeSettings *fmSettings = &actor->chr.RFmode; if (!fmSettings->isSaneFiremode()) /* just checks for valid values */ return false; const Item* weapon = actor->getHandItem(fmSettings->getHand()); if (!weapon) return false; const fireDef_t* fd = weapon->getFiredefs(); if (fd == nullptr) return false; if (fd->obj->weapons[fd->weapFdsIdx] == fmSettings->getWeapon() && fmSettings->getFmIdx() < fd->obj->numFiredefs[fd->weapFdsIdx]) { return true; } return false; } /** * @brief Updates the reaction fire settings in case something was moved into a hand or from a hand * that would make the current settings invalid * @param[in,out] ent The actor edict to check the settings for * @param[in] fmIdx The fire mode index that should be used for reaction fire * @param[in] hand The hand that should be used for reaction fire * @param[in] od The object/weapon for the reaction fire */ void G_ReactionFireSettingsUpdate (Edict* ent, fireDefIndex_t fmIdx, actorHands_t hand, const objDef_t* od) { ent->chr.RFmode.set(hand, fmIdx, od); /* FiremodeSettings */ if (!G_ActorHasWorkingFireModeSet(ent)) { /* Disable reaction fire if no valid firemode was found. */ G_ClientStateChange(ent->getPlayer(), ent, ~STATE_REACTION, true); return; } G_EventReactionFireChange(*ent); /* If reaction fire is active, update the reserved TUs */ if (G_IsReaction(ent)) { G_ReactionFireSettingsReserveTUs(ent); } } /** * @brief Checks whether an actor has enough TUs left to activate reaction fire. * @param[in] ent The actors edict to check for TUs for * @return @c true if the given actor has enough TUs left to activate reaction fire, @c false otherwise. */ static bool G_ActorHasEnoughTUsReactionFire (const Edict* ent) { const int TUs = G_ActorGetTUForReactionFire(ent); const chrReservations_t* res = &ent->chr.reservedTus; return ent->TU - TUs >= res->shot + res->crouch; } /** * @param ent The actor to set the reaction fire for * @return @c true if the needed settings could have been made or settings are * already valid, @c false otherwise. */ static bool G_ReactionFireSettingsSetDefault (Edict* ent) { if (G_ActorHasWorkingFireModeSet(ent)) return true; actorHands_t hand = ACTOR_HAND_RIGHT; const Item* item = ent->getHandItem(hand); if (!item) { hand = ACTOR_HAND_LEFT; item = ent->getHandItem(hand); } if (!item) return false; const objDef_t* weapon = item->getReactionFireWeaponType(); if (!weapon) return false; ent->chr.RFmode.set(hand, 0, weapon); /* no special firemode */ if (!G_ActorHasWorkingFireModeSet(ent)) return false; if (!G_IsAI(ent)) G_EventReactionFireChange(*ent); return true; } /** * @brief Checks whether the actor is allowed to activate reaction fire and will informs the player about * the reason if this would not work. * @param[in] ent The actor to check * @return @c true if the actor is allowed to activate it, @c false otherwise */ static bool G_ReactionFireCanBeEnabled (const Edict* ent) { /* check ent is a suitable shooter */ if (!ent->inuse || !G_IsLivingActor(ent)) return false; if (G_MatchIsRunning() && ent->team != level.activeTeam) return false; /* actor may not carry weapons at all - so no further checking is needed */ if (!ent->chr.teamDef->weapons) return false; if (!ent->chr.inv.holdsReactionFireWeapon()) { G_ClientPrintf(ent->getPlayer(), PRINT_HUD, _("No reaction fire enabled weapon.")); return false; } if (!G_ActorHasWorkingFireModeSet(ent)) { G_ClientPrintf(ent->getPlayer(), PRINT_HUD, _("No fire mode selected for reaction fire.")); return false; } if (!G_ActorHasEnoughTUsReactionFire(ent)) { G_ClientPrintf(ent->getPlayer(), PRINT_HUD, _("Not enough TUs left for activating reaction fire.")); return false; } return true; } /** * @brief Set the reaction fire TU reservation for an actor * @param[in,out] ent The actor edict to set the TUs for * @return @c true if TUs for reaction fire were reserved, @c false if the reservation was set * back to @c 0 */ bool G_ReactionFireSettingsReserveTUs (Edict* ent) { if (G_ReactionFireSettingsSetDefault(ent) && G_ReactionFireCanBeEnabled(ent)) { const int TUs = G_ActorGetTUForReactionFire(ent); /* Enable requested reaction fire. */ G_ActorReserveTUs(ent, TUs, ent->chr.reservedTus.shot, ent->chr.reservedTus.crouch); return true; } G_ActorReserveTUs(ent, 0, ent->chr.reservedTus.shot, ent->chr.reservedTus.crouch); return false; } inline bool ReactionFire::isPossible (Edict* shooter, const Edict* target) const { return isEnemy(shooter, target) && canReact(shooter, target) && canSee(shooter, target); } /** * @brief Check whether we want to shoot at the target. * @param[in] shooter The entity that might be firing * @param[in] target The entity that might be fired at */ bool ReactionFire::isEnemy (const Edict* shooter, const Edict* target) const { /* an entity can't reaction fire at itself */ if (shooter == target) return false; /* Don't react in your own turn */ if (shooter->team == level.activeTeam) return false; if (G_IsDead(target)) return false; /* If reaction fire is triggered by a friendly unit * and the shooter is still sane, don't shoot; * well, if the shooter isn't sane anymore... */ if (G_IsCivilian(target) || target->team == shooter->team) if (!G_IsShaken(shooter) || (float) shooter->morale / mor_shaken->value > frand()) return false; return true; } /** * @brief Check whether shooter can reaction fire at target at all. * @param[in] shooter The entity that might be firing * @param[in] target The entity that might be fired at */ bool ReactionFire::canReact (Edict* shooter, const Edict* target) const { /* shooter can't use RF if is in STATE_DAZED (flashbang impact) */ if (G_IsDazed(shooter)) return false; /* check shooter has reaction fire enabled */ if (!G_IsReaction(shooter)) return false; /* check shooter has weapon in RF hand */ if (!shooter->getHandItem(shooter->chr.RFmode.getHand())) { /* print character info if this happens, for now */ gi.DPrintf("Reaction fire enabled but no weapon for hand (name=%s,entnum=%i,hand=%i,fmIdx=%i)\n", shooter->chr.name, shooter->number, shooter->chr.RFmode.getHand(), shooter->chr.RFmode.getFmIdx()); G_RemoveReaction(shooter); return false; } return true; } /** * @brief Check whether shooter can see his target well enough * @param[in] shooter The entity that might be firing * @param[in] target The entity that might be fired at */ bool ReactionFire::canSee (const Edict* shooter, const Edict* target) const { if (!G_IsVisibleForTeam(target, shooter->team)) return false; /* check in range and visible */ const int spotDist = G_VisCheckDist(shooter); if (VectorDistSqr(shooter->origin, target->origin) > spotDist * spotDist) return false; const bool frustum = G_FrustumVis(shooter, target->origin); if (!frustum) return false; const float actorVis = G_ActorVis(shooter->origin, shooter, target, true); if (actorVis < 0.1) return false; return true; } /** * @brief Check whether 'target' has just triggered any new reaction fire * @param[in] target The entity triggering fire */ void ReactionFire::updateAllTargets (const Edict* target) { Edict* shooter = nullptr; /* check all possible shooters */ while ((shooter = G_EdictsGetNextLivingActor(shooter))) { /* check whether reaction fire is possible (friend/foe, LoS) */ if (isPossible(shooter, target)) { const int TUs = G_ReactionFireGetTUsForItem(shooter, target); if (TUs < 0) continue; /* no suitable weapon */ rft.add(shooter, target, TUs); } else { rft.remove(shooter, target); } } } void ReactionFire::resetTargets (const Edict *shooter) { rft.resetTargetList(shooter); } /** * @brief Perform the reaction fire shot * @param[in] shooter The actor that is trying to shoot * @param[in] at Position to fire on. * @param[in] type What type of shot this is (left, right reaction-left etc...). * @param[in] firemode The firemode index of the ammo for the used weapon (objDef.fd[][x]) . * @return true if everything went ok (i.e. the shot(s) where fired ok), otherwise false. * @sa G_ClientShoot */ bool ReactionFire::shoot (Edict* shooter, const pos3_t at, shoot_types_t type, fireDefIndex_t firemode) { const int minhit = 30; shot_mock_t mock; int i; const Player &player = shooter->getPlayer(); /* this is the max amount of friendly units that were hit during the mock calculation */ int maxff; if (G_IsInsane(shooter)) maxff = 100; else if (G_IsRaged(shooter)) maxff = 60; else if (G_IsPanicked(shooter)) maxff = 30; else if (G_IsShaken(shooter)) maxff = 15; else maxff = 5; /* calculate the mock values - e.g. how many friendly units we would hit * when opening the reaction fire */ for (i = 0; i < 100; i++) if (!G_ClientShoot(player, shooter, at, type, firemode, &mock, false, 0)) break; const int ff = mock.friendCount + (G_IsAlien(shooter) ? 0 : mock.civilian); if (ff <= maxff && mock.enemyCount >= minhit) return G_ClientShoot(player, shooter, at, type, firemode, nullptr, false, 0); return false; } /** * @brief Resolve the reaction fire for an entity, this checks that the entity can fire and then takes the shot * @param[in] shooter The entity using reaction fire * @param[in] target The victim of the reaction fire * @return true if the entity fired, false otherwise */ bool ReactionFire::tryToShoot (Edict* shooter, const Edict* target) { /* check for valid target */ assert(target); /* shooter can't take a reaction shot if it's not possible - and check that * the target is still alive */ if (!isPossible(shooter, target)) { rft.remove(shooter, target); return false; } /* take the shot */ const bool tookShot = rf.shoot(shooter, target->pos, ST_RIGHT_REACTION, shooter->chr.RFmode.getFmIdx()); if (tookShot) { /* clear any shakenness */ G_RemoveShaken(shooter); } return tookShot; } void ReactionFire::notifyClientOnShot (const Edict* target, int tusTarget) { rft.notifyClientOnShot(target, tusTarget); } void ReactionFire::notifyClientOnStep (const Edict* target, int step) { rft.notifyClientOnStep(target, step); } /** * @brief Check all entities to see whether target has caused reaction fire to resolve. * @param[in] target The entity that might be resolving reaction fire * @returns whether any entity fired (or would fire) upon target * @sa G_ReactionFireOnMovement * @sa G_ReactionFirePostShot */ bool ReactionFire::checkExecution (const Edict* target) { Edict* shooter = nullptr; bool fired = false; /* check all possible shooters */ while ((shooter = G_EdictsGetNextLivingActor(shooter))) { const int tus = G_ReactionFireGetTUsForItem(shooter, target); /* indicates an RF weapon is there */ if (tus <= 1) continue; if (!rft.hasExpired(shooter, target, 0)) continue; if (!rf.tryToShoot(shooter, target)) continue; rft.advance(shooter, tus); fired |= true; } return fired; } #if DEBUG_RF /** * @brief Prints some reaction fire data to the console * @param[in] target The target entity */ static void G_ReactionFirePrintSituation (Edict* target) { if (!G_IsAlien(target)) return; Com_Printf("Alien %i at %i/%i/%i TU:%i\n", target->number, target->pos[0], target->pos[1], target->pos[2], target->TU); Edict* shooter = nullptr; /* check all possible shooters */ while ((shooter = G_EdictsGetNextLivingActor(shooter))) { if (G_IsAlien(shooter) || G_IsCivilian(shooter)) continue; char msgHdr[100]; Com_sprintf(msgHdr, sizeof(msgHdr), "S%i: at %i/%i/%i RF: ", shooter->number, shooter->pos[0], shooter->pos[1], shooter->pos[2]); int ttus = rft.getTriggerTUs(shooter, target); if (ttus == -2) Com_Printf("%s not initialized\n", msgHdr); if (ttus == -1) Com_Printf("%s not aiming\n", msgHdr); else if (rft.hasExpired(shooter, target, 0)) Com_Printf("expired\n", msgHdr); else Com_Printf("%s not yet: %i\n", msgHdr, ttus); } } #endif /** * @brief Called when 'target' moves, possibly triggering or resolving reaction fire * @param[in] target The target entity * @return true If any shots were (or would be) taken * @sa G_ClientMove */ bool G_ReactionFireOnMovement (Edict* target, int step) { #if DEBUG_RF G_ReactionFirePrintSituation(target); #endif rf.notifyClientOnStep(target, step); /* Check to see whether this resolves any reaction fire */ const bool fired = rf.checkExecution(target); /* Check to see whether this triggers any reaction fire */ rf.updateAllTargets(target); return fired; } static void G_ReactionFireNofityClientStartShot (const Edict* target) { rft.notifyClientMove(target, MAX_ROUTE, true); } static void G_ReactionFireNofityClientEndShot (const Edict* target) { rft.notifyClientMove(target, MAX_ROUTE, false); } /** * @brief Called when 'target' is about to shoot, this forces a 'draw' to decide who gets the first shot * @param[in] target The entity about to shoot * @param[in] fdTime The TU of the shot * @sa G_ClientShoot */ void G_ReactionFirePreShot (const Edict* target, const int fdTime) { bool repeat = true; /* Check to see whether this triggers any reaction fire */ G_ReactionFireNofityClientStartShot(target); rf.updateAllTargets(target); rf.notifyClientOnShot(target, fdTime); /* if any reaction fire occurs, we have to loop through all entities again to allow * multiple (fast) RF snap shots before a (slow) aimed shot from the target occurs. */ while (repeat) { Edict* shooter = nullptr; repeat = false; /* check all ents to see who wins and who loses a draw */ while ((shooter = G_EdictsGetNextLivingActor(shooter))) { const int entTUs = G_ReactionFireGetTUsForItem(shooter, target); /* indicates an RF weapon is there */ if (entTUs <= 1) continue; if (!rft.hasExpired(shooter, target, fdTime)) continue; if (!rf.tryToShoot(shooter, target)) continue; repeat = true; rft.advance(shooter, fdTime); } } } /** * @brief Removes the given target from the reaction fire lists * @param[in] target The target to remove from the lists */ void G_ReactionFireOnDead (const Edict* target) { assert(G_IsDead(target)); rf.updateAllTargets(target); rf.resetTargets(target); } /** * @brief Called after 'target' has fired, this might trigger more reaction fire or resolve outstanding reaction fire (because target is out of time) * @param[in] target The entity that has just fired * @sa G_ClientShoot */ void G_ReactionFirePostShot (Edict* target) { /* Check to see whether this resolves any reaction fire */ rf.notifyClientOnShot(target, 0); rf.checkExecution(target); G_ReactionFireNofityClientEndShot(target); } /** * @brief Called at the end of turn, all outstanding reaction fire is resolved * @sa G_ClientEndRound */ void G_ReactionFireOnEndTurn (void) { /* we explicitly do nothing at end of turn, just reset the table */ rft.reset(); } /** * @brief Guess! Reset all "shaken" states on end of turn? * @note Normally called on end of turn. * @sa G_ClientStateChange * @param[in] team Index of team to loop through. */ void G_ReactionFireReset (int team) { Edict* ent = nullptr; while ((ent = G_EdictsGetNextLivingActorOfTeam(ent, team))) { G_RemoveShaken(ent); } } void G_ReactionFireNofityClientStartMove (const Edict* target) { /* note that this is sent _before_ the actual move event, so we can't use the step number */ rft.notifyClientMove(target, MAX_ROUTE, true); } void G_ReactionFireNofityClientEndMove (const Edict* target) { rft.notifyClientMove(target, target->moveinfo.steps - 1, false); }