bool AIHasClearShot(const Vec2i from, const Vec2i to) { // Perform 4 line tests - above, below, left and right // This is to account for possible positions for the muzzle Vec2i fromOffset = from; const int pad = 2; fromOffset.x = from.x - (ACTOR_W + pad) / 2; if (Vec2iToTile(fromOffset).x >= 0 && !AIHasClearLine(fromOffset, to, IsPosNoSee)) { return false; } fromOffset.x = from.x + (ACTOR_W + pad) / 2; if (Vec2iToTile(fromOffset).x < gMap.Size.x && !AIHasClearLine(fromOffset, to, IsPosNoSee)) { return false; } fromOffset.x = from.x; fromOffset.y = from.y - (ACTOR_H + pad) / 2; if (Vec2iToTile(fromOffset).y >= 0 && !AIHasClearLine(fromOffset, to, IsPosNoSee)) { return false; } fromOffset.y = from.y + (ACTOR_H + pad) / 2; if (Vec2iToTile(fromOffset).y < gMap.Size.y && !AIHasClearLine(fromOffset, to, IsPosNoSee)) { return false; } return true; }
int AIGoto(TActor *actor, Vec2i p, bool ignoreObjects) { Vec2i a = Vec2iFull2Real(actor->Pos); Vec2i currentTile = Vec2iToTile(a); Vec2i goalTile = Vec2iToTile(p); AIGotoContext *c = &actor->aiContext->Goto; // If we are already there, bail // This can happen if AI is trying to track the player, // but the player has died, for example. if (Vec2iEqual(currentTile, goalTile)) { return 0; } // If we are currently following an A* path, // and it is still valid, keep following it until // we have reached a new tile if (c && c->IsFollowing && AStarCloseToPath(c, currentTile, goalTile)) { return AStarFollow(c, currentTile, &actor->tileItem, a); } else if (AIHasClearPath(a, p, ignoreObjects)) { // Simple case: if there's a clear line between AI and target, // walk straight towards it return AIGotoDirect(a, p); } else { // We need to recalculate A* AStarContext ac; ac.Map = &gMap; ac.IsTileOk = ignoreObjects ? IsTileWalkable : IsTileWalkableAroundObjects; // First, if the goal tile is blocked itself, // find a nearby tile that can be walked to c->Goal = MapSearchTileAround(ac.Map, goalTile, ac.IsTileOk); c->PathIndex = 1; // start navigating to the next path node ASPathDestroy(c->Path); c->Path = ASPathCreate( &cPathNodeSource, &ac, ¤tTile, &c->Goal); // In case we can't calculate A* for some reason, // try simple navigation again if (ASPathGetCount(c->Path) <= 1) { debug( D_MAX, "Error: can't calculate path from {%d, %d} to {%d, %d}", currentTile.x, currentTile.y, goalTile.x, goalTile.y); return AIGotoDirect(a, p); } return AStarFollow(c, currentTile, &actor->tileItem, a); } }
void CollideAllItems( const TTileItem *item, const Vec2i pos, const int mask, const CollisionTeam team, const bool isPVP, CollideItemFunc func, void *data) { const Vec2i tv = Vec2iToTile(pos); Vec2i dv; // Check collisions with all other items on this tile, in all 8 directions for (dv.y = -1; dv.y <= 1; dv.y++) { for (dv.x = -1; dv.x <= 1; dv.x++) { const Vec2i dtv = Vec2iAdd(tv, dv); if (!MapIsTileIn(&gMap, dtv)) { continue; } CArray *tileThings = &MapGetTile(&gMap, dtv)->things; for (int i = 0; i < (int)tileThings->size; i++) { TTileItem *ti = ThingIdGetTileItem(CArrayGet(tileThings, i)); // Don't collide if items are on the same team if (!CollisionIsOnSameTeam(ti, team, isPVP)) { if (item != ti && (ti->flags & mask) && ItemsCollide(item, ti, pos)) { func(ti, data); } } } } } }
TTileItem *GetItemOnTileInCollision( TTileItem *item, Vec2i pos, int mask, CollisionTeam team, int isDogfight) { Vec2i tv = Vec2iToTile(pos); Vec2i dv; if (!MapIsTileIn(&gMap, tv)) { return NULL; } // Check collisions with all other items on this tile, in all 8 directions for (dv.y = -1; dv.y <= 1; dv.y++) { for (dv.x = -1; dv.x <= 1; dv.x++) { CArray *tileThings = &MapGetTile(&gMap, Vec2iAdd(tv, dv))->things; for (int i = 0; i < (int)tileThings->size; i++) { TTileItem *ti = ThingIdGetTileItem(CArrayGet(tileThings, i)); // Don't collide if items are on the same team if (!CollisionIsOnSameTeam(ti, team, isDogfight)) { if (item != ti && (ti->flags & mask) && ItemsCollide(item, ti, pos)) { return ti; } } } } } return NULL; }
// Use pathfinding to check that there is a path between // source and destination tiles bool AIHasPath(const Vec2i from, const Vec2i to, const bool ignoreObjects) { // Quick first test: check there is a clear path if (AIHasClearPath(from, to, ignoreObjects)) { return true; } // Pathfind AStarContext ac; ac.Map = &gMap; ac.IsTileOk = ignoreObjects ? IsTileWalkable : IsTileWalkableAroundObjects; Vec2i fromTile = Vec2iToTile(from); Vec2i toTile = MapSearchTileAround(ac.Map, Vec2iToTile(to), ac.IsTileOk); ASPath path = ASPathCreate(&cPathNodeSource, &ac, &fromTile, &toTile); size_t pathCount = ASPathGetCount(path); ASPathDestroy(path); return pathCount > 1; }
static bool TryPlacePickup(HealthPickups *h) { Vec2i size = Vec2iNew(HEALTH_W, HEALTH_H); // Attempt to place one in unexplored area for (int i = 0; i < 100; i++) { const Vec2i v = MapGenerateFreePosition(h->map, size); if (!Vec2iIsZero(v) && !MapGetTile(h->map, Vec2iToTile(v))->isVisited) { MapPlaceHealth(v); return true; } } // Attempt to place one in out-of-sight area for (int i = 0; i < 100; i++) { const Vec2i v = MapGenerateFreePosition(h->map, size); const Vec2i fullpos = Vec2iReal2Full(v); const TActor *closestPlayer = AIGetClosestPlayer(fullpos); if (!Vec2iIsZero(v) && (!closestPlayer || CHEBYSHEV_DISTANCE( fullpos.x, fullpos.y, closestPlayer->Pos.x, closestPlayer->Pos.y) >= 256 * 150)) { MapPlaceHealth(v); return true; } } // Attempt to place one anyway for (int i = 0; i < 100; i++) { const Vec2i v = MapGenerateFreePosition(h->map, size); if (!Vec2iIsZero(v)) { MapPlaceHealth(v); return true; } } return false; }
static bool IsPosNoSee(void *data, Vec2i pos) { return MapGetTile(data, Vec2iToTile(pos))->flags & MAPTILE_NO_SEE; }
static bool IsPosNoWalkAroundObjects(void *data, Vec2i pos) { return !IsTileWalkableAroundObjects(data, Vec2iToTile(pos)); }
static bool IsPosNoWalk(void *data, Vec2i pos) { return !IsTileWalkable(data, Vec2iToTile(pos)); }
static bool IsPosNoSee(void *data, Vec2i pos) { const Tile *t = MapGetTile(data, Vec2iToTile(pos)); return t != NULL && (t->flags & MAPTILE_NO_SEE); }
bool UpdateBullet(TMobileObject *obj, const int ticks) { obj->count += ticks; obj->soundLock = MAX(0, obj->soundLock - ticks); obj->specialLock = MAX(0, obj->specialLock - ticks); if (obj->count < obj->bulletClass->Delay) { return true; } if (obj->range >= 0 && obj->count > obj->range) { if (!gCampaign.IsClient) { FireGuns(obj, &obj->bulletClass->OutOfRangeGuns); } return false; } const Vec2i objPos = Vec2iNew(obj->x, obj->y); if (obj->bulletClass->SeekFactor > 0) { // Find the closest target to this bullet and steer towards it const TActor *owner = ActorGetByUID(obj->ActorUID); if (owner == NULL) { return false; } const TActor *target = AIGetClosestEnemy(objPos, owner, obj->flags); if (target && !target->dead) { for (int i = 0; i < ticks; i++) { obj->vel = SeekTowards( objPos, obj->vel, obj->bulletClass->SpeedLow, target->Pos, obj->bulletClass->SeekFactor); } } } Vec2i pos = Vec2iScale(Vec2iAdd(objPos, obj->vel), ticks); HitType hitItem = HIT_NONE; if (!gCampaign.IsClient) { hitItem = HitItem(obj, pos, obj->bulletClass->Persists); } const Vec2i realPos = Vec2iFull2Real(pos); // Falling (grenades) if (obj->bulletClass->Falling.GravityFactor != 0) { bool hasDropped = obj->z <= 0; for (int i = 0; i < ticks; i++) { obj->z += obj->dz; if (obj->z <= 0) { obj->z = 0; if (obj->bulletClass->Falling.Bounces) { obj->dz = -obj->dz / 2; } else { obj->dz = 0; } if (!hasDropped) { if (!gCampaign.IsClient) { FireGuns(obj, &obj->bulletClass->Falling.DropGuns); } } hasDropped = true; if (obj->bulletClass->Falling.DestroyOnDrop) { return false; } SoundPlayAt( &gSoundDevice, StrSound(obj->bulletClass->HitSound.Wall), realPos); } else { obj->dz -= obj->bulletClass->Falling.GravityFactor; } if (!obj->bulletClass->Falling.FallsDown) { obj->dz = MAX(0, obj->dz); } } } // Friction const bool isDiagonal = obj->vel.x != 0 && obj->vel.y != 0; int frictionComponent = isDiagonal ? (int)round(obj->bulletClass->Friction / sqrt(2)) : obj->bulletClass->Friction; for (int i = 0; i < ticks; i++) { if (obj->vel.x > 0) { obj->vel.x -= frictionComponent; } else if (obj->vel.x < 0) { obj->vel.x += frictionComponent; } if (obj->vel.y > 0) { obj->vel.y -= frictionComponent; } else if (obj->vel.y < 0) { obj->vel.y += frictionComponent; } } bool hitWall = false; if (!gCampaign.IsClient && hitItem == HIT_NONE) { hitWall = MapIsRealPosIn(&gMap, realPos) && ShootWall(realPos.x, realPos.y); } if (hitWall || hitItem != HIT_NONE) { GameEvent b = GameEventNew(GAME_EVENT_BULLET_BOUNCE); b.u.BulletBounce.UID = obj->UID; if (hitWall && !Vec2iIsZero(obj->vel)) { b.u.BulletBounce.HitType = (int)HIT_WALL; } else { b.u.BulletBounce.HitType = (int)hitItem; } bool alive = true; if ((hitWall && !obj->bulletClass->WallBounces) || ((hitItem != HIT_NONE) && obj->bulletClass->HitsObjects)) { b.u.BulletBounce.Spark = true; CASSERT(!gCampaign.IsClient, "Cannot process bounces as client"); FireGuns(obj, &obj->bulletClass->HitGuns); if (hitWall || !obj->bulletClass->Persists) { alive = false; } } b.u.BulletBounce.BouncePos = Vec2i2Net(pos); b.u.BulletBounce.BounceVel = Vec2i2Net(obj->vel); if (hitWall && !Vec2iIsZero(obj->vel)) { // Bouncing Vec2i bounceVel = obj->vel; pos = GetWallBounceFullPos(objPos, pos, &bounceVel); b.u.BulletBounce.BouncePos = Vec2i2Net(pos); b.u.BulletBounce.BounceVel = Vec2i2Net(bounceVel); obj->vel = bounceVel; } GameEventsEnqueue(&gGameEvents, b); if (!alive) { return false; } } if (!MapTryMoveTileItem(&gMap, &obj->tileItem, realPos)) { obj->count = obj->range; return false; } obj->x = pos.x; obj->y = pos.y; if (obj->bulletClass->Erratic) { for (int i = 0; i < ticks; i++) { obj->vel.x += ((rand() % 3) - 1) * 128; obj->vel.y += ((rand() % 3) - 1) * 128; } } // Proximity function, destroy // Only check proximity every now and then if (obj->bulletClass->ProximityGuns.size > 0 && !(obj->count & 3)) { if (!gCampaign.IsClient) { // Detonate the mine if there are characters in the tiles around it const Vec2i tv = Vec2iToTile(Vec2iFull2Real(pos)); Vec2i dv; for (dv.y = -1; dv.y <= 1; dv.y++) { for (dv.x = -1; dv.x <= 1; dv.x++) { const Vec2i dtv = Vec2iAdd(tv, dv); if (!MapIsTileIn(&gMap, dtv)) { continue; } if (TileHasCharacter(MapGetTile(&gMap, dtv))) { FireGuns(obj, &obj->bulletClass->ProximityGuns); return false; } } } } } return true; }