static void cullTerrain() { for (int x = 0; x < xSectors; x++) { for (int y = 0; y < ySectors; y++) { float xPos = world_coord(x * sectorSize + sectorSize / 2); float yPos = world_coord(y * sectorSize + sectorSize / 2); float distance = pow(player.p.x - xPos, 2) + pow(player.p.z - yPos, 2); if (distance > pow((double)world_coord(terrainDistance), 2)) { sectors[x * ySectors + y].draw = false; } else { sectors[x * ySectors + y].draw = true; if (sectors[x * ySectors + y].dirty) { updateSectorGeometry(x, y); sectors[x * ySectors + y].dirty = false; } } } } }
static QScriptValue js_playSound(QScriptContext *context, QScriptEngine *engine) { int player = engine->globalObject().property("me").toInt32(); if (player != selectedPlayer) { return QScriptValue(); } QString sound = context->argument(0).toString(); int soundID = audio_GetTrackID(sound.toUtf8().constData()); if (context->argumentCount() > 1) { int x = world_coord(context->argument(1).toInt32()); int y = world_coord(context->argument(2).toInt32()); int z = world_coord(context->argument(3).toInt32()); audio_QueueTrackPos(soundID, x, y, z); } else { audio_QueueTrack(soundID); /* -- FIXME properly (from original script func) if(bInTutorial) { audio_QueueTrack(ID_SOUND_OF_SILENCE); } */ } return QScriptValue(); }
// Returns the closest non-blocking tile to pos, or returns pos if no non-blocking tiles are present within a 2 tile distance. static Position findNonblockingPosition(Position pos, PROPULSION_TYPE propulsion, int player = 0, FPATH_MOVETYPE moveType = FMT_BLOCK) { Vector2i centreTile = map_coord(pos.xy); if (!fpathBaseBlockingTile(centreTile.x, centreTile.y, propulsion, player, moveType)) { return pos; // Fast case, pos is not on a blocking tile. } Vector2i bestTile = centreTile; int bestDistSq = INT32_MAX; for (int y = -2; y <= 2; ++y) for (int x = -2; x <= 2; ++x) { Vector2i tile = centreTile + Vector2i(x, y); Vector2i diff = world_coord(tile) + Vector2i(TILE_UNITS / 2, TILE_UNITS / 2) - pos.xy; int distSq = diff * diff; if (distSq < bestDistSq && !fpathBaseBlockingTile(tile.x, tile.y, propulsion, player, moveType)) { bestTile = tile; bestDistSq = distSq; } } // Return point on tile closest to the original pos. Vector2i minCoord = world_coord(bestTile); Vector2i maxCoord = minCoord + Vector2i(TILE_UNITS - 1, TILE_UNITS - 1); return Position(std::min(std::max(pos.x, minCoord.x), maxCoord.x), std::min(std::max(pos.y, minCoord.y), maxCoord.y), pos.z); }
/// Get the position of a grid point static void getGridPos(Vector3i *result, int x, int y, bool center, bool water) { if (center) { Vector3i a,b,c,d; getGridPos(&a, x , y , false, water); getGridPos(&b, x+1, y , false, water); getGridPos(&c, x , y+1, false, water); getGridPos(&d, x+1, y+1, false, water); averagePos(result, &a, &b, &c, &d); return; } result->x = world_coord(x); result->z = world_coord(-y); if (x <= 0 || y <= 0 || x >= mapWidth || y >= mapHeight) { result->y = 0; } else { result->y = map_TileHeight(x, y); if (water) { result->y = map_WaterHeight(x, y); } } }
/** Calculate the distance to a tile from a point * * @ingroup pathfinding */ static inline int fpathDistToTile(int tileX, int tileY, int pointX, int pointY) { // get the difference in world coords int xdiff = world_coord(tileX) - pointX; int ydiff = world_coord(tileY) - pointY; return iHypot(xdiff, ydiff); }
// /////////////////////////////////////////////////////////////// // receive splattered artifacts void recvMultiPlayerRandomArtifacts(NETQUEUE queue) { int count, i; uint8_t quantity, player; uint32_t tx,ty; uint32_t ref; FEATURE_TYPE type = FEAT_TREE; // Dummy initialisation. FEATURE *pF; NETbeginDecode(queue, GAME_ARTIFACTS); NETuint8_t(&quantity); NETenum(&type); debug(LOG_FEATURE, "receiving %u artifact(s) type: (%s)", quantity, feature_names[type]); for (i = 0; i < numFeatureStats && asFeatureStats[i].subType != type; i++) {} for (count = 0; count < quantity; count++) { MAPTILE *psTile; NETuint32_t(&tx); NETuint32_t(&ty); NETuint32_t(&ref); NETuint8_t(&player); if (tx == INVALID_XY) { continue; } else if (!tileOnMap(tx, ty)) { debug(LOG_ERROR, "Bad tile coordinates (%u,%u)", tx, ty); continue; } psTile = mapTile(tx, ty); if (!psTile || psTile->psObject != NULL) { debug(LOG_ERROR, "Already something at (%u,%u)!", tx, ty); continue; } pF = buildFeature((asFeatureStats + i), world_coord(tx), world_coord(ty), false); if (pF) { pF->id = ref; pF->player = player; syncDebugFeature(pF, '+'); } else { debug(LOG_ERROR, "Couldn't build feature %u for player %u ?", ref, player); } } NETend(); }
/* Move the particles */ void atmosUpdateSystem() { UDWORD i; UDWORD numberToAdd; Vector3f pos; // we don't want to do any of this while paused. if (!gamePaused() && weather != WT_NONE) { for (i = 0; i < MAX_ATMOS_PARTICLES; i++) { /* See if it's active */ if (asAtmosParts[i].status == APS_ACTIVE) { processParticle(&asAtmosParts[i]); } } /* This bit below needs to go into a "precipitation function" */ numberToAdd = ((weather == WT_SNOWING) ? 2 : 4); /* Temporary stuff - just adds a few particles! */ for (i = 0; i < numberToAdd; i++) { pos.x = player.p.x; pos.z = player.p.z; pos.x += world_coord(rand() % visibleTiles.x - visibleTiles.x / 2); pos.z += world_coord(rand() % visibleTiles.x - visibleTiles.y / 2); pos.y = 1000; /* If we've got one on the grid */ if (pos.x > 0 && pos.z > 0 && pos.x < (SDWORD)world_coord(mapWidth - 1) && pos.z < (SDWORD)world_coord(mapHeight - 1)) { /* On grid, so which particle shall we add? */ switch (weather) { case WT_SNOWING: atmosAddParticle(pos, AP_SNOW); break; case WT_RAINING: atmosAddParticle(pos, AP_RAIN); break; case WT_NONE: break; } } } } }
/** * Update the lightmap and draw the terrain and decals. * This function first draws the terrain in black, and then uses additive blending to put the terrain layers * on it one by one. Finally the decals are drawn. */ void drawTerrain(const glm::mat4 &mvp) { const glm::vec4 paramsXLight(1.0f / world_coord(mapWidth) *((float)mapWidth / lightmapWidth), 0, 0, 0); const glm::vec4 paramsYLight(0, 0, -1.0f / world_coord(mapHeight) *((float)mapHeight / lightmapHeight), 0); /////////////////////////////////// // set up the lightmap texture glActiveTexture(GL_TEXTURE1); // bind the texture lightmap_tex_num->bind(); // we limit the framerate of the lightmap, because updating a texture is an expensive operation if (realTime - lightmapLastUpdate >= LIGHTMAP_REFRESH) { lightmapLastUpdate = realTime; updateLightMap(); glPixelStorei(GL_UNPACK_ALIGNMENT, 1); lightmap_tex_num->upload(0, 0, 0, lightmapWidth, lightmapHeight, gfx_api::pixel_format::rgb, lightmapPixmap); } /////////////////////////////////// // terrain culling cullTerrain(); glActiveTexture(GL_TEXTURE0); // shift the lightmap half a tile as lights are supposed to be placed at the center of a tile const glm::mat4 lightMatrix = glm::translate(glm::vec3(1.f / lightmapWidth / 2, 1.f / lightmapHeight / 2, 0.f)); ////////////////////////////////////// // canvas to draw on drawDepthOnly(mvp, paramsXLight, paramsYLight); /////////////////////////////////// // terrain drawTerrainLayers(mvp, paramsXLight, paramsYLight, lightMatrix); ////////////////////////////////// // decals drawDecals(mvp, paramsXLight, paramsYLight, lightMatrix); //////////////////////////////// // disable the lightmap texture glActiveTexture(GL_TEXTURE1); glActiveTexture(GL_TEXTURE0); // leave everything in a sane state so it won't mess up somewhere else //glBindBuffer(GL_ARRAY_BUFFER, 0); // HACK Must unbind GL_ARRAY_BUFFER (don't know if it has to be unbound everywhere), otherwise text rendering may mysteriously crash. }
// //////////////////////////////////////////////////////////////////////////// // add an artifact on destruction if required. void technologyGiveAway(const STRUCTURE *pS) { int i; uint8_t count = 1; uint32_t x, y; FEATURE *pF = NULL; FEATURE_TYPE type = FEAT_GEN_ARTE; // If a fully built factory (or with modules under construction) which is our responsibility got destroyed if (pS->pStructureType->type == REF_FACTORY && (pS->status == SS_BUILT || pS->currentBuildPts >= pS->body) && myResponsibility(pS->player)) { x = map_coord(pS->pos.x); y = map_coord(pS->pos.y); // Pick a tile to place the artifact if (!pickATileGen(&x, &y, LOOK_FOR_EMPTY_TILE, zonedPAT)) { ASSERT(false, "technologyGiveAway: Unable to find a free location"); } // Get the feature offset for(i = 0; i < numFeatureStats && asFeatureStats[i].subType != FEAT_GEN_ARTE; i++); // 'Build' the artifact pF = buildFeature((asFeatureStats + i), world_coord(x), world_coord(y), false); if (pF) { pF->player = pS->player; } NETbeginEncode(NET_ARTIFACTS, NET_ALL_PLAYERS); { /* Make sure that we don't have to violate the constness of pS. * Since the nettype functions aren't const correct when sending */ uint8_t player = pS->player; NETuint8_t(&count); NETenum(&type); NETuint32_t(&x); NETuint32_t(&y); NETuint32_t(&pF->id); NETuint8_t(&player); } NETend(); } return; }
/* Makes a particle wrap around - if it goes off the grid, then it returns on the other side - provided it's still on world... Which it should be */ static void testParticleWrap(ATPART *psPart) { /* Gone off left side */ if (psPart->position.x < player.p.x - world_coord(visibleTiles.x) / 2) { psPart->position.x += world_coord(visibleTiles.x); } /* Gone off right side */ else if (psPart->position.x > (player.p.x + world_coord(visibleTiles.x) / 2)) { psPart->position.x -= world_coord(visibleTiles.x); } /* Gone off top */ if (psPart->position.z < player.p.z - world_coord(visibleTiles.y) / 2) { psPart->position.z += world_coord(visibleTiles.y); } /* Gone off bottom */ else if (psPart->position.z > (player.p.z + world_coord(visibleTiles.y) / 2)) { psPart->position.z -= world_coord(visibleTiles.y); } }
Vector3f audio_GetPlayerPos(void) { Vector3f pos; // Player's Y and Z interchanged // @NOTE Why? pos.x = player.p.x + world_coord(visibleTiles.x / 2); pos.y = player.p.z + world_coord(visibleTiles.x / 2); pos.z = player.p.y; // Invert Y to match QSOUND axes // @NOTE What is QSOUND? Why invert the Y axis? pos.y = world_coord(GetHeightOfMap()) - pos.y; return pos; }
static QScriptValue js_orderDroidLoc(QScriptContext *context, QScriptEngine *) { QScriptValue droidVal = context->argument(0); int id = droidVal.property("id").toInt32(); int player = droidVal.property("player").toInt32(); QScriptValue orderVal = context->argument(1); int x = context->argument(2).toInt32(); int y = context->argument(3).toInt32(); DROID_ORDER order = (DROID_ORDER)orderVal.toInt32(); DROID *psDroid = IdToDroid(id, player); SCRIPT_ASSERT(context, psDroid, "Droid id %d not found belonging to player %d", id, player); SCRIPT_ASSERT(context, tileOnMap(x, y), "Outside map bounds (%d, %d)", x, y); orderDroidLoc(psDroid, order, world_coord(x), world_coord(y), ModeQueue); return QScriptValue(); }
/** * Get QSound axial position from world (x,y) @FIXME we don't need to do this, since we are not using qsound. */ void audio_GetStaticPos(SDWORD iWorldX, SDWORD iWorldY, SDWORD *piX, SDWORD *piY, SDWORD *piZ) { *piX = iWorldX; *piZ = map_TileHeight(map_coord(iWorldX), map_coord(iWorldY)); /* invert y to match QSOUND axes */ *piY = world_coord(GetHeightOfMap()) - iWorldY; }
bool addOilDrum(uint8_t count) { syncDebug("Adding %d oil drums.", count); int featureIndex; for (featureIndex = 0; featureIndex < numFeatureStats && asFeatureStats[featureIndex].subType != FEAT_OIL_DRUM; ++featureIndex) {} if (featureIndex >= numFeatureStats) { debug(LOG_WARNING, "No oil drum feature!"); return false; // Return value ignored. } for (unsigned n = 0; n < count; ++n) { uint32_t x, y; for (int i = 0; i < 3; ++i) // try three times { // Between 10 and mapwidth - 10 x = gameRand(mapWidth - 20) + 10; y = gameRand(mapHeight - 20) + 10; if (pickATileGen(&x, &y, LOOK_FOR_EMPTY_TILE, zonedPAT)) { break; } x = INVALID_XY; } if (x == INVALID_XY) { syncDebug("Did not find location for oil drum."); debug(LOG_FEATURE, "Unable to find a free location."); continue; } FEATURE *pF = buildFeature(&asFeatureStats[featureIndex], world_coord(x), world_coord(y), false); if (pF) { pF->player = ANYPLAYER; syncDebugFeature(pF, '+'); } else { debug(LOG_ERROR, "Couldn't build oil drum?"); } } return true; }
/* Sets up the dummy target for the camera */ static void setUpRadarTarget(SDWORD x, SDWORD y) { radarTarget.pos.x = x; radarTarget.pos.y = y; if ((x < 0) || (y < 0) || (x > world_coord(mapWidth - 1)) || (y > world_coord(mapHeight - 1))) { radarTarget.pos.z = world_coord(1) * ELEVATION_SCALE + CAMERA_PIVOT_HEIGHT; } else { radarTarget.pos.z = map_Height(x,y) + CAMERA_PIVOT_HEIGHT; } radarTarget.rot.direction = calcDirection(player.p.x, player.p.z, x, y); radarTarget.rot.pitch = 0; radarTarget.rot.roll = 0; }
// //////////////////////////////////////////////////////////////////////////// // probably temporary. Places the camera on the players 1st droid or struct. Vector3i cameraToHome(UDWORD player,bool scroll) { Vector3i res; UDWORD x,y; STRUCTURE *psBuilding; for (psBuilding = apsStructLists[player]; psBuilding && (psBuilding->pStructureType->type != REF_HQ); psBuilding= psBuilding->psNext) {} if(psBuilding) { x= map_coord(psBuilding->pos.x); y= map_coord(psBuilding->pos.y); } else if (apsDroidLists[player]) // or first droid { x= map_coord(apsDroidLists[player]->pos.x); y= map_coord(apsDroidLists[player]->pos.y); } else if (apsStructLists[player]) // center on first struct { x= map_coord(apsStructLists[player]->pos.x); y= map_coord(apsStructLists[player]->pos.y); } else //or map center. { x= mapWidth/2; y= mapHeight/2; } if(scroll) { requestRadarTrack(world_coord(x), world_coord(y)); } else { setViewPos(x,y,true); } res.x = world_coord(x); res.y = map_TileHeight(x,y); res.z = world_coord(y); return res; }
void rayCast(Vector3i src, uint16_t direction, uint32_t length, RAY_CALLBACK callback, void *data) { uint32_t currLen; for (currLen = 0; currLen < length; currLen += TILE_UNITS) { Vector3i curr = src + Vector3i(iSinCosR(direction, currLen), 0); // stop at the edge of the map if (curr.x < 0 || curr.x >= world_coord(mapWidth) || curr.y < 0 || curr.y >= world_coord(mapHeight)) { return; } if (!callback(curr, currLen, data)) { // callback doesn't want any more points so return return; } } }
// @FIXME we don't need to do this, since we are not using qsound. void audio_GetObjectPos(SIMPLE_OBJECT *psBaseObj, SDWORD *piX, SDWORD *piY, SDWORD *piZ) { /* check is valid pointer */ ASSERT( psBaseObj != NULL, "audio_GetObjectPos: game object pointer invalid" ); *piX = psBaseObj->pos.x; *piZ = map_TileHeight(map_coord(psBaseObj->pos.x), map_coord(psBaseObj->pos.y)); /* invert y to match QSOUND axes */ *piY = world_coord(GetHeightOfMap()) - psBaseObj->pos.y; }
static QScriptValue js_groupAddArea(QScriptContext *context, QScriptEngine *engine) { int groupId = context->argument(0).toInt32(); int player = engine->globalObject().property("me").toInt32(); int x1 = world_coord(context->argument(1).toInt32()); int y1 = world_coord(context->argument(2).toInt32()); int x2 = world_coord(context->argument(3).toInt32()); int y2 = world_coord(context->argument(4).toInt32()); DROID_GROUP *psGroup = grpFind(groupId); SCRIPT_ASSERT(context, psGroup, "Invalid group index %d", groupId); for (DROID *psDroid = apsDroidLists[player]; psGroup && psDroid; psDroid = psDroid->psNext) { if (psDroid->pos.x >= x1 && psDroid->pos.x <= x2 && psDroid->pos.y >= y1 && psDroid->pos.y <= y2 && psDroid->droidType != DROID_COMMAND && psDroid->droidType != DROID_TRANSPORTER) { psGroup->add(psDroid); } } return QScriptValue(); }
// Give a Droid an order to a location bool scrOrderDroidLoc(void) { DROID *psDroid; DROID_ORDER order; SDWORD x, y; if (!stackPopParams(4, ST_DROID, &psDroid, VAL_INT, &order, VAL_INT, &x, VAL_INT, &y)) { return false; } ASSERT(psDroid != NULL, "scrOrderUnitLoc: Invalid unit pointer"); if (psDroid == NULL) { return false; } if (order != DORDER_MOVE && order != DORDER_SCOUT) { ASSERT(false, "scrOrderUnitLoc: Invalid order"); return false; } if (x < 0 || x > world_coord(mapWidth) || y < 0 || y > world_coord(mapHeight)) { ASSERT(false, "scrOrderUnitLoc: Invalid location"); return false; } orderDroidLoc(psDroid, order, x, y, ModeQueue); return true; }
// //////////////////////////////////////////////////////////////////////////// // add an artifact on destruction if required. void technologyGiveAway(const STRUCTURE *pS) { // If a fully built factory (or with modules under construction) which is our responsibility got destroyed if (pS->pStructureType->type == REF_FACTORY && (pS->status == SS_BUILT || pS->currentBuildPts >= pS->body)) { syncDebug("Adding artefact."); } else { syncDebug("Not adding artefact."); return; } int featureIndex; for (featureIndex = 0; featureIndex < numFeatureStats && asFeatureStats[featureIndex].subType != FEAT_GEN_ARTE; ++featureIndex) {} if (featureIndex >= numFeatureStats) { debug(LOG_WARNING, "No artefact feature!"); return; } uint32_t x = map_coord(pS->pos.x), y = map_coord(pS->pos.y); if (!pickATileGen(&x, &y, LOOK_FOR_EMPTY_TILE, zonedPAT)) { syncDebug("Did not find location for oil drum."); debug(LOG_FEATURE, "Unable to find a free location."); return; } FEATURE *pF = buildFeature(&asFeatureStats[featureIndex], world_coord(x), world_coord(y), false); if (pF) { pF->player = pS->player; syncDebugFeature(pF, '+'); } else { debug(LOG_ERROR, "Couldn't build artefact?"); } }
static void updateSpotters() { static GridList gridList; // static to avoid allocations. for (int i = 0; i < apsInvisibleViewers.size(); i++) { SPOTTER *psSpot = apsInvisibleViewers.at(i); if (psSpot->expiryTime != 0 && psSpot->expiryTime < gameTime) { delete psSpot; apsInvisibleViewers.erase(apsInvisibleViewers.begin() + i); continue; } // else, ie if not expired, show objects around it gridList = gridStartIterateUnseen(world_coord(psSpot->pos.x), world_coord(psSpot->pos.y), psSpot->sensorRadius, psSpot->player); for (GridIterator gi = gridList.begin(); gi != gridList.end(); ++gi) { BASE_OBJECT *psObj = *gi; // Tell system that this side can see this object setSeenBy(psObj, psSpot->player, UBYTE_MAX); } } }
Vector3f audio_GetPlayerPos() { Vector3f pos; pos.x = player.p.x; pos.y = player.p.z; pos.z = player.p.y; // Invert Y to match QSOUND axes // @NOTE What is QSOUND? Why invert the Y axis? pos.y = world_coord(mapHeight) - pos.y; return pos; }
// Give a group an order to a location bool scrOrderGroupLoc(void) { DROID_GROUP *psGroup; DROID_ORDER order; SDWORD x, y; if (!stackPopParams(4, ST_GROUP, &psGroup, VAL_INT, &order, VAL_INT, &x, VAL_INT, &y)) { return false; } ASSERT(psGroup != NULL, "scrOrderGroupLoc: Invalid group pointer"); if (order != DORDER_MOVE && order != DORDER_SCOUT) { ASSERT(false, "scrOrderGroupLoc: Invalid order"); return false; } if (x < 0 || x > world_coord(mapWidth) || y < 0 || y > world_coord(mapHeight)) { ASSERT(false, "Invalid map location (%d, %d), max is (%d, %d)", x, y, world_coord(mapWidth), world_coord(mapHeight)); return false; } debug(LOG_NEVER, "group %p (%u) order %d (%d,%d)", psGroup, psGroup->getNumMembers(), order, x, y); psGroup->orderGroup(order, (UDWORD)x, (UDWORD)y); return true; }
// Finds the next intersection of the line with a vertical grid line (or with a horizontal grid line, if called with x and y swapped). static bool tryStep(int32_t &tile, int32_t step, int32_t &cur, int32_t end, int32_t &px, int32_t &py, int32_t sx, int32_t sy, int32_t dx, int32_t dy) { tile += step; if (cur == end) { return false; // No more vertical grid lines to cross before reaching the endpoint. } // Find the point on the line with the x coordinate world_coord(cur). px = world_coord(cur); py = sy + int64_t(px - sx) * (dy - sy) / (dx - sx); cur += step; return true; }
void rayCast(Vector2i src, Vector2i dst, RAY_CALLBACK callback, void *data) { if (!callback(src, 0, data) || src == dst) // Start at src. { return; // Callback gave up after the first point, or there are no other points. } Vector2i srcM = map_coord(src); Vector2i dstM = map_coord(dst); Vector2i step, tile, cur, end; initSteps(srcM.x, dstM.x, tile.x, step.x, cur.x, end.x); initSteps(srcM.y, dstM.y, tile.y, step.y, cur.y, end.y); Vector2i prev(0, 0); // Dummy initialisation. bool first = true; Vector2i nextX(0, 0), nextY(0, 0); // Dummy initialisations. bool canX = tryStep(tile.x, step.x, cur.x, end.x, nextX.x, nextX.y, src.x, src.y, dst.x, dst.y); bool canY = tryStep(tile.y, step.y, cur.y, end.y, nextY.y, nextY.x, src.y, src.x, dst.y, dst.x); while (canX || canY) { int32_t xDist = abs(nextX.x - src.x) + abs(nextX.y - src.y); int32_t yDist = abs(nextY.x - src.x) + abs(nextY.y - src.y); Vector2i sel; Vector2i selTile; if (canX && (!canY || xDist < yDist)) // The line crosses a vertical grid line next. { sel = nextX; selTile = tile; canX = tryStep(tile.x, step.x, cur.x, end.x, nextX.x, nextX.y, src.x, src.y, dst.x, dst.y); } else // The line crosses a horizontal grid line next. { assert(canY); sel = nextY; selTile = tile; canY = tryStep(tile.y, step.y, cur.y, end.y, nextY.y, nextY.x, src.y, src.x, dst.y, dst.x); } if (!first) { // Find midpoint. Vector2i avg = (prev + sel) / 2; // But make sure it's on the right tile, since it could be off-by-one if the line passes exactly through a grid intersection. avg.x = std::min(std::max(avg.x, world_coord(selTile.x)), world_coord(selTile.x + 1) - 1); avg.y = std::min(std::max(avg.y, world_coord(selTile.y)), world_coord(selTile.y + 1) - 1); if (!worldOnMap(avg) || !callback(avg, iHypot(avg), data)) { return; // Callback doesn't want any more points, or we reached the edge of the map, so return. } } prev = sel; first = false; } // Include the endpoint. if (!worldOnMap(dst)) { return; // Stop, since reached the edge of the map. } callback(dst, iHypot(dst), data); }
/* Fire a weapon at something */ bool combFire(WEAPON *psWeap, BASE_OBJECT *psAttacker, BASE_OBJECT *psTarget, int weapon_slot) { WEAPON_STATS *psStats; UDWORD damLevel; UDWORD firePause; SDWORD longRange; DROID *psDroid = NULL; int compIndex; CHECK_OBJECT(psAttacker); CHECK_OBJECT(psTarget); ASSERT(psWeap != NULL, "Invalid weapon pointer"); /* Watermelon:dont shoot if the weapon_slot of a vtol is empty */ if (psAttacker->type == OBJ_DROID && isVtolDroid(((DROID *)psAttacker))) { if (((DROID *)psAttacker)->sMove.iAttackRuns[weapon_slot] >= 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; } /*see if reload-able weapon and out of ammo*/ if (psStats->reloadTime && !psWeap->ammo) { if (gameTime - psWeap->lastFired < weaponReloadTime(psStats, psAttacker->player)) { return false; } //reset the ammo level psWeap->ammo = psStats->numRounds; } /* See when the weapon last fired to control it's rate of fire */ firePause = weaponFirePause(psStats, psAttacker->player); // increase the pause if heavily damaged switch (psAttacker->type) { case OBJ_DROID: psDroid = (DROID *)psAttacker; damLevel = PERCENT(psDroid->body, psDroid->originalBody); break; case OBJ_STRUCTURE: damLevel = PERCENT(((STRUCTURE *)psAttacker)->body, structureBody((STRUCTURE *)psAttacker)); break; default: damLevel = 100; break; } if (damLevel < HEAVY_DAMAGE_LEVEL) { firePause += firePause; } if (gameTime - psWeap->lastFired <= firePause) { /* Too soon to fire again */ return false; } // add a random delay to the fire // With logical updates, a good graphics gard no longer gives a better ROF. // TODO Should still replace this with something saner, such as a ±1% random deviation in reload time. int fireChance = gameTime - (psWeap->lastFired + firePause); if (gameRand(RANDOM_PAUSE) > fireChance) { 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, psStats->pName, psTarget->id); return false; } /* Check we can see the target */ if (psAttacker->type == OBJ_DROID && !isVtolDroid((DROID *)psAttacker) && (proj_Direct(psStats) || actionInsideMinRange(psDroid, psTarget, psStats))) { if(!lineOfFire(psAttacker, psTarget, weapon_slot, true)) { // Can't see the target - can't hit it with direct fire objTrace(psAttacker->id, "combFire(%u[%s]->%u): Droid has no direct line of sight to target", psAttacker->id, ((DROID *)psAttacker)->aName, psTarget->id); return false; } } else if ((psAttacker->type == OBJ_STRUCTURE) && (((STRUCTURE *)psAttacker)->pStructureType->height == 1) && proj_Direct(psStats)) { // a bunker can't shoot through walls if (!lineOfFire(psAttacker, psTarget, weapon_slot, true)) { // Can't see the target - can't hit it with direct fire objTrace(psAttacker->id, "combFire(%u[%s]->%u): Structure has no direct line of sight to target", psAttacker->id, ((STRUCTURE *)psAttacker)->pStructureType->pName, psTarget->id); return false; } } else if ( proj_Direct(psStats) ) { // VTOL or tall building if (!lineOfFire(psAttacker, psTarget, weapon_slot, false)) { // Can't see the target - can't hit it with direct fire objTrace(psAttacker->id, "combFire(%u[%s]->%u): Tall object has no direct line of sight to target", psAttacker->id, psStats->pName, 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); /* modification by CorvusCorax - calculate shooting angle */ int min_angle = 0; // only calculate 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 <= psStats->shortRange) && (dist >= psStats->minRange)) { // get weapon chance to hit in the short range baseHitChance = weaponShortHit(psStats,psAttacker->player); } else if ((dist <= longRange && dist >= psStats->minRange) || (psAttacker->type == OBJ_DROID && !proj_Direct(psStats) && actionInsideMinRange(psDroid, psTarget, psStats))) { // 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, psStats->pName, 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; } // fire while moving modifiers if (psAttacker->type == OBJ_DROID && ((DROID *)psAttacker)->sMove.Status != MOVEINACTIVE) { switch (psStats->fireOnMove) { case FOM_NO: // Can't fire while moving return false; break; case FOM_PARTIAL: resultHitChance = FOM_PARTIAL_ACCURACY_PENALTY * resultHitChance / 100; break; case FOM_YES: // can fire while moving break; } } /* -------!!! From that point we are sure that we are firing !!!------- */ /* note when the weapon fired */ psWeap->lastFired = gameTime; /* reduce ammo if salvo */ if (psStats->reloadTime) { psWeap->ammo--; } // increment the shots counter psWeap->shotsFired++; // predicted X,Y offset per sec Vector3i predict = psTarget->pos; //Watermelon:Target prediction if (isDroid(psTarget)) { DROID *psDroid = castDroid(psTarget); int32_t flightTime; if (proj_Direct(psStats) || dist <= psStats->minRange) { flightTime = dist / 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); } /* 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=%hhu : ", psStats->pName, psTarget->id, resultHitChance, 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); return true; }
/* 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"); /* Watermelon:dont shoot if the weapon_slot of a vtol is empty */ if (psAttacker->type == OBJ_DROID && isVtolDroid(((DROID *)psAttacker))) { if (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->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->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, psStats->pName, 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); 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->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, psStats->pName, 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; } // fire while moving modifiers if (psAttacker->type == OBJ_DROID && ((DROID *)psAttacker)->sMove.Status != MOVEINACTIVE) { switch (psStats->fireOnMove) { case FOM_NO: // Can't fire while moving return false; break; case FOM_PARTIAL: resultHitChance = FOM_PARTIAL_ACCURACY_PENALTY * resultHitChance / 100; break; case FOM_YES: // can fire while moving break; } } /* -------!!! From that point we are sure that we are firing !!!------- */ // Add a random delay to the next shot. // TODO Add deltaFireTime to the time it takes to fire next. If just adding to psWeap->lastFired, it might put it in the future, causing assertions. And if not sometimes putting it in the future, the fire rate would be lower than advertised. //int fireJitter = firePause/100; // ±1% variation in fire rate. //int deltaFireTime = gameRand(fireJitter*2 + 1) - fireJitter; /* note when the weapon fired */ psWeap->lastFired = fireTime; /* reduce ammo if salvo */ if (psStats->reloadTime) { psWeap->ammo--; } // increment the shots counter psWeap->shotsFired++; // predicted X,Y offset per sec Vector3i predict = psTarget->pos; //Watermelon:Target prediction if (isDroid(psTarget) && castDroid(psTarget)->sMove.bumpTime == 0) { DROID *psDroid = castDroid(psTarget); int32_t flightTime; if (proj_Direct(psStats) || dist <= psStats->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 playerEmpTime = getEmpDisableTime(psTarget->player); int empTime = playerEmpTime - (gameTime - psTarget->timeLastHit); CLIP(empTime, 0, playerEmpTime); if (empTime >= playerEmpTime * 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", psStats->pName, 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; }
/** Draw the droids and structure positions on the radar. */ static void DrawRadarObjects() { UBYTE clan; PIELIGHT playerCol; PIELIGHT flashCol; int x, y; /* Show droids on map - go through all players */ for (clan = 0; clan < MAX_PLAYERS; clan++) { DROID *psDroid; //see if have to draw enemy/ally color if (bEnemyAllyRadarColor) { if (clan == selectedPlayer) { playerCol = colRadarMe; } else { playerCol = (aiCheckAlliances(selectedPlayer, clan) ? colRadarAlly : colRadarEnemy); } } else { //original 8-color mode STATIC_ASSERT(MAX_PLAYERS <= ARRAY_SIZE(clanColours)); playerCol = clanColours[getPlayerColour(clan)]; } STATIC_ASSERT(MAX_PLAYERS <= ARRAY_SIZE(flashColours)); flashCol = flashColours[getPlayerColour(clan)]; /* Go through all droids */ for (psDroid = apsDroidLists[clan]; psDroid != NULL; psDroid = psDroid->psNext) { if (psDroid->pos.x < world_coord(scrollMinX) || psDroid->pos.y < world_coord(scrollMinY) || psDroid->pos.x >= world_coord(scrollMaxX) || psDroid->pos.y >= world_coord(scrollMaxY)) { continue; } if (psDroid->visible[selectedPlayer] || (bMultiPlayer && alliancesSharedVision(game.alliance) && aiCheckAlliances(selectedPlayer, psDroid->player))) { int x = psDroid->pos.x / TILE_UNITS; int y = psDroid->pos.y / TILE_UNITS; size_t pos = (x - scrollMinX) + (y - scrollMinY) * radarTexWidth; ASSERT(pos * sizeof(*radarBuffer) < radarBufferSize, "Buffer overrun"); if (clan == selectedPlayer && gameTime - psDroid->timeLastHit < HIT_NOTIFICATION) { radarBuffer[pos] = flashCol.rgba; } else { radarBuffer[pos] = playerCol.rgba; } } } } /* Do the same for structures */ for (x = scrollMinX; x < scrollMaxX; x++) { for (y = scrollMinY; y < scrollMaxY; y++) { MAPTILE *psTile = mapTile(x, y); STRUCTURE *psStruct; size_t pos = (x - scrollMinX) + (y - scrollMinY) * radarTexWidth; ASSERT(pos * sizeof(*radarBuffer) < radarBufferSize, "Buffer overrun"); if (!TileHasStructure(psTile)) { continue; } psStruct = (STRUCTURE *)psTile->psObject; clan = psStruct->player; //see if have to draw enemy/ally color if (bEnemyAllyRadarColor) { if (clan == selectedPlayer) { playerCol = colRadarMe; } else { playerCol = (aiCheckAlliances(selectedPlayer, clan) ? colRadarAlly : colRadarEnemy); } } else { //original 8-color mode playerCol = clanColours[getPlayerColour(clan)]; } flashCol = flashColours[getPlayerColour(clan)]; if (psStruct->visible[selectedPlayer] || (bMultiPlayer && alliancesSharedVision(game.alliance) && aiCheckAlliances(selectedPlayer, psStruct->player))) { if (clan == selectedPlayer && gameTime - psStruct->timeLastHit < HIT_NOTIFICATION) { radarBuffer[pos] = flashCol.rgba; } else { radarBuffer[pos] = playerCol.rgba; } } } } }
// not a direct script function but a helper for scrSkDefenseLocation and scrSkDefenseLocationB static bool defenseLocation(bool variantB) { SDWORD *pX, *pY, statIndex, statIndex2; UDWORD x, y, gX, gY, dist, player, nearestSoFar, count; GATEWAY *psGate, *psChosenGate; DROID *psDroid; UDWORD x1, x2, x3, x4, y1, y2, y3, y4; bool noWater; UDWORD minCount; UDWORD offset; if (!stackPopParams(6, VAL_REF | VAL_INT, &pX, VAL_REF | VAL_INT, &pY, ST_STRUCTURESTAT, &statIndex, ST_STRUCTURESTAT, &statIndex2, ST_DROID, &psDroid, VAL_INT, &player)) { debug(LOG_ERROR, "defenseLocation: failed to pop"); return false; } if (player >= MAX_PLAYERS) { ASSERT(false, "defenseLocation:player number is too high"); return false; } ASSERT_OR_RETURN(false, statIndex < numStructureStats, "Invalid range referenced for numStructureStats, %d > %d", statIndex, numStructureStats); ASSERT_OR_RETURN(false, statIndex2 < numStructureStats, "Invalid range referenced for numStructureStats, %d > %d", statIndex2, numStructureStats); STRUCTURE_STATS *psWStats = (asStructureStats + statIndex2); // check for wacky coords. if (*pX < 0 || *pX > world_coord(mapWidth) || *pY < 0 || *pY > world_coord(mapHeight) ) { goto failed; } x = map_coord(*pX); // change to tile coords. y = map_coord(*pY); // go down the gateways, find the nearest gateway with >1 empty tiles nearestSoFar = UDWORD_MAX; psChosenGate = NULL; for (psGate = gwGetGateways(); psGate; psGate = psGate->psNext) { if (auxTile(psGate->x1, psGate->y1, player) & AUXBITS_THREAT) { continue; // enemy can shoot there, not safe to build } count = 0; noWater = true; // does it have >1 tile unoccupied. if (psGate->x1 == psGate->x2) { // vert //skip gates that are too short if (variantB && (psGate->y2 - psGate->y1) <= 2) { continue; } gX = psGate->x1; for (gY = psGate->y1; gY <= psGate->y2; gY++) { if (! TileIsOccupied(mapTile(gX, gY))) { count++; } if (terrainType(mapTile(gX, gY)) == TER_WATER) { noWater = false; } } } else { // horiz //skip gates that are too short if (variantB && (psGate->x2 - psGate->x1) <= 2) { continue; } gY = psGate->y1; for (gX = psGate->x1; gX <= psGate->x2; gX++) { if (! TileIsOccupied(mapTile(gX, gY))) { count++; } if (terrainType(mapTile(gX, gY)) == TER_WATER) { noWater = false; } } } if (variantB) { minCount = 2; } else { minCount = 1; } if (count > minCount && noWater) //<NEW> min 2 tiles { // ok it's free. Is it the nearest one yet? /* Get gateway midpoint */ gX = (psGate->x1 + psGate->x2) / 2; gY = (psGate->y1 + psGate->y2) / 2; /* Estimate the distance to it */ dist = iHypot(x - gX, y - gY); /* Is it best we've found? */ if (dist < nearestSoFar && dist < 30) { /* Yes, then keep a record of it */ nearestSoFar = dist; psChosenGate = psGate; } } } if (!psChosenGate) // we have a gateway. { goto failed; } // find an unnocupied tile on that gateway. if (psChosenGate->x1 == psChosenGate->x2) { // vert gX = psChosenGate->x1; for (gY = psChosenGate->y1; gY <= psChosenGate->y2; gY++) { if (! TileIsOccupied(mapTile(gX, gY))) { y = gY; x = gX; break; } } } else { // horiz gY = psChosenGate->y1; for (gX = psChosenGate->x1; gX <= psChosenGate->x2; gX++) { if (! TileIsOccupied(mapTile(gX, gY))) { y = gY; x = gX; break; } } } // back to world coords and store result. *pX = world_coord(x) + (TILE_UNITS / 2); // return centre of tile. *pY = world_coord(y) + (TILE_UNITS / 2); scrFunctionResult.v.bval = true; if (!stackPushResult(VAL_BOOL, &scrFunctionResult)) // success { return false; } // order the droid to build two walls, one either side of the gateway. // or one in the case of a 2 size gateway. //find center of the gateway x = (psChosenGate->x1 + psChosenGate->x2) / 2; y = (psChosenGate->y1 + psChosenGate->y2) / 2; //find start pos of the gateway x1 = world_coord(psChosenGate->x1) + (TILE_UNITS / 2); y1 = world_coord(psChosenGate->y1) + (TILE_UNITS / 2); if (variantB) { offset = 2; } else { offset = 1; } if (psChosenGate->x1 == psChosenGate->x2) //vert { x2 = x1; //vert: end x pos of the first section = start x pos y2 = world_coord(y - 1) + TILE_UNITS / 2; //start y loc of the first sec x3 = x1; y3 = world_coord(y + offset) + TILE_UNITS / 2; } else //hor { x2 = world_coord(x - 1) + TILE_UNITS / 2; y2 = y1; x3 = world_coord(x + offset) + TILE_UNITS / 2; y3 = y1; } //end coords of the second section x4 = world_coord(psChosenGate->x2) + TILE_UNITS / 2; y4 = world_coord(psChosenGate->y2) + TILE_UNITS / 2; // first section. if (x1 == x2 && y1 == y2) //first sec is 1 tile only: ((2 tile gate) or (3 tile gate and first sec)) { orderDroidStatsLocDir(psDroid, DORDER_BUILD, psWStats, x1, y1, 0, ModeQueue); } else { orderDroidStatsTwoLocDir(psDroid, DORDER_LINEBUILD, psWStats, x1, y1, x2, y2, 0, ModeQueue); } // second section if (x3 == x4 && y3 == y4) { orderDroidStatsLocDirAdd(psDroid, DORDER_BUILD, psWStats, x3, y3, 0); } else { orderDroidStatsTwoLocDirAdd(psDroid, DORDER_LINEBUILD, psWStats, x3, y3, x4, y4, 0); } return true; failed: scrFunctionResult.v.bval = false; if (!stackPushResult(VAL_BOOL, &scrFunctionResult)) // failed! { return false; } return true; }