/* * $Id: map.los.c,v 1.1.1.1 2005/01/11 21:18:08 kstevens Exp $ * * Author: Thomas Wouters <thomas@xs4all.net> * * Copyright (c) 2002 Thomas Wouters * All rights reserved * */ #include "mech.h" #include "btmacros.h" #include "mech.sensor.h" #include "map.los.h" #include "p.mech.utils.h" #define INDEX2X(i) ((i%(losmap.xsize))+(losmap.startx)) #define INDEX2Y(i) ((i/(losmap.xsize))+(losmap.starty)) extern int TraceLOS(MAP * map, int ax, int ay, int bx, int by, lostrace_info ** result); static hexlosmap_info losmap; int LOSMap_Hex2Index(hexlosmap_info * losmap, int x, int y) { if (x < losmap->startx || x > losmap->startx + losmap->xsize || y < losmap->starty || y > losmap->starty + losmap->ysize) { SendError(tprintf("LOSMap request from out of bounds hex: %d,%d", x, y)); return 0; } return ((y - losmap->starty) * losmap->xsize) + (x - losmap->startx); } static float MechHeight(MECH * mech) { switch (MechType(mech)) { case CLASS_MECH: return 0.2 + !Fallen(mech); case CLASS_SPHEROID_DS: return 4.2; case CLASS_DS: return 2.2; case CLASS_MW: case CLASS_VEH_NAVAL: return 0.01; } return 0.2; } static void set_hexlosinfo(int x, int y, int flag) { if (x < (losmap.startx) || x >= (losmap.startx) + (losmap.xsize) || y < (losmap.starty) || y >= (losmap.starty) + (losmap.ysize)) { return; } losmap.map[LOSMap_Hex2Index(&losmap, x, y)] |= (flag | MAPLOSHEX_SEEN); } static int hexlit(int x, int y) { if (x < (losmap.startx) || x >= (losmap.startx) + (losmap.xsize) || y < (losmap.starty) || y >= (losmap.starty) + (losmap.ysize)) { return 0; } return (losmap.map[LOSMap_Hex2Index(&losmap, x, y)] & MAPLOSHEX_LIT); } static void set_sliteinfo(int x, int y, int flag) { if (x < (losmap.startx) || x >= (losmap.startx) + (losmap.xsize) || y < (losmap.starty) || y >= (losmap.starty) + (losmap.ysize)) { return; } losmap.flags |= MAPLOS_FLAG_SLITE; losmap.map[LOSMap_Hex2Index(&losmap, x, y)] |= flag; } /* To efficiently set all hexes NOLOS if neither sensor supports seeing * terrain, and (in the future) to set all hexes LOSALL if either sensor * sees all terrain (e.g. 'sattelite downlink' sensor) */ static void set_hexlosall(int flag) { memset(&(losmap.map), flag | MAPLOSHEX_SEEN, losmap.xsize * losmap.ysize); } /* The following functions are, effectively, STUBS. They should be replaced with functions in the sensor struct, instead of their functionality being copied all over the tree. */ static int MechSeesThroughWoods(MECH * mech, MAP *map, int nwoods, int sensor) { int sn = MechSensor(mech)[sensor]; int fake_losflag = nwoods * MECHLOSFLAG_WOOD; int res = sensors[sn].cansee_func(mech, NULL, map, 1, fake_losflag); return res; } static int MechSeesOverMountain(MECH * mech, MAP *map, int sensor) { int sn = MechSensor(mech)[sensor]; int fake_losflag = MECHLOSFLAG_MNTN; return sensors[sn].cansee_func(mech, NULL, map, 1, fake_losflag); } static int MechSeesThroughWater(MECH * mech, MAP *map, int nwater, int sensor) { int sn = MechSensor(mech)[sensor]; int fake_losflag = nwater * MECHLOSFLAG_WATER; return sensors[sn].cansee_func(mech, NULL, map, 1, fake_losflag); } static int MechSeesRange(MECH * mech, MAP * map, int x, int y, int z, int sensor) { int sn = MechSensor(mech)[sensor]; float fx, fy, range, maxvis = sensors[sn].maxvis; MapCoordToRealCoord(x, y, &fx, &fy); range = FindRange(MechFX(mech), MechFY(mech), MechFZ(mech), fx, fy, ZSCALE * z); /* XXX HACK: code duplication. this should be replaced with sensor * functions */ if (sn < 2) /* V or L sensors */ maxvis = map->mapvis; if (sn == 1 && map->maplight == 0) /* L sensors in darkness */ maxvis *= 2; if (!sensors[sn].fullvision) { int arc = InWeaponArc(mech, fx, fy); if (!(arc & (FORWARDARC | TURRETARC))) { if (MechSensor(mech)[0] == MechSensor(mech)[1]) maxvis = (maxvis * 200 / 300); else maxvis = 0; } } if (sn == 0 && maxvis && range >= maxvis && (losmap.flags & MAPLOS_FLAG_SLITE)) return -1; return range < maxvis; } static int MechSLitesRange(MECH * mech, int x, int y, int z) { float fx, fy, range; int arc, maxvis = 60; MapCoordToRealCoord(x, y, &fx, &fy); arc = InWeaponArc(mech, fx, fy); if (!(arc & (FORWARDARC | TURRETARC))) { return 0; } range = FindRange(MechFX(mech), MechFY(mech), MechFZ(mech), fx, fy, ZSCALE * z); return range < maxvis; } static int MechSeesTerrain(MECH * mech, int sn) { return MechSensor(mech)[sn] < 4; } /* General idea: stateful LOS checking. * To minimize the number of lostracing we do, we calculate the losmap by * tracing los to all 'edge' hexes, and traversing that line of hexes * marking each hex as 'seen' and as 'los-or-not'. For each sensor on the * 'mech, we keep track of how steep the angle has to be in order for the * sensor to 'see' the terrain. The start angle is -20 (which should be low * enough for common purposes, even on jumping 'mechs) for sensors that can * see terrain, and 1000 for sensors that can't -- basically flagging the * whole line of sight as 'not seen' for that sensor. * In order to take wood-blockage into account, we also keep track of the * minimum 'block' angle. That is, if it is not equal to minangle, * blockangle is the angle below which 'woodcount' woods stand between the * current hex and the seeing 'mech. If we end up with a hex between * minangle and blockangle, we need to check if the sensor can see through * that many woods. * Blocking entirely, because of water- or EM-effects, is done by setting * the minangle and blockangle to 1000, a value high enough to block los to * all following hexes. To determine whether a sensors sees through a hex, * fake losflags are passed to the regular sensor functions... hacks, and * logic-duplication (the worst kind) but they work for now. * This is all proof-of-concept, based on Cord Awtry's ideas for * 'underground' maps. This should all be rewritten, together with the * sensor code, to have one general 'tracelos' function, which calls * callbacks defined on a state struct and stores its state info on that * same struct. That way, map-los, 'mech-los, searchlight-los and such can * all use the same routine, using different callbacks. * Known bugs / problems: * - It behaves awkwardly around water. It doesn't handle the transition as * it should. This requires sufficient rewriting that I do not plan to do * it before the whole sensor overhaul. * - It has too great a leniency in what terrain height you can see. You can * sometimes see a level 1 hex behind a level 2 hex if you are in a 'mech, * fallen on a level 1 hex. (you shouldn't.) */ static void trace_slitelos(MAP * map, MECH * mech, int index, float start_height) { float minangle = -20; lostrace_info *trace_coords; int trace_range = 0; int trace_x, trace_y, trace_height; float trace_a; int trace_coordnum = TraceLOS(map, MechX(mech), MechY(mech), INDEX2X(index), INDEX2Y(index), &trace_coords); for (; trace_range < trace_coordnum; trace_range++) { trace_x = trace_coords[trace_range].x; trace_y = trace_coords[trace_range].y; trace_height = MAX(0, Elevation(map, trace_x, trace_y)); if (!MechSLitesRange(mech, trace_x, trace_y, trace_height)) return; trace_a = (trace_height - start_height) / (trace_range + 1); switch (GetTerrain(map, trace_x, trace_y)) { case HEAVY_FOREST: case LIGHT_FOREST: case SMOKE: trace_a += 2; } if (trace_a < minangle) continue; set_sliteinfo(trace_x, trace_y, MAPLOSHEX_LIT); minangle = trace_a; } } static void litemark_callback(MAP * map, int x, int y) { set_sliteinfo(x, y, MAPLOSHEX_LIT); } static void litemark_map(MAP * map) { MECH *mech; int i; int index; mapobj *fire; for (fire = first_mapobj(map, TYPE_FIRE); fire; fire = next_mapobj(fire)) { set_sliteinfo(fire->x, fire->y, MAPLOSHEX_LIT); visit_neighbor_hexes(map, fire->x, fire->y, litemark_callback); } for (i = 0; i < map->first_free; i++) { if (map->mechsOnMap[i] < 0) continue; mech = FindObjectsData(map->mechsOnMap[i]); if (!mech) continue; if (Jellied(mech)) { set_sliteinfo(MechX(mech), MechY(mech), MAPLOSHEX_LIT); visit_neighbor_hexes(map, MechX(mech), MechY(mech), litemark_callback); } if (!MechLites(mech)) continue; for (index = 0; index < losmap.xsize * losmap.ysize; index++) { trace_slitelos(map, mech, index, MechZ(mech) + MechHeight(mech)); } } } #define DEF_MINA(mech, sn) (MechSeesTerrain(mech, sn) ? -20 : 1000) static void trace_maphexlos(MAP * map, MECH * mech, int index, int tracew, float start_height) { int trace_water[MAX_SENSORS] = { tracew, tracew }; float minangle[MAX_SENSORS] = { DEF_MINA(mech, 0), DEF_MINA(mech, 1) }; float blockangle[MAX_SENSORS] = { DEF_MINA(mech, 0), DEF_MINA(mech, 1) }; int woodcount[MAX_SENSORS] = { 0, 0 }; int watercount[MAX_SENSORS] = { 0, 0 }; lostrace_info *trace_coords; int trace_range = 0; int trace_coordnum = TraceLOS(map, MechX(mech), MechY(mech), INDEX2X(index), INDEX2Y(index), &trace_coords); for (; trace_range < trace_coordnum; trace_range++) { int seestate; int trace_x = trace_coords[trace_range].x; int trace_y = trace_coords[trace_range].y; int trace_height = Elevation(map, trace_x, trace_y); float trace_a = (trace_height - start_height) / (trace_range + 1); float trace_ba = ((trace_height + 2 - start_height)) / (trace_range + 1); int trace_terrain = GetRTerrain(map, trace_x, trace_y); int nsensor, newwoods; for (nsensor = 0; nsensor < NUMSENSORS(mech); nsensor++) { /* If the current hex and all its terrain ('blockangle') lies * below our minimum angle of sight, we won't see it at all; * jump straight ahead to the water/mountain checks. This check * is made first, because it is the cheapest check and the * general mechanism to signal 'no more visibility on this line * of sight' is to set trace_ba to an impossible angle. */ if (trace_ba < minangle[nsensor]) { set_hexlosinfo(trace_x, trace_y, MAPLOSHEX_NOLOS); goto hexinfluence; } /* Then we check for range. */ seestate = MechSeesRange(mech, map, trace_x, trace_y, trace_height, nsensor); if (seestate == 0) { set_hexlosinfo(trace_x, trace_y, MAPLOSHEX_NOLOS); minangle[nsensor] = blockangle[nsensor] = 1000; goto hexinfluence; } /* Count the number of woods. */ newwoods = 0; switch (trace_terrain) { case HEAVY_FOREST: newwoods++; /* FALLTHROUGH */ case LIGHT_FOREST: newwoods++; /* Because we aren't in water, we stop tracing below water */ trace_water[nsensor] = 0; break; } if (!newwoods) { if (trace_a < minangle[nsensor] || (seestate < 0 && !hexlit(trace_x, trace_y))) { set_hexlosinfo(trace_x, trace_y, MAPLOSHEX_NOLOS); } else { set_hexlosinfo(trace_x, trace_y, MAPLOSHEX_SEE); blockangle[nsensor] = minangle[nsensor] = trace_a; woodcount[nsensor] = 0; } goto hexinfluence; } if (blockangle[nsensor] < trace_a) { minangle[nsensor] = trace_a; blockangle[nsensor] = trace_ba; woodcount[nsensor] = newwoods; } else if (!MechSeesThroughWoods(mech, map, woodcount[nsensor] + newwoods, nsensor)) { if (trace_ba >= blockangle[nsensor]) { minangle[nsensor] = blockangle[nsensor]; blockangle[nsensor] = trace_ba; woodcount[nsensor] = newwoods; } else minangle[nsensor] = trace_ba; } else { minangle[nsensor] = trace_a; woodcount[nsensor] += newwoods; } if (trace_terrain == WATER) { if (trace_water[nsensor]) watercount[nsensor]++; if (!trace_water[nsensor] || !MechSeesThroughWater(mech, map, watercount[nsensor], nsensor)) { if (seestate < 0 && !hexlit(trace_x, trace_y)) set_hexlosinfo(trace_x, trace_y, MAPLOSHEX_NOLOS); else set_hexlosinfo(trace_x, trace_y, MAPLOSHEX_SEETERRAIN); } } else if (seestate < 0 && !hexlit(trace_x, trace_y)) set_hexlosinfo(trace_x, trace_y, MAPLOSHEX_NOLOS); else set_hexlosinfo(trace_x, trace_y, MAPLOSHEX_SEE); hexinfluence: if (trace_terrain == WATER && !MechSeesThroughWater(mech, map, 1, nsensor)) { set_hexlosinfo(trace_x, trace_y, MAPLOSHEX_NOLOS); minangle[nsensor] = blockangle[nsensor] = 1000; continue; } trace_water[nsensor] = 0; if (trace_terrain == MOUNTAINS && !MechSeesOverMountain(mech, map, nsensor)) { set_hexlosinfo(trace_x, trace_y, MAPLOSHEX_NOLOS); minangle[nsensor] = blockangle[nsensor] = 1000; continue; } } } } hexlosmap_info *CalculateLOSMap(MAP * map, MECH * mech, int sx, int sy, int xsz, int ysz) { int index, underterrain, bothworlds; float start_height; /* Some safeguarding on size */ if (xsz > MAPLOS_MAXX || ysz > MAPLOS_MAXY) { SendError(tprintf("xsize (%d vs %d) or ysize (%d vs %d) " "to CalculateLOSMap too large, for mech #%d", xsz, MAPLOS_MAXX, ysz, MAPLOS_MAXY, mech->mynum)); return NULL; } losmap.startx = sx; losmap.starty = sy; losmap.xsize = xsz; losmap.ysize = ysz; losmap.flags = 0; memset(losmap.map, 0, xsz * ysz); underterrain = MechZ(mech) <= -1; if (IsWater(MechRTerrain(mech)) && ((MechType(mech) == CLASS_MECH && MechZ(mech) == -1) || ((WaterBeast(mech) || MechMove(mech) == MOVE_HOVER) && MechZ(mech) == 0))) { bothworlds = 1; } else bothworlds = 0; start_height = MechZ(mech) + MechHeight(mech); if (MechCritStatus(mech) & CLAIRVOYANT) { set_hexlosall(MAPLOSHEX_SEE); return &losmap; } if (!MechSeesTerrain(mech, 0) && !MechSeesTerrain(mech, 1)) { set_hexlosall(MAPLOSHEX_NOLOS); return &losmap; } /* In order for slites to properly light terrain, we have to mark the * losmap with all lit hexes first. Which means going over all 'mechs on * the map and tag all hexes that they light. */ litemark_map(map); /* In order to do the most efficient lostracing, we make losmaps by * first tracing from the 'mech hex to the upper Y-row, the lower Y-row, * the leftmost X-row, the rightmost X-row, and then all hexes starting * at the upper left corner to make sure we have seen all hexes. (It is * entirely possible for a hex not to be visited yet, even if we traced * to every other hex.) */ for (index = 0; index < xsz; index++) { if (losmap.map[index] & MAPLOSHEX_SEEN) continue; trace_maphexlos(map, mech, index, underterrain || bothworlds, start_height); } for (index = (ysz - 1) * xsz; index < ysz * xsz; index++) { if (losmap.map[index] & MAPLOSHEX_SEEN) continue; trace_maphexlos(map, mech, index, underterrain || bothworlds, start_height); } for (index = xsz; index < ysz * xsz; index += xsz) { if (losmap.map[index] & MAPLOSHEX_SEEN) continue; trace_maphexlos(map, mech, index, underterrain || bothworlds, start_height); } for (index = 2 * xsz - 1; index < ysz * xsz; index += xsz) { if (losmap.map[index] & MAPLOSHEX_SEEN) continue; trace_maphexlos(map, mech, index, underterrain || bothworlds, start_height); } for (index = 0; index < xsz * ysz; index++) { if (losmap.map[index] & MAPLOSHEX_SEEN) continue; trace_maphexlos(map, mech, index, underterrain || bothworlds, start_height); } return &losmap; }