/** * @file automap.cpp * * Implementation of the in-game map overlay. */ #include "automap.h" #include #include "control.h" #include "engine/render/automap_render.hpp" #include "inv.h" #include "monster.h" #include "palette.h" #include "player.h" #include "setmaps.h" #include "utils/language.h" #include "utils/ui_fwd.h" namespace devilution { namespace { /** * Maps from tile_id to automap type. */ std::array AutomapTypes; Point Automap; enum MapColors : uint8_t { /** color used to draw the player's arrow */ MapColorsPlayer = (PAL8_ORANGE + 1), /** color for bright map lines (doors, stairs etc.) */ MapColorsBright = PAL8_YELLOW, /** color for dim map lines/dots */ MapColorsDim = (PAL16_YELLOW + 8), /** color for items on automap */ MapColorsItem = (PAL8_BLUE + 1), }; constexpr uint16_t MapFlagsType = 0x000F; /** these are in the second byte */ enum MapFlags : uint8_t { // clang-format off MapFlagsMapVerticalDoor = 1 << 0, MapFlagsMapHorizontalDoor = 1 << 1, MapFlagsVerticalArch = 1 << 2, MapFlagsHorizontalArch = 1 << 3, MapFlagsVerticalGrate = 1 << 4, MapFlagsHorizontalGrate = 1 << 5, MapFlagsDirt = 1 << 6, MapFlagsStairs = 1 << 7, // clang-format on }; void DrawDiamond(const CelOutputBuffer &out, Point center, uint8_t color) { const Point left { center.x - AmLine16, center.y }; const Point top { center.x, center.y - AmLine8 }; const Point bottom { center.x, center.y + AmLine8 }; DrawMapLineNE(out, left, AmLine8, color); DrawMapLineSE(out, left, AmLine8, color); DrawMapLineSE(out, top, AmLine8, color); DrawMapLineNE(out, bottom, AmLine8, color); } void DrawMapVerticalDoor(const CelOutputBuffer &out, Point center) { DrawMapLineNE(out, { center.x + AmLine8, center.y - AmLine4 }, AmLine4, MapColorsDim); DrawMapLineNE(out, { center.x - AmLine16, center.y + AmLine8 }, AmLine4, MapColorsDim); DrawDiamond(out, center, MapColorsBright); } void DrawMapHorizontalDoor(const CelOutputBuffer &out, Point center) { DrawMapLineSE(out, { center.x - AmLine16, center.y - AmLine8 }, AmLine4, MapColorsDim); DrawMapLineSE(out, { center.x + AmLine8, center.y + AmLine4 }, AmLine4, MapColorsDim); DrawDiamond(out, center, MapColorsBright); } /** * @brief Renders the given automap shape at the specified screen coordinates. */ void DrawAutomapTile(const CelOutputBuffer &out, Point center, uint16_t automapType) { uint8_t flags = automapType >> 8; if ((flags & MapFlagsDirt) != 0) { out.SetPixel(center, MapColorsDim); out.SetPixel({ center.x - AmLine8, center.y - AmLine4 }, MapColorsDim); out.SetPixel({ center.x - AmLine8, center.y + AmLine4 }, MapColorsDim); out.SetPixel({ center.x + AmLine8, center.y - AmLine4 }, MapColorsDim); out.SetPixel({ center.x + AmLine8, center.y + AmLine4 }, MapColorsDim); out.SetPixel({ center.x - AmLine16, center.y }, MapColorsDim); out.SetPixel({ center.x + AmLine16, center.y }, MapColorsDim); out.SetPixel({ center.x, center.y - AmLine8 }, MapColorsDim); out.SetPixel({ center.x, center.y + AmLine8 }, MapColorsDim); out.SetPixel({ center.x + AmLine8 - AmLine32, center.y + AmLine4 }, MapColorsDim); out.SetPixel({ center.x - AmLine8 + AmLine32, center.y + AmLine4 }, MapColorsDim); out.SetPixel({ center.x - AmLine16, center.y + AmLine8 }, MapColorsDim); out.SetPixel({ center.x + AmLine16, center.y + AmLine8 }, MapColorsDim); out.SetPixel({ center.x - AmLine8, center.y + AmLine16 - AmLine4 }, MapColorsDim); out.SetPixel({ center.x + AmLine8, center.y + AmLine16 - AmLine4 }, MapColorsDim); out.SetPixel({ center.x, center.y + AmLine16 }, MapColorsDim); } if ((flags & MapFlagsStairs) != 0) { constexpr int NumStairSteps = 4; const Point offset = { -AmLine8, AmLine4 }; Point p = { center.x - AmLine8, center.y - AmLine8 - AmLine4 }; for (int i = 0; i < NumStairSteps; ++i) { DrawMapLineSE(out, p, AmLine16, MapColorsBright); p += offset; } } bool drawVertical = false; bool drawHorizontal = false; bool drawCaveHorizontal = false; bool drawCaveVertical = false; switch (automapType & MapFlagsType) { case 1: // stand-alone column or other unpassable object DrawDiamond(out, { center.x, center.y - AmLine8 }, MapColorsDim); break; case 2: case 5: drawVertical = true; break; case 3: case 6: drawHorizontal = true; break; case 4: drawVertical = true; drawHorizontal = true; break; case 8: drawVertical = true; drawCaveHorizontal = true; break; case 9: drawHorizontal = true; drawCaveVertical = true; break; case 10: drawCaveHorizontal = true; break; case 11: drawCaveVertical = true; break; case 12: drawCaveHorizontal = true; drawCaveVertical = true; break; } if (drawVertical) { // right-facing obstacle if ((flags & MapFlagsMapVerticalDoor) != 0) { // two wall segments with a door in the middle DrawMapVerticalDoor(out, { center.x - AmLine16, center.y - AmLine8 }); } if ((flags & MapFlagsVerticalGrate) != 0) { // right-facing half-wall DrawMapLineNE(out, { center.x - AmLine32, center.y }, AmLine8, MapColorsDim); flags |= MapFlagsVerticalArch; } if ((flags & MapFlagsVerticalArch) != 0) { // window or passable column DrawDiamond(out, { center.x, center.y - AmLine8 }, MapColorsDim); } if ((flags & (MapFlagsMapVerticalDoor | MapFlagsVerticalGrate | MapFlagsVerticalArch)) == 0) { DrawMapLineNE(out, { center.x - AmLine32, center.y }, AmLine16, MapColorsDim); } } if (drawHorizontal) { // left-facing obstacle if ((flags & MapFlagsMapHorizontalDoor) != 0) { DrawMapHorizontalDoor(out, { center.x + AmLine16, center.y - AmLine8 }); } if ((flags & MapFlagsHorizontalGrate) != 0) { DrawMapLineSE(out, { center.x + AmLine16, center.y - AmLine8 }, AmLine8, MapColorsDim); flags |= MapFlagsHorizontalArch; } if ((flags & MapFlagsHorizontalArch) != 0) { DrawDiamond(out, { center.x, center.y - AmLine8 }, MapColorsDim); } if ((flags & (MapFlagsMapHorizontalDoor | MapFlagsHorizontalGrate | MapFlagsHorizontalArch)) == 0) { DrawMapLineSE(out, { center.x, center.y - AmLine16 }, AmLine16, MapColorsDim); } } // For caves the horizontal/vertical flags are swapped if (drawCaveHorizontal) { if ((flags & MapFlagsMapVerticalDoor) != 0) { DrawMapHorizontalDoor(out, { center.x - AmLine16, center.y + AmLine8 }); } else { DrawMapLineSE(out, { center.x - AmLine32, center.y }, AmLine16, MapColorsDim); } } if (drawCaveVertical) { if ((flags & MapFlagsMapHorizontalDoor) != 0) { DrawMapVerticalDoor(out, { center.x + AmLine16, center.y + AmLine8 }); } else { DrawMapLineNE(out, { center.x, center.y + AmLine16 }, AmLine16, MapColorsDim); } } } void SearchAutomapItem(const CelOutputBuffer &out) { Point tile = plr[myplr].position.tile; if (plr[myplr]._pmode == PM_WALK3) { tile = plr[myplr].position.future; if (plr[myplr]._pdir == DIR_W) tile.x++; else tile.y++; } const int startX = clamp(tile.x - 8, 0, MAXDUNX); const int startY = clamp(tile.y - 8, 0, MAXDUNY); const int endX = clamp(tile.x + 8, 0, MAXDUNX); const int endY = clamp(tile.y + 8, 0, MAXDUNY); for (int i = startX; i < endX; i++) { for (int j = startY; j < endY; j++) { if (dItem[i][j] == 0) continue; int px = i - 2 * AutomapOffset.x - ViewX; int py = j - 2 * AutomapOffset.y - ViewY; Point screen = { (ScrollInfo.offset.x * AutoMapScale / 100 / 2) + (px - py) * AmLine16 + gnScreenWidth / 2, (ScrollInfo.offset.y * AutoMapScale / 100 / 2) + (px + py) * AmLine8 + (gnScreenHeight - PANEL_HEIGHT) / 2 }; if (CanPanelsCoverView()) { if (invflag || sbookflag) screen.x -= 160; if (chrflag || questlog) screen.x += 160; } screen.y -= AmLine8; DrawDiamond(out, screen, MapColorsItem); } } } /** * @brief Renders an arrow on the automap, centered on and facing the direction of the player. */ void DrawAutomapPlr(const CelOutputBuffer &out, int playerId) { int playerColor = MapColorsPlayer + (8 * playerId) % 128; Point tile = plr[playerId].position.tile; if (plr[playerId]._pmode == PM_WALK3) { tile = plr[playerId].position.future; if (plr[playerId]._pdir == DIR_W) tile.x++; else tile.y++; } int px = tile.x - 2 * AutomapOffset.x - ViewX; int py = tile.y - 2 * AutomapOffset.y - ViewY; Point base = { (plr[playerId].position.offset.x * AutoMapScale / 100 / 2) + (ScrollInfo.offset.x * AutoMapScale / 100 / 2) + (px - py) * AmLine16 + gnScreenWidth / 2, (plr[playerId].position.offset.y * AutoMapScale / 100 / 2) + (ScrollInfo.offset.y * AutoMapScale / 100 / 2) + (px + py) * AmLine8 + (gnScreenHeight - PANEL_HEIGHT) / 2 }; if (CanPanelsCoverView()) { if (invflag || sbookflag) base.x -= gnScreenWidth / 4; if (chrflag || questlog) base.x += gnScreenWidth / 4; } base.y -= AmLine8; switch (plr[playerId]._pdir) { case DIR_N: { const Point point { base.x, base.y - AmLine16 }; DrawVerticalLine(out, point, AmLine16, playerColor); DrawMapLineSteepNE(out, { point.x - AmLine4, point.y + 2 * AmLine4 }, AmLine4, playerColor); DrawMapLineSteepNW(out, { point.x + AmLine4, point.y + 2 * AmLine4 }, AmLine4, playerColor); } break; case DIR_NE: { const Point point { base.x + AmLine16, base.y - AmLine8 }; DrawHorizontalLine(out, { point.x - AmLine8, point.y }, AmLine8, playerColor); DrawMapLineNE(out, { point.x - 2 * AmLine8, point.y + AmLine8 }, AmLine8, playerColor); DrawMapLineSteepSW(out, point, AmLine4, playerColor); } break; case DIR_E: { const Point point { base.x + AmLine16, base.y }; DrawMapLineNW(out, point, AmLine4, playerColor); DrawHorizontalLine(out, { point.x - AmLine16, point.y }, AmLine16, playerColor); DrawMapLineSW(out, point, AmLine4, playerColor); } break; case DIR_SE: { const Point point { base.x + AmLine16, base.y + AmLine8 }; DrawMapLineSE(out, { point.x - 2 * AmLine8, point.y - AmLine8 }, AmLine8, playerColor); DrawHorizontalLine(out, { point.x - (AmLine8 + 1), point.y }, AmLine8 + 1, playerColor); DrawMapLineSteepNW(out, point, AmLine4, playerColor); } break; case DIR_S: case DIR_OMNI: { const Point point { base.x, base.y + AmLine16 }; DrawVerticalLine(out, { point.x, point.y - AmLine16 }, AmLine16, playerColor); DrawMapLineSteepSW(out, { point.x + AmLine4, point.y - 2 * AmLine4 }, AmLine4, playerColor); DrawMapLineSteepSE(out, { point.x - AmLine4, point.y - 2 * AmLine4 }, AmLine4, playerColor); } break; case DIR_SW: { const Point point { base.x - AmLine16, base.y + AmLine8 }; DrawMapLineSteepNE(out, point, AmLine4, playerColor); DrawMapLineSW(out, { point.x + 2 * AmLine8, point.y - AmLine8 }, AmLine8, playerColor); DrawHorizontalLine(out, point, AmLine8 + 1, playerColor); } break; case DIR_W: { const Point point { base.x - AmLine16, base.y }; DrawMapLineNE(out, point, AmLine4, playerColor); DrawHorizontalLine(out, point, AmLine16 + 1, playerColor); DrawMapLineSE(out, point, AmLine4, playerColor); } break; case DIR_NW: { const Point point { base.x - AmLine16, base.y - AmLine8 }; DrawMapLineNW(out, { point.x + 2 * AmLine8, point.y + AmLine8 }, AmLine8, playerColor); DrawHorizontalLine(out, point, AmLine8 + 1, playerColor); DrawMapLineSteepSE(out, point, AmLine4, playerColor); } break; } } /** * @brief Returns the automap shape at the given coordinate. */ uint16_t GetAutomapType(Point map, bool view) { if (view && map.x == -1 && map.y >= 0 && map.y < DMAXY && AutomapView[0][map.y]) { if ((GetAutomapType({ 0, map.y }, false) & (MapFlagsDirt << 8)) != 0) { return 0; } return MapFlagsDirt << 8; } if (view && map.y == -1 && map.x >= 0 && map.x < DMAXY && AutomapView[map.x][0]) { if ((GetAutomapType({ map.x, 0 }, false) & (MapFlagsDirt << 8)) != 0) { return 0; } return MapFlagsDirt << 8; } if (map.x < 0 || map.x >= DMAXX) { return 0; } if (map.y < 0 || map.y >= DMAXX) { return 0; } if (!AutomapView[map.x][map.y] && view) { return 0; } uint16_t rv = AutomapTypes[dungeon[map.x][map.y]]; if (rv == 7) { if (((GetAutomapType({ map.x - 1, map.y }, false) >> 8) & MapFlagsHorizontalArch) != 0) { if (((GetAutomapType({ map.x, map.y - 1 }, false) >> 8) & MapFlagsVerticalArch) != 0) { rv = 1; } } } return rv; } /** * @brief Renders game info, such as the name of the current level, and in multi player the name of the game and the game password. */ void DrawAutomapText(const CelOutputBuffer &out) { char desc[256]; int nextLine = 20; if (gbIsMultiplayer) { if (strcasecmp("0.0.0.0", szPlayerName) != 0) { strcat(strcpy(desc, _("game: ")), szPlayerName); DrawString(out, desc, { 8, nextLine, 0, 0 }); nextLine += 15; } if (szPlayerDescript[0] != '\0') { strcat(strcpy(desc, _("password: ")), szPlayerDescript); DrawString(out, desc, { 8, nextLine, 0, 0 }); nextLine += 15; } } if (setlevel) { DrawString(out, _(quest_level_names[setlvlnum]), { 8, nextLine, 0, 0 }); return; } if (currlevel != 0) { if (currlevel >= 17 && currlevel <= 20) { sprintf(desc, _("Level: Nest %i"), currlevel - 16); } else if (currlevel >= 21 && currlevel <= 24) { sprintf(desc, _("Level: Crypt %i"), currlevel - 20); } else { sprintf(desc, _("Level: %i"), currlevel); } DrawString(out, desc, { 8, nextLine, 0, 0 }); } } std::unique_ptr LoadAutomapData(size_t &tileCount) { switch (leveltype) { case DTYPE_CATHEDRAL: if (currlevel < 21) return LoadFileInMem("Levels\\L1Data\\L1.AMP", &tileCount); return LoadFileInMem("NLevels\\L5Data\\L5.AMP", &tileCount); case DTYPE_CATACOMBS: return LoadFileInMem("Levels\\L2Data\\L2.AMP", &tileCount); case DTYPE_CAVES: if (currlevel < 17) return LoadFileInMem("Levels\\L3Data\\L3.AMP", &tileCount); return LoadFileInMem("NLevels\\L6Data\\L6.AMP", &tileCount); case DTYPE_HELL: return LoadFileInMem("Levels\\L4Data\\L4.AMP", &tileCount); default: return nullptr; } } } // namespace bool AutomapActive; bool AutomapView[DMAXX][DMAXY]; int AutoMapScale; Point AutomapOffset; int AmLine64; int AmLine32; int AmLine16; int AmLine8; int AmLine4; void InitAutomapOnce() { AutomapActive = false; AutoMapScale = 50; AmLine64 = 32; AmLine32 = 16; AmLine16 = 8; AmLine8 = 4; AmLine4 = 2; } void InitAutomap() { size_t tileCount = 0; std::unique_ptr tileTypes = LoadAutomapData(tileCount); for (unsigned i = 0; i < tileCount; i++) { AutomapTypes[i + 1] = tileTypes[i]; } tileTypes = nullptr; memset(AutomapView, 0, sizeof(AutomapView)); for (auto &column : dFlags) for (auto &dFlag : column) dFlag &= ~BFLAG_EXPLORED; } void StartAutomap() { AutomapOffset = { 0, 0 }; AutomapActive = true; } void AutomapUp() { AutomapOffset.x--; AutomapOffset.y--; } void AutomapDown() { AutomapOffset.x++; AutomapOffset.y++; } void AutomapLeft() { AutomapOffset.x--; AutomapOffset.y++; } void AutomapRight() { AutomapOffset.x++; AutomapOffset.y--; } void AutomapZoomIn() { if (AutoMapScale >= 200) return; AutoMapScale += 5; AmLine64 = (AutoMapScale * 64) / 100; AmLine32 = AmLine64 / 2; AmLine16 = AmLine32 / 2; AmLine8 = AmLine16 / 2; AmLine4 = AmLine8 / 2; } void AutomapZoomOut() { if (AutoMapScale <= 50) return; AutoMapScale -= 5; AmLine64 = (AutoMapScale * 64) / 100; AmLine32 = AmLine64 / 2; AmLine16 = AmLine32 / 2; AmLine8 = AmLine16 / 2; AmLine4 = AmLine8 / 2; } void DrawAutomap(const CelOutputBuffer &out) { if (leveltype == DTYPE_TOWN) { DrawAutomapText(out); return; } Automap = { (ViewX - 16) / 2, (ViewY - 16) / 2 }; while (Automap.x + AutomapOffset.x < 0) AutomapOffset.x++; while (Automap.x + AutomapOffset.x >= DMAXX) AutomapOffset.x--; while (Automap.y + AutomapOffset.y < 0) AutomapOffset.y++; while (Automap.y + AutomapOffset.y >= DMAXY) AutomapOffset.y--; Automap += AutomapOffset; int d = (AutoMapScale * 64) / 100; int cells = 2 * (gnScreenWidth / 2 / d) + 1; if (((gnScreenWidth / 2) % d) != 0) cells++; if (((gnScreenWidth / 2) % d) >= (AutoMapScale * 32) / 100) cells++; if ((ScrollInfo.offset.x + ScrollInfo.offset.y) != 0) cells++; Point screen { gnScreenWidth / 2, (gnScreenHeight - PANEL_HEIGHT) / 2 }; if ((cells & 1) != 0) { screen.x -= AmLine64 * ((cells - 1) / 2); screen.y -= AmLine32 * ((cells + 1) / 2); } else { screen.x -= AmLine64 * (cells / 2) - AmLine32; screen.y -= AmLine32 * (cells / 2) + AmLine16; } if ((ViewX & 1) != 0) { screen.x -= AmLine16; screen.y -= AmLine8; } if ((ViewY & 1) != 0) { screen.x += AmLine16; screen.y -= AmLine8; } screen.x += AutoMapScale * ScrollInfo.offset.x / 100 / 2; screen.y += AutoMapScale * ScrollInfo.offset.y / 100 / 2; if (CanPanelsCoverView()) { if (invflag || sbookflag) { screen.x -= gnScreenWidth / 4; } if (chrflag || questlog) { screen.x += gnScreenWidth / 4; } } Point map = { Automap.x - cells, Automap.y - 1 }; for (int i = 0; i <= cells + 1; i++) { Point tile1 = screen; for (int j = 0; j < cells; j++) { uint16_t mapType = GetAutomapType({ map.x + j, map.y - j }, true); if (mapType != 0) DrawAutomapTile(out, tile1, mapType); tile1.x += AmLine64; } map.y++; Point tile2 { screen.x - AmLine32, screen.y + AmLine16 }; for (int j = 0; j <= cells; j++) { uint16_t mapType = GetAutomapType({ map.x + j, map.y - j }, true); if (mapType != 0) DrawAutomapTile(out, tile2, mapType); tile2.x += AmLine64; } map.x++; screen.y += AmLine32; } for (unsigned playerId = 0; playerId < MAX_PLRS; playerId++) { if (plr[playerId].plrlevel == plr[myplr].plrlevel && plr[playerId].plractive && !plr[playerId]._pLvlChanging) { DrawAutomapPlr(out, playerId); } } if (AutoMapShowItems) SearchAutomapItem(out); DrawAutomapText(out); } void SetAutomapView(Point tile) { const Point map { (tile.x - 16) / 2, (tile.y - 16) / 2 }; if (map.x < 0 || map.x >= DMAXX || map.y < 0 || map.y >= DMAXY) { return; } AutomapView[map.x][map.y] = true; uint16_t mapType = GetAutomapType(map, false); uint16_t solid = mapType & 0x4000; switch (mapType & MapFlagsType) { case 2: if (solid != 0) { if (GetAutomapType({ map.x, map.y + 1 }, false) == 0x4007) AutomapView[map.x][map.y + 1] = true; } else if ((GetAutomapType({ map.x - 1, map.y }, false) & 0x4000) != 0) { AutomapView[map.x - 1][map.y] = true; } break; case 3: if (solid != 0) { if (GetAutomapType({ map.x + 1, map.y }, false) == 0x4007) AutomapView[map.x + 1][map.y] = true; } else if ((GetAutomapType({ map.x, map.y - 1 }, false) & 0x4000) != 0) { AutomapView[map.x][map.y - 1] = true; } break; case 4: if (solid != 0) { if (GetAutomapType({ map.x, map.y + 1 }, false) == 0x4007) AutomapView[map.x][map.y + 1] = true; if (GetAutomapType({ map.x + 1, map.y }, false) == 0x4007) AutomapView[map.x + 1][map.y] = true; } else { if ((GetAutomapType({ map.x - 1, map.y }, false) & 0x4000) != 0) AutomapView[map.x - 1][map.y] = true; if ((GetAutomapType({ map.x, map.y - 1 }, false) & 0x4000) != 0) AutomapView[map.x][map.y - 1] = true; if ((GetAutomapType({ map.x - 1, map.y - 1 }, false) & 0x4000) != 0) AutomapView[map.x - 1][map.y - 1] = true; } break; case 5: if (solid != 0) { if ((GetAutomapType({ map.x, map.y - 1 }, false) & 0x4000) != 0) AutomapView[map.x][map.y - 1] = true; if (GetAutomapType({ map.x, map.y + 1 }, false) == 0x4007) AutomapView[map.x][map.y + 1] = true; } else if ((GetAutomapType({ map.x - 1, map.y }, false) & 0x4000) != 0) { AutomapView[map.x - 1][map.y] = true; } break; case 6: if (solid != 0) { if ((GetAutomapType({ map.x - 1, map.y }, false) & 0x4000) != 0) AutomapView[map.x - 1][map.y] = true; if (GetAutomapType({ map.x + 1, map.y }, false) == 0x4007) AutomapView[map.x + 1][map.y] = true; } else if ((GetAutomapType({ map.x, map.y - 1 }, false) & 0x4000) != 0) { AutomapView[map.x][map.y - 1] = true; } break; } } void AutomapZoomReset() { AutomapOffset = { 0, 0 }; AmLine64 = (AutoMapScale * 64) / 100; AmLine32 = AmLine64 / 2; AmLine16 = AmLine32 / 2; AmLine8 = AmLine16 / 2; AmLine4 = AmLine8 / 2; } } // namespace devilution