/* * Touch_Item */ void Touch_Item( edict_t *ent, edict_t *other, cplane_t *plane, int surfFlags ) { bool taken; const gsitem_t *item = ent->item; if( !other->r.client || G_ISGHOSTING( other ) ) return; if( !( other->r.client->ps.pmove.stats[PM_STAT_FEATURES] & PMFEAT_ITEMPICK ) ) return; if( !item || !( item->flags & ITFLAG_PICKABLE ) ) return; // not a grabbable item if( !G_Gametype_CanPickUpItem( item ) ) return; taken = G_PickupItem( other, item, ent->spawnflags, ent->count, ent->invpak ); if( !( ent->spawnflags & ITEM_TARGETS_USED ) ) { G_UseTargets( ent, other ); ent->spawnflags |= ITEM_TARGETS_USED; } if( !taken ) return; if( ent->spawnflags & ITEM_TIMED ) ent->r.owner = other; // flash the screen G_AddPlayerStateEvent( other->r.client, PSEV_PICKUP, ( item->flags & IT_WEAPON ? item->tag : 0 ) ); G_AwardPlayerPickup( other, ent ); // for messages other->r.client->teamstate.last_pickup = ent; // show icon and name on status bar other->r.client->ps.stats[STAT_PICKUP_ITEM] = item->tag; other->r.client->resp.pickup_msg_time = level.time + 3000; if( ent->attenuation ) Touch_ItemSound( other, item ); if( !( ent->spawnflags & DROPPED_ITEM ) && G_Gametype_CanRespawnItem( item ) ) { if( (item->type & IT_WEAPON ) && GS_RaceGametype() ) return; // weapons stay in race SetRespawn( ent, G_Gametype_RespawnTimeForItem( item ) ); return; } G_FreeEdict( ent ); }
/* * Touch_Item */ void Touch_Item( edict_t *ent, edict_t *other, cplane_t *plane, int surfFlags ) { qboolean taken; if( !other->r.client || G_ISGHOSTING( other ) ) return; if( !( other->r.client->ps.pmove.stats[PM_STAT_FEATURES] & PMFEAT_ITEMPICK ) ) return; if( !ent->item || !( ent->item->flags & ITFLAG_PICKABLE ) ) return; // not a grabbable item if( !G_Gametype_CanPickUpItem( ent->item ) ) return; taken = G_PickupItem( ent, other ); if( !( ent->spawnflags & ITEM_TARGETS_USED ) ) { G_UseTargets( ent, other ); ent->spawnflags |= ITEM_TARGETS_USED; } if( !taken ) return; // flash the screen G_AddPlayerStateEvent( other->r.client, PSEV_PICKUP, ( ent->item->flags & IT_WEAPON ? ent->item->tag : 0 ) ); G_AwardPlayerPickup( other, ent ); // for messages other->r.client->teamstate.last_pickup = ent; // show icon and name on status bar other->r.client->ps.stats[STAT_PICKUP_ITEM] = ent->item->tag; other->r.client->resp.pickup_msg_time = level.time + 3000; if( ent->attenuation ) Touch_ItemSound( other, ent->item ); if( !( ent->spawnflags & ( DROPPED_ITEM | DROPPED_PLAYER_ITEM ) ) && G_Gametype_CanRespawnItem( ent->item ) ) SetRespawn( ent, G_Gametype_RespawnTimeForItem( ent->item ) ); else G_FreeEdict( ent ); }
//========================================== // AI_PickShortRangeGoal // Pick best goal based on importance and range. This function // overrides the long range goal selection for items that // are very close to the bot and are reachable. //========================================== static void AI_PickShortRangeGoal( edict_t *self ) { edict_t *bestGoal = NULL; float bestWeight = 0; nav_ents_t *goalEnt; const gsitem_t *item; bool canPickupItems; int i; if( !self->r.client || G_ISGHOSTING( self ) ) return; if( self->ai->state_combat_timeout > level.time ) { self->ai->shortRangeGoalTimeout = self->ai->state_combat_timeout; return; } if( self->ai->shortRangeGoalTimeout > level.time ) return; canPickupItems = (self->r.client->ps.pmove.stats[PM_STAT_FEATURES] & PMFEAT_ITEMPICK) != 0 ? true : false; self->ai->shortRangeGoalTimeout = level.time + AI_SHORT_RANGE_GOAL_DELAY; self->movetarget = NULL; FOREACH_GOALENT( goalEnt ) { float dist; i = goalEnt->id; if( !goalEnt->ent->r.inuse || goalEnt->ent->r.solid == SOLID_NOT ) continue; if( goalEnt->ent->r.client ) continue; if( self->ai->status.entityWeights[i] <= 0.0f ) continue; item = goalEnt->ent->item; if( canPickupItems && item ) { if( !G_Gametype_CanPickUpItem( item ) || !( item->flags & ITFLAG_PICKABLE ) ) { continue; } } dist = DistanceFast( self->s.origin, goalEnt->ent->s.origin ); if( goalEnt == self->ai->goalEnt ) { if( dist > AI_GOAL_SR_LR_RADIUS ) continue; } else { if( dist > AI_GOAL_SR_RADIUS ) continue; } clamp_low( dist, 0.01f ); if( AI_ShortRangeReachable( self, goalEnt->ent->s.origin ) ) { float weight; bool in_front = G_InFront( self, goalEnt->ent ); // Long range goal gets top priority if( in_front && goalEnt == self->ai->goalEnt ) { bestGoal = goalEnt->ent; break; } // get the one with the best weight weight = self->ai->status.entityWeights[i] / dist * (in_front ? 1.0f : 0.5f); if( weight > bestWeight ) { bestWeight = weight; bestGoal = goalEnt->ent; } } } if( bestGoal ) { self->movetarget = bestGoal; if( nav.debugMode && bot_showsrgoal->integer ) G_PrintChasersf( self, "%i %s: selected a %s for SR goal.\n", level.framenum, self->ai->pers.netname, self->movetarget->classname ); } else { // got nothing else to do so keep scanning self->ai->shortRangeGoalTimeout = level.time + AI_SHORT_RANGE_GOAL_DELAY_IDLE; } }
//========================================== // AI_PickLongRangeGoal // // Evaluate the best long range goal and send the bot on // its way. This is a good time waster, so use it sparingly. // Do not call it for every think cycle. // // jal: I don't think there is any problem by calling it, // now that we have stored the costs at the nav.costs table (I don't do it anyway) //========================================== void AI_PickLongRangeGoal( edict_t *self ) { #define WEIGHT_MAXDISTANCE_FACTOR 20000.0f #define COST_INFLUENCE 0.5f int i; float weight, bestWeight = 0.0; int current_node; float cost; float dist; nav_ents_t *goalEnt, *bestGoalEnt = NULL; AI_ClearGoal( self ); if( G_ISGHOSTING( self ) ) return; if( self->ai->longRangeGoalTimeout > level.time ) return; if( !self->r.client->ps.pmove.stats[PM_STAT_MAXSPEED] ) { return; } self->ai->longRangeGoalTimeout = level.time + AI_LONG_RANGE_GOAL_DELAY + brandom( 0, 1000 ); // look for a target current_node = AI_FindClosestReachableNode( self->s.origin, self, ( ( 1 + self->ai->nearest_node_tries ) * NODE_DENSITY ), NODE_ALL ); self->ai->current_node = current_node; if( current_node == NODE_INVALID ) { if( nav.debugMode && bot_showlrgoal->integer ) G_PrintChasersf( self, "%s: LRGOAL: Closest node not found. Tries:%i\n", self->ai->pers.netname, self->ai->nearest_node_tries ); self->ai->nearest_node_tries++; // extend search radius with each try return; } self->ai->nearest_node_tries = 0; // Run the list of potential goal entities FOREACH_GOALENT( goalEnt ) { i = goalEnt->id; if( !goalEnt->ent ) continue; if( !goalEnt->ent->r.inuse ) { goalEnt->node = NODE_INVALID; continue; } if( goalEnt->ent->r.client ) { if( G_ISGHOSTING( goalEnt->ent ) || goalEnt->ent->flags & (FL_NOTARGET|FL_BUSY) ) goalEnt->node = NODE_INVALID; else goalEnt->node = AI_FindClosestReachableNode( goalEnt->ent->s.origin, goalEnt->ent, NODE_DENSITY, NODE_ALL ); } if( goalEnt->ent->item ) { if( !G_Gametype_CanPickUpItem( goalEnt->ent->item ) ) continue; } if( goalEnt->node == NODE_INVALID ) continue; weight = self->ai->status.entityWeights[i]; if( weight <= 0.0f ) continue; // don't try to find cost for too far away objects dist = DistanceFast( self->s.origin, goalEnt->ent->s.origin ); if( dist > WEIGHT_MAXDISTANCE_FACTOR * weight/* || dist < AI_GOAL_SR_RADIUS*/ ) continue; cost = AI_FindCost( current_node, goalEnt->node, self->ai->status.moveTypesMask ); if( cost == NODE_INVALID ) continue; cost -= brandom( 0, 2000 ); // allow random variations clamp_low( cost, 1 ); weight = ( 1000 * weight ) / ( cost * COST_INFLUENCE ); // Check against cost of getting there if( weight > bestWeight ) { bestWeight = weight; bestGoalEnt = goalEnt; } } if( bestGoalEnt ) { self->ai->goalEnt = bestGoalEnt; AI_SetGoal( self, bestGoalEnt->node ); if( self->ai->goalEnt != NULL && nav.debugMode && bot_showlrgoal->integer ) G_PrintChasersf( self, "%s: selected a %s at node %d for LR goal. (weight %f)\n", self->ai->pers.netname, self->ai->goalEnt->ent->classname, self->ai->goalEnt->node, bestWeight ); return; } if( nav.debugMode && bot_showlrgoal->integer ) G_PrintChasersf( self, "%s: did not find a LR goal.\n", self->ai->pers.netname ); #undef WEIGHT_MAXDISTANCE_FACTOR #undef COST_INFLUENCE }
SelectedNavEntity BotItemsSelector::SuggestGoalNavEntity( const SelectedNavEntity &currSelectedNavEntity ) { UpdateInternalItemAndGoalWeights(); StaticVector<NavEntityAndWeight, MAX_NAVENTS> rawWeightCandidates; const auto levelTime = level.time; auto *navEntitiesRegistry = NavEntitiesRegistry::Instance(); for( auto it = navEntitiesRegistry->begin(), end = navEntitiesRegistry->end(); it != end; ++it ) { const NavEntity *navEnt = *it; if( navEnt->IsDisabled() ) { continue; } // We cannot just set a zero internal weight for a temporarily disabled nav entity // (it might be overridden by an external weight, and we should not modify external weights // as script users expect them remaining the same unless explicitly changed via script API) if( disabledForSelectionUntil[navEnt->Id()] >= levelTime ) { continue; } // Since movable goals have been introduced (and clients qualify as movable goals), prevent picking itself as a goal. if( navEnt->Id() == ENTNUM( self ) ) { continue; } if( navEnt->Item() && !G_Gametype_CanPickUpItem( navEnt->Item() ) ) { continue; } // Reject an entity quickly if it looks like blocked by an enemy that is close to the entity. // Note than passing this test does not guarantee that entire path to the entity is not blocked by enemies. if( self->ai->botRef->routeCache->AreaDisabled( navEnt->AasAreaNum() ) ) { continue; } // This is a coarse and cheap test, helps to reject recently picked armors and powerups unsigned spawnTime = navEnt->SpawnTime(); // A feasible spawn time (non-zero) always >= level.time. if( !spawnTime || level.time - spawnTime > 15000 ) { continue; } float weight = GetEntityWeight( navEnt->Id() ); if( weight > 0 ) { rawWeightCandidates.push_back( NavEntityAndWeight( navEnt, weight ) ); } } // Sort all pre-selected candidates by their raw weights std::sort( rawWeightCandidates.begin(), rawWeightCandidates.end() ); // Try checking whether the bot is in some floor cluster to give a greater weight for items in the same cluster int currFloorClusterNum = 0; const auto &entityPhysicsState = self->ai->botRef->EntityPhysicsState(); const auto *aasFloorClusterNums = AiAasWorld::Instance()->AreaFloorClusterNums(); if( aasFloorClusterNums[entityPhysicsState->CurrAasAreaNum()] ) { currFloorClusterNum = aasFloorClusterNums[entityPhysicsState->CurrAasAreaNum()]; } else if( aasFloorClusterNums[entityPhysicsState->DroppedToFloorAasAreaNum()] ) { currFloorClusterNum = aasFloorClusterNums[entityPhysicsState->DroppedToFloorAasAreaNum()]; } const NavEntity *currGoalNavEntity = currSelectedNavEntity.navEntity; float currGoalEntWeight = 0.0f; float currGoalEntCost = 0.0f; const NavEntity *bestNavEnt = nullptr; float bestWeight = 0.000001f; float bestNavEntCost = 0.0f; // Test not more than 16 best pre-selected by raw weight candidates. // (We try to avoid too many expensive FindTravelTimeToGoalArea() calls, // thats why we start from the best item to avoid wasting these calls for low-priority items) for( unsigned i = 0, end = std::min( rawWeightCandidates.size(), 16U ); i < end; ++i ) { const NavEntity *navEnt = rawWeightCandidates[i].goal; float weight = rawWeightCandidates[i].weight; unsigned moveDuration = 1; unsigned waitDuration = 1; if( self->ai->botRef->CurrAreaNum() != navEnt->AasAreaNum() ) { // We ignore cost of traveling in goal area, since: // 1) to estimate it we have to retrieve reachability to goal area from last area before the goal area // 2) it is relative low compared to overall travel cost, and movement in areas is cheap anyway moveDuration = self->ai->botRef->botBrain.FindTravelTimeToGoalArea( navEnt->AasAreaNum() ) * 10U; // AAS functions return 0 as a "none" value, 1 as a lowest feasible value if( !moveDuration ) { continue; } if( navEnt->IsDroppedEntity() ) { // Do not pick an entity that is likely to dispose before it may be reached if( navEnt->Timeout() <= level.time + moveDuration ) { continue; } } } unsigned spawnTime = navEnt->SpawnTime(); // The entity is not spawned and respawn time is unknown if( !spawnTime ) { continue; } // Entity origin may be reached at this time unsigned reachTime = level.time + moveDuration; if( reachTime < spawnTime ) { waitDuration = spawnTime - reachTime; } if( waitDuration > navEnt->MaxWaitDuration() ) { continue; } float moveCost = MOVE_TIME_WEIGHT * moveDuration * navEnt->CostInfluence(); float cost = 0.0001f + moveCost + WAIT_TIME_WEIGHT * waitDuration * navEnt->CostInfluence(); weight = ( 1000 * weight ) / cost; // If the bot is inside a floor cluster if( currFloorClusterNum ) { // Greatly increase weight for items in the same floor cluster if( currFloorClusterNum == aasFloorClusterNums[navEnt->AasAreaNum()] ) { weight *= 4.0f; } } // Store current weight of the current goal entity if( currGoalNavEntity == navEnt ) { currGoalEntWeight = weight; // Waiting time is handled by the planner for wait actions separately. currGoalEntCost = moveCost; } if( weight > bestWeight ) { bestNavEnt = navEnt; bestWeight = weight; // Waiting time is handled by the planner for wait actions separately. bestNavEntCost = moveCost; } } if( !bestNavEnt ) { Debug( "Can't find a feasible long-term goal nav. entity\n" ); return SelectedNavEntity( nullptr, std::numeric_limits<float>::max(), 0.0f, level.time + 200 ); } // If it is time to pick a new goal (not just re-evaluate current one), do not be too sticky to the current goal const float currToBestWeightThreshold = currGoalNavEntity != nullptr ? 0.6f : 0.8f; if( currGoalNavEntity && currGoalNavEntity == bestNavEnt ) { constexpr const char *format = "current goal entity %s is kept as still having best weight %.3f\n"; Debug( format, currGoalNavEntity->Name(), bestWeight ); return SelectedNavEntity( bestNavEnt, bestNavEntCost, GetGoalWeight( bestNavEnt->Id() ), level.time + 4000 ); } else if( currGoalEntWeight > 0 && currGoalEntWeight / bestWeight > currToBestWeightThreshold ) { constexpr const char *format = "current goal entity %s is kept as having weight %.3f good enough to not consider picking another one\n"; // If currGoalEntWeight > 0, currLongTermGoalEnt is guaranteed to be non-null Debug( format, currGoalNavEntity->Name(), currGoalEntWeight ); return SelectedNavEntity( currGoalNavEntity, currGoalEntCost, GetGoalWeight( bestNavEnt->Id() ), level.time + 2500 ); } else { if( currGoalNavEntity ) { const char *format = "suggested %s weighted %.3f as a long-term goal instead of %s weighted now as %.3f\n"; Debug( format, bestNavEnt->Name(), bestWeight, currGoalNavEntity->Name(), currGoalEntWeight ); } else { Debug( "suggested %s weighted %.3f as a new long-term goal\n", bestNavEnt->Name(), bestWeight ); } return SelectedNavEntity( bestNavEnt, bestNavEntCost, GetGoalWeight( bestNavEnt->Id() ), level.time + 2500 ); } }