/** * @file objects.cpp * * Implementation of object functionality, interaction, spawning, loading, etc. */ #include #include #include #include #include #include #include #include #include "DiabloUI/ui_flags.hpp" #include "automap.h" #include "cursor.h" #ifdef _DEBUG #include "debug.h" #endif #include "diablo_msg.hpp" #include "engine/backbuffer_state.hpp" #include "engine/load_cel.hpp" #include "engine/load_file.hpp" #include "engine/points_in_rectangle_range.hpp" #include "engine/random.hpp" #include "headless_mode.hpp" #include "inv.h" #include "inv_iterators.hpp" #include "levels/crypt.h" #include "levels/drlg_l4.h" #include "levels/setmaps.h" #include "levels/themes.h" #include "levels/tile_properties.hpp" #include "lighting.h" #include "minitext.h" #include "missiles.h" #include "monster.h" #include "objdat.h" #include "options.h" #include "qol/stash.h" #include "stores.h" #include "towners.h" #include "track.h" #include "utils/algorithm/container.hpp" #include "utils/is_of.hpp" #include "utils/language.h" #include "utils/log.hpp" #include "utils/str_cat.hpp" namespace devilution { Object Objects[MAXOBJECTS]; int AvailableObjects[MAXOBJECTS]; int ActiveObjects[MAXOBJECTS]; int ActiveObjectCount; bool LoadingMapObjects; namespace { enum shrine_type : uint8_t { ShrineMysterious, ShrineHidden, ShrineGloomy, ShrineWeird, ShrineMagical, ShrineStone, ShrineReligious, ShrineEnchanted, ShrineThaumaturgic, ShrineFascinating, ShrineCryptic, ShrineMagicaL2, ShrineEldritch, ShrineEerie, ShrineDivine, ShrineHoly, ShrineSacred, ShrineSpiritual, ShrineSpooky, ShrineAbandoned, ShrineCreepy, ShrineQuiet, ShrineSecluded, ShrineOrnate, ShrineGlimmering, ShrineTainted, ShrineOily, ShrineGlowing, ShrineMendicant, ShrineSparkling, ShrineTown, ShrineShimmering, ShrineSolar, ShrineMurphys, NumberOfShrineTypes }; enum { // clang-format off DOOR_CLOSED = 0, DOOR_OPEN = 1, DOOR_BLOCKED = 2, // clang-format on }; int trapid; int trapdir; OptionalOwnedClxSpriteList pObjCels[40]; object_graphic_id ObjFileList[40]; /** Specifies the number of active objects. */ int leverid; int numobjfiles; /** Tracks progress through the tome sequence that spawns Na-Krul (see OperateNakrulBook()) */ int NaKrulTomeSequence; /** Specifies the X-coordinate delta between barrels. */ int bxadd[8] = { -1, 0, 1, -1, 1, -1, 0, 1 }; /** Specifies the Y-coordinate delta between barrels. */ int byadd[8] = { -1, -1, -1, 0, 0, 1, 1, 1 }; /** Maps from shrine_id to shrine name. */ const char *const ShrineNames[] = { // TRANSLATORS: Shrine Name Block N_("Mysterious"), N_("Hidden"), N_("Gloomy"), N_("Weird"), N_("Magical"), N_("Stone"), N_("Religious"), N_("Enchanted"), N_("Thaumaturgic"), N_("Fascinating"), N_("Cryptic"), N_("Magical"), N_("Eldritch"), N_("Eerie"), N_("Divine"), N_("Holy"), N_("Sacred"), N_("Spiritual"), N_("Spooky"), N_("Abandoned"), N_("Creepy"), N_("Quiet"), N_("Secluded"), N_("Ornate"), N_("Glimmering"), N_("Tainted"), N_("Oily"), N_("Glowing"), N_("Mendicant's"), N_("Sparkling"), N_("Town"), N_("Shimmering"), N_("Solar"), // TRANSLATORS: Shrine Name Block end N_("Murphy's"), }; /** * Specifies the game type for which each shrine may appear. * ShrineTypeAny - sp & mp * ShrineTypeSingle - sp only * ShrineTypeMulti - mp only */ enum shrine_gametype : uint8_t { ShrineTypeAny, ShrineTypeSingle, ShrineTypeMulti, }; shrine_gametype shrineavail[] = { ShrineTypeAny, // Mysterious ShrineTypeAny, // Hidden ShrineTypeSingle, // Gloomy ShrineTypeSingle, // Weird ShrineTypeAny, // Magical ShrineTypeAny, // Stone ShrineTypeAny, // Religious ShrineTypeAny, // Enchanted ShrineTypeSingle, // Thaumaturgic ShrineTypeAny, // Fascinating ShrineTypeAny, // Cryptic ShrineTypeAny, // Magical ShrineTypeAny, // Eldritch ShrineTypeAny, // Eerie ShrineTypeAny, // Divine ShrineTypeAny, // Holy ShrineTypeAny, // Sacred ShrineTypeAny, // Spiritual ShrineTypeMulti, // Spooky ShrineTypeAny, // Abandoned ShrineTypeAny, // Creepy ShrineTypeAny, // Quiet ShrineTypeAny, // Secluded ShrineTypeAny, // Ornate ShrineTypeAny, // Glimmering ShrineTypeMulti, // Tainted ShrineTypeAny, // Oily ShrineTypeAny, // Glowing ShrineTypeAny, // Mendicant's ShrineTypeAny, // Sparkling ShrineTypeAny, // Town ShrineTypeAny, // Shimmering ShrineTypeSingle, // Solar, ShrineTypeAny, // Murphy's }; /** Maps from book_id to book name. */ const char *const StoryBookName[] = { N_(/* TRANSLATORS: Book Title */ "The Great Conflict"), N_(/* TRANSLATORS: Book Title */ "The Wages of Sin are War"), N_(/* TRANSLATORS: Book Title */ "The Tale of the Horadrim"), N_(/* TRANSLATORS: Book Title */ "The Dark Exile"), N_(/* TRANSLATORS: Book Title */ "The Sin War"), N_(/* TRANSLATORS: Book Title */ "The Binding of the Three"), N_(/* TRANSLATORS: Book Title */ "The Realms Beyond"), N_(/* TRANSLATORS: Book Title */ "Tale of the Three"), N_(/* TRANSLATORS: Book Title */ "The Black King"), N_(/* TRANSLATORS: Book Title */ "Journal: The Ensorcellment"), N_(/* TRANSLATORS: Book Title */ "Journal: The Meeting"), N_(/* TRANSLATORS: Book Title */ "Journal: The Tirade"), N_(/* TRANSLATORS: Book Title */ "Journal: His Power Grows"), N_(/* TRANSLATORS: Book Title */ "Journal: NA-KRUL"), N_(/* TRANSLATORS: Book Title */ "Journal: The End"), N_(/* TRANSLATORS: Book Title */ "A Spellbook"), }; /** Specifies the speech IDs of each dungeon type narrator book, for each player class. */ _speech_id StoryText[3][3] = { { TEXT_BOOK11, TEXT_BOOK12, TEXT_BOOK13 }, { TEXT_BOOK21, TEXT_BOOK22, TEXT_BOOK23 }, { TEXT_BOOK31, TEXT_BOOK32, TEXT_BOOK33 } }; bool RndLocOk(Point p) { if (dMonster[p.x][p.y] != 0) return false; if (dPlayer[p.x][p.y] != 0) return false; if (IsObjectAtPosition(p)) return false; if (TileContainsSetPiece(p)) return false; if (TileHasAny(p, TileProperties::Solid)) return false; return IsNoneOf(leveltype, DTYPE_CATHEDRAL, DTYPE_CRYPT) || dPiece[p.x][p.y] <= 125 || dPiece[p.x][p.y] >= 143; } bool IsAreaOk(Rectangle rect) { return c_all_of(PointsInRectangle(rect), &RndLocOk); } bool CanPlaceWallTrap(Point pos) { if (dObject[pos.x][pos.y] != 0) return false; if (TileContainsSetPiece(pos)) return false; return TileHasAny(pos, TileProperties::Trap); } void InitRndLocObj(int min, int max, _object_id objtype) { const int numobjs = GenerateRnd(max - min) + min; for (int i = 0; i < numobjs; i++) { while (true) { const int xp = GenerateRnd(80) + 16; const int yp = GenerateRnd(80) + 16; if (IsAreaOk(Rectangle { { xp - 1, yp - 1 }, { 3, 3 } })) { AddObject(objtype, { xp, yp }); break; } } } } void InitRndLocBigObj(int min, int max, _object_id objtype) { const int numobjs = GenerateRnd(max - min) + min; for (int i = 0; i < numobjs; i++) { while (true) { const int xp = GenerateRnd(80) + 16; const int yp = GenerateRnd(80) + 16; if (IsAreaOk(Rectangle { { xp - 1, yp - 2 }, { 3, 4 } })) { AddObject(objtype, { xp, yp }); break; } } } } bool CanPlaceRandomObject(Point position, Displacement standoff) { return IsAreaOk(Rectangle { position - standoff, Size { standoff.deltaX * 2 + 1, standoff.deltaY * 2 + 1 } }); } std::optional GetRandomObjectPosition(Displacement standoff) { for (int i = 0; i <= 20000; i++) { Point position = Point { GenerateRnd(80), GenerateRnd(80) } + Displacement { 16, 16 }; if (CanPlaceRandomObject(position, standoff)) return position; } return {}; } void InitRndLocObj5x5(int min, int max, _object_id objtype) { const int numobjs = min + GenerateRnd(max - min); for (int i = 0; i < numobjs; i++) { std::optional position = GetRandomObjectPosition({ 2, 2 }); if (!position) return; AddObject(objtype, *position); } } void ClrAllObjects() { for (Object &object : Objects) { object = {}; } ActiveObjectCount = 0; for (int i = 0; i < MAXOBJECTS; i++) { AvailableObjects[i] = i; } memset(ActiveObjects, 0, sizeof(ActiveObjects)); trapdir = 0; trapid = 1; leverid = 1; } void AddTortures() { for (int oy = 0; oy < MAXDUNY; oy++) { for (int ox = 0; ox < MAXDUNX; ox++) { if (dPiece[ox][oy] == 366) { AddObject(OBJ_TORTURE1, { ox, oy + 1 }); AddObject(OBJ_TORTURE3, { ox + 2, oy - 1 }); AddObject(OBJ_TORTURE2, { ox, oy + 3 }); AddObject(OBJ_TORTURE4, { ox + 4, oy - 1 }); AddObject(OBJ_TORTURE5, { ox, oy + 5 }); AddObject(OBJ_TNUDEM1, { ox + 1, oy + 3 }); AddObject(OBJ_TNUDEM2, { ox + 4, oy + 5 }); AddObject(OBJ_TNUDEM3, { ox + 2, oy }); AddObject(OBJ_TNUDEM4, { ox + 3, oy + 2 }); AddObject(OBJ_TNUDEW1, { ox + 2, oy + 4 }); AddObject(OBJ_TNUDEW2, { ox + 2, oy + 1 }); AddObject(OBJ_TNUDEW3, { ox + 4, oy + 2 }); } } } } void AddCandles() { const int tx = Quests[Q_PWATER].position.x; const int ty = Quests[Q_PWATER].position.y; AddObject(OBJ_STORYCANDLE, { tx - 2, ty + 1 }); AddObject(OBJ_STORYCANDLE, { tx + 3, ty + 1 }); AddObject(OBJ_STORYCANDLE, { tx - 1, ty + 2 }); AddObject(OBJ_STORYCANDLE, { tx + 2, ty + 2 }); } /** * @brief Attempts to spawn a book somewhere on the current floor which when activated will change a region of the map. * * This object acts like a lever and will cause a change to the map based on what quest is active. The exact effect is * determined by OperateBookLever(). * * @param affectedArea The map region to be updated when this object is activated by the player. * @param msg The quest text to play when the player activates the book. */ void AddBookLever(_object_id type, WorldTileRectangle affectedArea, _speech_id msg) { std::optional position = GetRandomObjectPosition({ 2, 2 }); if (!position) return; if (type == OBJ_BLOODBOOK) position = SetPiece.position.megaToWorld() + Displacement { 9, 24 }; Object *lever = AddObject(type, *position); assert(lever != nullptr); lever->InitializeQuestBook(affectedArea, leverid, msg); leverid++; } void InitRndBarrels() { _object_id barrelId = OBJ_BARREL; _object_id explosiveBarrelId = OBJ_BARRELEX; if (leveltype == DTYPE_NEST) { barrelId = OBJ_POD; explosiveBarrelId = OBJ_PODEX; } else if (leveltype == DTYPE_CRYPT) { barrelId = OBJ_URN; explosiveBarrelId = OBJ_URNEX; } /** number of groups of barrels to generate */ const int numobjs = GenerateRnd(5) + 3; for (int i = 0; i < numobjs; i++) { int xp; int yp; do { xp = GenerateRnd(80) + 16; yp = GenerateRnd(80) + 16; } while (!RndLocOk({ xp, yp })); _object_id o = FlipCoin(4) ? explosiveBarrelId : barrelId; AddObject(o, { xp, yp }); bool found = true; /** regulates chance to stop placing barrels in current group */ int p = 0; /** number of barrels in current group */ int c = 1; while (FlipCoin(p) && found) { /** number of tries of placing next barrel in current group */ int t = 0; found = false; while (true) { if (t >= 3) break; const int dir = GenerateRnd(8); xp += bxadd[dir]; yp += byadd[dir]; found = RndLocOk({ xp, yp }); t++; if (found) break; } if (found) { o = FlipCoin(5) ? explosiveBarrelId : barrelId; AddObject(o, { xp, yp }); c++; } p = c / 2; } } } void AddL2Torches() { for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) { const Point testPosition = { i, j }; if (TileContainsSetPiece(testPosition)) continue; const int pn = dPiece[i][j]; if (pn == 0 && FlipCoin(3)) { AddObject(OBJ_TORCHL2, testPosition); } if (pn == 4 && FlipCoin(3)) { AddObject(OBJ_TORCHR2, testPosition); } if (pn == 36 && FlipCoin(10) && !IsObjectAtPosition(testPosition + Direction::NorthWest)) { AddObject(OBJ_TORCHL, testPosition + Direction::NorthWest); } if (pn == 40 && FlipCoin(10) && !IsObjectAtPosition(testPosition + Direction::NorthEast)) { AddObject(OBJ_TORCHR, testPosition + Direction::NorthEast); } } } } void AddObjTraps() { int rndv; if (currlevel == 1) rndv = 10; if (currlevel >= 2) rndv = 15; if (currlevel >= 5) rndv = 20; if (currlevel >= 7) rndv = 25; for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) { Object *triggerObject = FindObjectAtPosition({ i, j }, false); if (triggerObject == nullptr || GenerateRnd(100) >= rndv) continue; if (!AllObjects[triggerObject->_otype].isTrap()) continue; Object *trapObject = nullptr; if (FlipCoin()) { int xp = i - 1; while (IsTileNotSolid({ xp, j })) xp--; if (!CanPlaceWallTrap({ xp, j }) || i - xp <= 1) continue; trapObject = AddObject(OBJ_TRAPL, { xp, j }); } else { int yp = j - 1; while (IsTileNotSolid({ i, yp })) yp--; if (!CanPlaceWallTrap({ i, yp }) || j - yp <= 1) continue; trapObject = AddObject(OBJ_TRAPR, { i, yp }); } if (trapObject != nullptr) { // nullptr check just in case we fail to find a valid location to place a trap in the chosen direction trapObject->_oVar1 = i; trapObject->_oVar2 = j; triggerObject->_oTrapFlag = true; } } } } void AddChestTraps() { for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) { // NOLINT(modernize-loop-convert) Object *chestObject = FindObjectAtPosition({ i, j }, false); if (chestObject != nullptr && chestObject->IsUntrappedChest() && GenerateRnd(100) < 10) { switch (chestObject->_otype) { case OBJ_CHEST1: chestObject->_otype = OBJ_TCHEST1; break; case OBJ_CHEST2: chestObject->_otype = OBJ_TCHEST2; break; case OBJ_CHEST3: chestObject->_otype = OBJ_TCHEST3; break; default: break; } chestObject->_oTrapFlag = true; if (leveltype == DTYPE_CATACOMBS) { chestObject->_oVar4 = GenerateRnd(2); } else { chestObject->_oVar4 = GenerateRnd(gbIsHellfire ? 6 : 3); } } } } } void LoadMapObjects(const char *path, Point start, WorldTileRectangle mapRange = {}, int leveridx = 0) { LoadingMapObjects = true; auto dunData = LoadFileInMem(path); WorldTileSize size = GetDunSize(dunData.get()); const int layer2Offset = 2 + size.width * size.height; // The rest of the layers are at dPiece scale size *= static_cast(2); const uint16_t *objectLayer = &dunData[layer2Offset + size.width * size.height * 2]; for (WorldTileCoord j = 0; j < size.height; j++) { for (WorldTileCoord i = 0; i < size.width; i++) { auto objectId = static_cast(SDL_SwapLE16(objectLayer[j * size.width + i])); if (objectId != 0) { const Point mapPos = start + Displacement { i, j }; Object *mapObject = AddObject(ObjTypeConv[objectId], mapPos); if (leveridx > 0 && mapObject != nullptr) mapObject->InitializeLoadedObject(mapRange, leveridx); } } } LoadingMapObjects = false; } void AddDiabObjs() { LoadMapObjects("levels\\l4data\\diab1.dun", DiabloQuad1.megaToWorld(), { DiabloQuad2, { 11, 12 } }, 1); LoadMapObjects("levels\\l4data\\diab2a.dun", DiabloQuad2.megaToWorld(), { DiabloQuad3, { 11, 11 } }, 2); LoadMapObjects("levels\\l4data\\diab3a.dun", DiabloQuad3.megaToWorld(), { DiabloQuad4, { 9, 9 } }, 3); } void AddCryptObject(Object &object, int a2) { if (a2 > 5) { const Player &myPlayer = *MyPlayer; switch (a2) { case 6: switch (myPlayer._pClass) { case HeroClass::Warrior: case HeroClass::Barbarian: object._oVar2 = TEXT_BOOKA; break; case HeroClass::Rogue: object._oVar2 = TEXT_RBOOKA; break; case HeroClass::Sorcerer: object._oVar2 = TEXT_MBOOKA; break; case HeroClass::Monk: object._oVar2 = TEXT_OBOOKA; break; case HeroClass::Bard: object._oVar2 = TEXT_BBOOKA; break; } break; case 7: switch (myPlayer._pClass) { case HeroClass::Warrior: case HeroClass::Barbarian: object._oVar2 = TEXT_BOOKB; break; case HeroClass::Rogue: object._oVar2 = TEXT_RBOOKB; break; case HeroClass::Sorcerer: object._oVar2 = TEXT_MBOOKB; break; case HeroClass::Monk: object._oVar2 = TEXT_OBOOKB; break; case HeroClass::Bard: object._oVar2 = TEXT_BBOOKB; break; } break; case 8: switch (myPlayer._pClass) { case HeroClass::Warrior: case HeroClass::Barbarian: object._oVar2 = TEXT_BOOKC; break; case HeroClass::Rogue: object._oVar2 = TEXT_RBOOKC; break; case HeroClass::Sorcerer: object._oVar2 = TEXT_MBOOKC; break; case HeroClass::Monk: object._oVar2 = TEXT_OBOOKC; break; case HeroClass::Bard: object._oVar2 = TEXT_BBOOKC; break; } break; } object._oVar3 = 15; object._oVar8 = a2; } else { object._oVar2 = a2 + TEXT_SKLJRN; object._oVar3 = a2 + 9; object._oVar8 = 0; } object._oVar1 = 1; object._oAnimFrame = 5 - 2 * object._oVar1; object._oVar4 = object._oAnimFrame + 1; } void SetupObject(Object &object, Point position, _object_id ot) { const ObjectData &objectData = AllObjects[ot]; object._otype = ot; object_graphic_id ofi = objectData.ofindex; object.position = position; if (!HeadlessMode) { const auto &found = c_find(ObjFileList, ofi); if (found == std::end(ObjFileList)) { LogCritical("Unable to find object_graphic_id {} in list of objects to load, level generation error.", static_cast(ofi)); return; } const size_t j = std::distance(std::begin(ObjFileList), found); if (pObjCels[j]) { object._oAnimData.emplace(*pObjCels[j]); } else { object._oAnimData = std::nullopt; } } object._oAnimFlag = objectData.isAnimated(); if (object._oAnimFlag) { object._oAnimDelay = objectData.animDelay; object._oAnimCnt = GenerateRnd(object._oAnimDelay); object._oAnimLen = objectData.animLen; object._oAnimFrame = GenerateRnd(object._oAnimLen - 1) + 1; } else { object._oAnimDelay = 1000; object._oAnimCnt = 0; object._oAnimLen = objectData.animLen; object._oAnimFrame = objectData.animDelay; } object._oAnimWidth = objectData.animWidth; object._oSolidFlag = objectData.isSolid() ? 1 : 0; object._oMissFlag = objectData.missilesPassThrough() ? 1 : 0; object.applyLighting = objectData.applyLighting(); object._oDelFlag = false; object._oBreak = objectData.isBreakable() ? 1 : 0; object.selectionRegion = objectData.selectionRegion; object._oPreFlag = false; object._oTrapFlag = false; object._oDoorFlag = false; } void AddCryptBook(_object_id ot, int v2, Point position) { if (ActiveObjectCount >= MAXOBJECTS) return; const int oi = AvailableObjects[0]; AvailableObjects[0] = AvailableObjects[MAXOBJECTS - 1 - ActiveObjectCount]; ActiveObjects[ActiveObjectCount] = oi; dObject[position.x][position.y] = oi + 1; Object &object = Objects[oi]; SetupObject(object, position, ot); AddCryptObject(object, v2); ActiveObjectCount++; } void AddCryptStoryBook(int s) { std::optional position = GetRandomObjectPosition({ 3, 2 }); if (!position) return; AddCryptBook(OBJ_L5BOOKS, s, *position); AddObject(OBJ_L5CANDLE, *position + Displacement { -2, 1 }); AddObject(OBJ_L5CANDLE, *position + Displacement { -2, 0 }); AddObject(OBJ_L5CANDLE, *position + Displacement { -1, -1 }); AddObject(OBJ_L5CANDLE, *position + Displacement { 1, -1 }); AddObject(OBJ_L5CANDLE, *position + Displacement { 2, 0 }); AddObject(OBJ_L5CANDLE, *position + Displacement { 2, 1 }); } void AddNakrulLever() { while (true) { const int xp = GenerateRnd(80) + 16; const int yp = GenerateRnd(80) + 16; if (IsAreaOk(Rectangle { { xp - 1, yp - 1 }, { 3, 3 } })) { break; } } AddObject(OBJ_L5LEVER, { UberRow + 3, UberCol - 1 }); } void AddNakrulBook(int a1, Point position) { AddCryptBook(OBJ_L5BOOKS, a1, position); } void AddNakrulGate() { AddNakrulLever(); switch (GenerateRnd(6)) { case 0: AddNakrulBook(6, { UberRow + 3, UberCol }); AddNakrulBook(7, { UberRow + 2, UberCol - 3 }); AddNakrulBook(8, { UberRow + 2, UberCol + 2 }); break; case 1: AddNakrulBook(6, { UberRow + 3, UberCol }); AddNakrulBook(8, { UberRow + 2, UberCol - 3 }); AddNakrulBook(7, { UberRow + 2, UberCol + 2 }); break; case 2: AddNakrulBook(7, { UberRow + 3, UberCol }); AddNakrulBook(6, { UberRow + 2, UberCol - 3 }); AddNakrulBook(8, { UberRow + 2, UberCol + 2 }); break; case 3: AddNakrulBook(7, { UberRow + 3, UberCol }); AddNakrulBook(8, { UberRow + 2, UberCol - 3 }); AddNakrulBook(6, { UberRow + 2, UberCol + 2 }); break; case 4: AddNakrulBook(8, { UberRow + 3, UberCol }); AddNakrulBook(7, { UberRow + 2, UberCol - 3 }); AddNakrulBook(6, { UberRow + 2, UberCol + 2 }); break; case 5: AddNakrulBook(8, { UberRow + 3, UberCol }); AddNakrulBook(6, { UberRow + 2, UberCol - 3 }); AddNakrulBook(7, { UberRow + 2, UberCol + 2 }); break; } } void AddStoryBooks() { std::optional position = GetRandomObjectPosition({ 3, 2 }); if (!position) return; AddObject(OBJ_STORYBOOK, *position); AddObject(OBJ_STORYCANDLE, *position + Displacement { -2, 1 }); AddObject(OBJ_STORYCANDLE, *position + Displacement { -2, 0 }); AddObject(OBJ_STORYCANDLE, *position + Displacement { -1, -1 }); AddObject(OBJ_STORYCANDLE, *position + Displacement { 1, -1 }); AddObject(OBJ_STORYCANDLE, *position + Displacement { 2, 0 }); AddObject(OBJ_STORYCANDLE, *position + Displacement { 2, 1 }); } void AddHookedBodies(int freq) { for (WorldTileCoord j = 0; j < DMAXY; j++) { const WorldTileCoord jj = 16 + j * 2; for (WorldTileCoord i = 0; i < DMAXX; i++) { const WorldTileCoord ii = 16 + i * 2; if (dungeon[i][j] != 1 && dungeon[i][j] != 2) continue; if (!FlipCoin(freq)) continue; if (IsNearThemeRoom({ i, j })) continue; if (dungeon[i][j] == 1 && dungeon[i + 1][j] == 6) { switch (GenerateRnd(3)) { case 0: AddObject(OBJ_TORTURE1, { ii + 1, jj }); break; case 1: AddObject(OBJ_TORTURE2, { ii + 1, jj }); break; case 2: AddObject(OBJ_TORTURE5, { ii + 1, jj }); break; } continue; } if (dungeon[i][j] == 2 && dungeon[i][j + 1] == 6) { AddObject(PickRandomlyAmong({ OBJ_TORTURE3, OBJ_TORTURE4 }), { ii, jj }); } } } } void AddL4Goodies() { AddHookedBodies(6); InitRndLocObj(2, 6, OBJ_TNUDEM1); InitRndLocObj(2, 6, OBJ_TNUDEM2); InitRndLocObj(2, 6, OBJ_TNUDEM3); InitRndLocObj(2, 6, OBJ_TNUDEM4); InitRndLocObj(2, 6, OBJ_TNUDEW1); InitRndLocObj(2, 6, OBJ_TNUDEW2); InitRndLocObj(2, 6, OBJ_TNUDEW3); InitRndLocObj(2, 6, OBJ_DECAP); InitRndLocObj(1, 3, OBJ_CAULDRON); } void AddLazStand() { int cnt = 0; int xp; int yp; while (true) { xp = GenerateRnd(80) + 16; yp = GenerateRnd(80) + 16; if (!IsAreaOk(Rectangle { { xp - 2, yp - 3 }, { 6, 7 } })) { cnt++; if (cnt > 10000) { InitRndLocObj(1, 1, OBJ_LAZSTAND); return; } } else { break; } } AddObject(OBJ_LAZSTAND, { xp, yp }); AddObject(OBJ_TNUDEM2, { xp, yp + 2 }); AddObject(OBJ_STORYCANDLE, { xp + 1, yp + 2 }); AddObject(OBJ_TNUDEM3, { xp + 2, yp + 2 }); AddObject(OBJ_TNUDEW1, { xp, yp - 2 }); AddObject(OBJ_STORYCANDLE, { xp + 1, yp - 2 }); AddObject(OBJ_TNUDEW2, { xp + 2, yp - 2 }); AddObject(OBJ_STORYCANDLE, { xp - 1, yp - 1 }); AddObject(OBJ_TNUDEW3, { xp - 1, yp }); AddObject(OBJ_STORYCANDLE, { xp - 1, yp + 1 }); } void DeleteObject(int oi, int i) { const Object &object = Objects[oi]; const Point position = object.position; dObject[position.x][position.y] = 0; AvailableObjects[-ActiveObjectCount + MAXOBJECTS] = oi; ActiveObjectCount--; if (ObjectUnderCursor == &object) // Unselect object if this was highlighted by player ObjectUnderCursor = nullptr; if (ActiveObjectCount > 0 && i != ActiveObjectCount) ActiveObjects[i] = ActiveObjects[ActiveObjectCount]; } void AddChest(Object &chest) { if (FlipCoin()) chest._oAnimFrame += 3; chest._oRndSeed = AdvanceRndSeed(); switch (chest._otype) { case OBJ_CHEST1: case OBJ_TCHEST1: if (setlevel) { chest._oVar1 = 1; break; } chest._oVar1 = GenerateRnd(2); break; case OBJ_TCHEST2: case OBJ_CHEST2: if (setlevel) { chest._oVar1 = 2; break; } chest._oVar1 = GenerateRnd(3); break; case OBJ_TCHEST3: case OBJ_CHEST3: if (setlevel) { chest._oVar1 = 3; break; } chest._oVar1 = GenerateRnd(4); break; default: break; } chest._oVar2 = GenerateRnd(8); } void ObjSetMicro(Point position, int pn) { dPiece[position.x][position.y] = pn; } void DoorSet(Point position, bool isLeftDoor) { const int pn = dPiece[position.x][position.y]; switch (pn) { case 42: ObjSetMicro(position, 391); break; case 44: ObjSetMicro(position, 393); break; case 49: ObjSetMicro(position, isLeftDoor ? 410 : 411); break; case 53: ObjSetMicro(position, 396); break; case 54: ObjSetMicro(position, 397); break; case 60: ObjSetMicro(position, 398); break; case 66: ObjSetMicro(position, 399); break; case 67: ObjSetMicro(position, 400); break; case 68: ObjSetMicro(position, 402); break; case 69: ObjSetMicro(position, 403); break; case 71: ObjSetMicro(position, 405); break; case 211: ObjSetMicro(position, 406); break; case 353: ObjSetMicro(position, 408); break; case 354: ObjSetMicro(position, 409); break; case 410: case 411: ObjSetMicro(position, 395); break; } } void CryptDoorSet(Point position, bool isLeftDoor) { const int pn = dPiece[position.x][position.y]; switch (pn) { case 74: ObjSetMicro(position, 203); break; case 78: ObjSetMicro(position, 207); break; case 85: ObjSetMicro(position, isLeftDoor ? 231 : 233); break; case 90: ObjSetMicro(position, 214); break; case 92: ObjSetMicro(position, 217); break; case 98: ObjSetMicro(position, 219); break; case 110: ObjSetMicro(position, 221); break; case 112: ObjSetMicro(position, 223); break; case 114: ObjSetMicro(position, 225); break; case 116: ObjSetMicro(position, 227); break; case 118: ObjSetMicro(position, 229); break; case 231: case 233: ObjSetMicro(position, 211); break; } } void SetDoorStateOpen(Object &door) { door._oVar4 = DOOR_OPEN; door._oPreFlag = true; door._oMissFlag = true; door.selectionRegion = SelectionRegion::Middle; switch (door._otype) { case OBJ_L1LDOOR: // 214: blood splater // 407: blood pool // 392: open door (no frame) ObjSetMicro(door.position, door._oVar1 == 214 ? 407 : 392); dSpecial[door.position.x][door.position.y] = 7; DoorSet(door.position + Direction::NorthEast, true); break; case OBJ_L1RDOOR: ObjSetMicro(door.position, 394); dSpecial[door.position.x][door.position.y] = 8; DoorSet(door.position + Direction::NorthWest, false); break; case OBJ_L2LDOOR: ObjSetMicro(door.position, 12); dSpecial[door.position.x][door.position.y] = 5; break; case OBJ_L2RDOOR: ObjSetMicro(door.position, 16); dSpecial[door.position.x][door.position.y] = 6; break; case OBJ_L3LDOOR: ObjSetMicro(door.position, 537); break; case OBJ_L3RDOOR: ObjSetMicro(door.position, 540); break; case OBJ_L5LDOOR: ObjSetMicro(door.position, 205); CryptDoorSet(door.position + Direction::NorthEast, true); break; case OBJ_L5RDOOR: ObjSetMicro(door.position, 208); CryptDoorSet(door.position + Direction::NorthWest, false); break; default: break; } } void SetDoorStateClosed(Object &door) { door._oVar4 = DOOR_CLOSED; door._oPreFlag = false; door._oMissFlag = false; door.selectionRegion = SelectionRegion::Bottom | SelectionRegion::Middle; switch (door._otype) { case OBJ_L1LDOOR: { // Clear overlapping arches dSpecial[door.position.x][door.position.y] = 0; ObjSetMicro(door.position, door._oVar1 - 1); // Restore the normal tile where the open door used to be auto openPosition = door.position + Direction::NorthEast; if (door._oVar2 == 50 && dPiece[openPosition.x][openPosition.y] == 395) ObjSetMicro(openPosition, 411); else ObjSetMicro(openPosition, door._oVar2 - 1); break; } break; case OBJ_L1RDOOR: { // Clear overlapping arches dSpecial[door.position.x][door.position.y] = 0; ObjSetMicro(door.position, door._oVar1 - 1); // Restore the normal tile where the open door used to be auto openPosition = door.position + Direction::NorthWest; if (door._oVar2 == 50 && dPiece[openPosition.x][openPosition.y] == 395) ObjSetMicro(openPosition, 410); else ObjSetMicro(openPosition, door._oVar2 - 1); break; } break; case OBJ_L2LDOOR: // Clear overlapping arches dSpecial[door.position.x][door.position.y] = 0; ObjSetMicro(door.position, 537); break; case OBJ_L2RDOOR: // Clear overlapping arches dSpecial[door.position.x][door.position.y] = 0; ObjSetMicro(door.position, 539); break; case OBJ_L3LDOOR: ObjSetMicro(door.position, 530); break; case OBJ_L3RDOOR: ObjSetMicro(door.position, 533); break; case OBJ_L5LDOOR: { ObjSetMicro(door.position, door._oVar1 - 1); // Restore the normal tile where the open door used to be auto openPosition = door.position + Direction::NorthEast; if (door._oVar2 == 86 && dPiece[openPosition.x][openPosition.y] == 209) ObjSetMicro(openPosition, 233); else ObjSetMicro(openPosition, door._oVar2 - 1); } break; case OBJ_L5RDOOR: { ObjSetMicro(door.position, door._oVar1 - 1); // Restore the normal tile where the open door used to be auto openPosition = door.position + Direction::NorthWest; if (door._oVar2 == 86 && dPiece[openPosition.x][openPosition.y] == 209) ObjSetMicro(openPosition, 231); else ObjSetMicro(openPosition, door._oVar2 - 1); } break; default: break; } } void AddDoor(Object &door) { door._oDoorFlag = true; switch (door._otype) { case OBJ_L1LDOOR: case OBJ_L5LDOOR: door._oVar1 = dPiece[door.position.x][door.position.y] + 1; door._oVar2 = dPiece[door.position.x][door.position.y - 1] + 1; break; case OBJ_L1RDOOR: case OBJ_L5RDOOR: door._oVar1 = dPiece[door.position.x][door.position.y] + 1; door._oVar2 = dPiece[door.position.x - 1][door.position.y] + 1; break; default: break; } SetDoorStateClosed(door); } void AddSarcophagus(Object &sarcophagus) { dObject[sarcophagus.position.x][sarcophagus.position.y - 1] = -(static_cast(sarcophagus.GetId()) + 1); sarcophagus._oVar1 = GenerateRnd(10); sarcophagus._oRndSeed = AdvanceRndSeed(); if (sarcophagus._oVar1 >= 8) { Monster *monster = PreSpawnSkeleton(); if (monster != nullptr) { sarcophagus._oVar2 = static_cast(monster->getId()); } else { sarcophagus._oVar2 = -1; } } } void AddFlameTrap(Object &flameTrap) { flameTrap._oVar1 = trapid; flameTrap._oVar2 = 0; flameTrap._oVar3 = trapdir; flameTrap._oVar4 = 0; } void AddFlameLever(Object &flameLever) { flameLever._oVar1 = trapid; flameLever._oVar2 = static_cast(MissileID::InfernoControl); } void AddTrap(Object &trap) { int effectiveLevel = currlevel; if (leveltype == DTYPE_NEST) effectiveLevel -= 4; else if (leveltype == DTYPE_CRYPT) effectiveLevel -= 8; const int missileType = GenerateRnd(effectiveLevel / 3 + 1); if (missileType == 0) trap._oVar3 = static_cast(MissileID::Arrow); if (missileType == 1) trap._oVar3 = static_cast(MissileID::Firebolt); if (missileType == 2) trap._oVar3 = static_cast(MissileID::LightningControl); trap._oVar4 = 0; } void AddObjectLight(Object &object) { int radius; switch (object._otype) { case OBJ_STORYCANDLE: case OBJ_L5CANDLE: radius = 3; break; case OBJ_L1LIGHT: case OBJ_SKFIRE: case OBJ_CANDLE1: case OBJ_CANDLE2: case OBJ_BOOKCANDLE: case OBJ_BCROSS: case OBJ_TBCROSS: radius = 5; break; case OBJ_TORCHL: case OBJ_TORCHR: case OBJ_TORCHL2: case OBJ_TORCHR2: radius = 8; break; default: return; } DoLighting(object.position, radius, {}); if (LoadingMapObjects) { DoUnLight(object.position, radius); UpdateLighting = true; } object._oVar1 = -1; } void AddBarrel(Object &barrel) { barrel._oVar1 = 0; barrel._oRndSeed = AdvanceRndSeed(); barrel._oVar2 = barrel.isExplosive() ? 0 : GenerateRnd(10); barrel._oVar3 = GenerateRnd(3); if (barrel._oVar2 >= 8) { Monster *skeleton = PreSpawnSkeleton(); if (skeleton != nullptr) { barrel._oVar4 = static_cast(skeleton->getId()); } else { barrel._oVar4 = -1; } } } void AddShrine(Object &shrine) { shrine._oRndSeed = AdvanceRndSeed(); shrine._oPreFlag = true; const int shrineCount = gbIsHellfire ? NumberOfShrineTypes : 26; bool slist[NumberOfShrineTypes] = {}; for (int i = 0; i < shrineCount; i++) { bool isShrineAvailable = true; if (gbIsMultiplayer) { isShrineAvailable = (shrineavail[i] != ShrineTypeSingle); } else { isShrineAvailable = (shrineavail[i] != ShrineTypeMulti); } const bool isEnchantedShrine = (i == ShrineEnchanted); const bool isCorrectLevelType = IsAnyOf(leveltype, DTYPE_CATHEDRAL, DTYPE_CATACOMBS); slist[i] = isShrineAvailable && (!isEnchantedShrine || isCorrectLevelType); } int selectedIndex; do { selectedIndex = GenerateRnd(shrineCount); } while (!slist[selectedIndex]); shrine._oVar1 = selectedIndex; if (!FlipCoin()) { shrine._oAnimFrame = 12; shrine._oAnimLen = 22; } } void AddBookcase(Object &bookcase) { bookcase._oRndSeed = AdvanceRndSeed(); bookcase._oPreFlag = true; } void AddLargeFountain(Object &fountain) { const int ox = fountain.position.x; const int oy = fountain.position.y; const uint8_t id = -(static_cast(fountain.GetId()) + 1); dObject[ox][oy - 1] = id; dObject[ox - 1][oy] = id; dObject[ox - 1][oy - 1] = id; fountain._oRndSeed = AdvanceRndSeed(); } void AddArmorStand(Object &armorStand) { if (!armorFlag) { armorStand._oAnimFlag = true; armorStand.selectionRegion = SelectionRegion::None; } armorStand._oRndSeed = AdvanceRndSeed(); } void AddDecapitatedBody(Object &decapitatedBody) { decapitatedBody._oRndSeed = AdvanceRndSeed(); decapitatedBody._oAnimFrame = GenerateRnd(8) + 1; decapitatedBody._oPreFlag = true; } void AddBookOfVileness(Object &bookOfVileness) { if (setlevel && setlvlnum == SL_VILEBETRAYER) { bookOfVileness._oAnimFrame = 4; } } void AddMagicCircle(Object &magicCircle) { magicCircle._oRndSeed = AdvanceRndSeed(); magicCircle._oPreFlag = true; magicCircle._oVar6 = 0; magicCircle._oVar5 = 1; } void AddPedestalOfBlood(Object &pedestalOfBlood) { pedestalOfBlood._oVar1 = SetPiece.position.x; pedestalOfBlood._oVar2 = SetPiece.position.y; pedestalOfBlood._oVar3 = SetPiece.position.x + SetPiece.size.width; pedestalOfBlood._oVar4 = SetPiece.position.y + SetPiece.size.height; pedestalOfBlood._oVar6 = 0; } void AddStoryBook(Object &storyBook) { storyBook._oVar1 = (DungeonSeeds[16] >> 16) % 3; if (currlevel == 4) storyBook._oVar2 = StoryText[storyBook._oVar1][0]; else if (currlevel == 8) storyBook._oVar2 = StoryText[storyBook._oVar1][1]; else if (currlevel == 12) storyBook._oVar2 = StoryText[storyBook._oVar1][2]; storyBook._oVar3 = (currlevel / 4) + 3 * storyBook._oVar1 - 1; storyBook._oAnimFrame = 5 - 2 * storyBook._oVar1; storyBook._oVar4 = storyBook._oAnimFrame + 1; } void AddWeaponRack(Object &weaponRack) { if (!weaponFlag) { weaponRack._oAnimFlag = true; weaponRack.selectionRegion = SelectionRegion::None; } weaponRack._oRndSeed = AdvanceRndSeed(); } void AddTorturedBody(Object &torturedBody) { torturedBody._oRndSeed = AdvanceRndSeed(); torturedBody._oAnimFrame = GenerateRnd(4) + 1; torturedBody._oPreFlag = true; } Point GetRndObjLoc(int randarea) { if (randarea == 0) return { 0, 0 }; int tries = 0; int x; int y; while (true) { tries++; if (tries > 1000 && randarea > 1) randarea--; x = GenerateRnd(MAXDUNX); y = GenerateRnd(MAXDUNY); if (IsAreaOk(Rectangle { { x, y }, { randarea, randarea } })) break; } return { x, y }; } void AddMushPatch() { if (ActiveObjectCount < MAXOBJECTS) { const int i = AvailableObjects[0]; const Point loc = GetRndObjLoc(5); dObject[loc.x + 1][loc.y + 1] = -(i + 1); dObject[loc.x + 2][loc.y + 1] = -(i + 1); dObject[loc.x + 1][loc.y + 2] = -(i + 1); AddObject(OBJ_MUSHPATCH, { loc.x + 2, loc.y + 2 }); } } bool IsLightVisible(Object &light, int lightRadius) { #ifdef _DEBUG if (DisableLighting) return false; #endif for (const Player &player : Players) { if (!player.plractive) continue; if (!player.isOnActiveLevel()) continue; if (player.position.tile.WalkingDistance(light.position) < lightRadius + 10) { return true; } } return false; } void UpdateObjectLight(Object &light, int lightRadius) { if (light._oVar1 == -1) { return; } if (IsLightVisible(light, lightRadius)) { if (light._oVar1 == 0) light._olid = AddLight(light.position, lightRadius); light._oVar1 = 1; } else { if (light._oVar1 == 1) AddUnLight(light._olid); light._oVar1 = 0; } } void UpdateCircle(Object &circle) { Player *playerOnCircle = PlayerAtPosition(circle.position); if (!playerOnCircle) { if (circle._otype == OBJ_MCIRCLE1) circle._oAnimFrame = 1; if (circle._otype == OBJ_MCIRCLE2) circle._oAnimFrame = 3; circle._oVar6 = 0; return; } if (circle._otype == OBJ_MCIRCLE1) circle._oAnimFrame = 2; if (circle._otype == OBJ_MCIRCLE2) circle._oAnimFrame = 4; if (circle.position == Point { 45, 47 }) { circle._oVar6 = 2; } else if (circle.position == Point { 26, 46 }) { circle._oVar6 = 1; } else { circle._oVar6 = 0; } if (circle.position == Point { 35, 36 } && circle._oVar5 == 3) { circle._oVar6 = 4; if (Quests[Q_BETRAYER]._qvar1 <= 4) { LoadingMapObjects = true; ObjChangeMap(circle._oVar1, circle._oVar2, circle._oVar3, circle._oVar4); LoadingMapObjects = false; Quests[Q_BETRAYER]._qvar1 = 4; NetSendCmdQuest(true, Quests[Q_BETRAYER]); } AddMissile(playerOnCircle->position.tile, { 35, 46 }, Direction::South, MissileID::Phasing, TARGET_BOTH, *playerOnCircle, 0, 0); if (playerOnCircle == MyPlayer) { LastPlayerAction = PlayerActionType::None; sgbMouseDown = CLICK_NONE; } ClrPlrPath(*playerOnCircle); StartStand(*playerOnCircle, Direction::South); } } void ObjectStopAnim(Object &object) { if (object._oAnimFrame == object._oAnimLen) { object._oAnimCnt = 0; object._oAnimDelay = 1000; } } /** * @brief Checks if an open door can be closed * * In order to be able to close a door the space where the closed door would be must be free of bodies, monsters, players, and items * * @param doorPosition Map tile where the door is in its closed position * @return true if the door is free to be closed, false if anything is blocking it */ inline bool IsDoorClear(const Object &door) { return dCorpse[door.position.x][door.position.y] == 0 && dMonster[door.position.x][door.position.y] == 0 && dItem[door.position.x][door.position.y] == 0 && dPlayer[door.position.x][door.position.y] == 0; } void UpdateDoor(Object &door) { if (door._oVar4 == DOOR_CLOSED) { return; } door._oVar4 = IsDoorClear(door) ? DOOR_OPEN : DOOR_BLOCKED; } void UpdateSarcophagus(Object &sarcophagus) { if (sarcophagus._oAnimFrame == sarcophagus._oAnimLen) sarcophagus._oAnimFlag = false; } void ActivateTrapLine(int ttype, int tid) { for (int i = 0; i < ActiveObjectCount; i++) { Object &trap = Objects[ActiveObjects[i]]; if (trap._otype == ttype && trap._oVar1 == tid) { trap._oVar4 = 1; trap._oAnimFlag = true; trap._oAnimDelay = 1; trap._olid = AddLight(trap.position, 1); } } } void UpdateFlameTrap(Object &trap) { if (trap._oVar2 != 0) { if (trap._oVar4 != 0) { trap._oAnimFrame--; if (trap._oAnimFrame == 1) { trap._oVar4 = 0; AddUnLight(trap._olid); } else if (trap._oAnimFrame <= 4) { ChangeLightRadius(trap._olid, trap._oAnimFrame); } } } else if (trap._oVar4 == 0) { if (trap._oVar3 == 2) { int x = trap.position.x - 2; const int y = trap.position.y; for (int j = 0; j < 5; j++) { if (dPlayer[x][y] != 0 || dMonster[x][y] != 0) trap._oVar4 = 1; x++; } } else { const int x = trap.position.x; int y = trap.position.y - 2; for (int k = 0; k < 5; k++) { if (dPlayer[x][y] != 0 || dMonster[x][y] != 0) trap._oVar4 = 1; y++; } } if (trap._oVar4 != 0) ActivateTrapLine(trap._otype, trap._oVar1); } else { const int damage[6] = { 6, 8, 10, 12, 10, 12 }; const int mindam = damage[leveltype - 1]; const int maxdam = mindam * 2; const int x = trap.position.x; const int y = trap.position.y; constexpr MissileID TrapMissile = MissileID::FireWallControl; if (dMonster[x][y] > 0) MonsterTrapHit(dMonster[x][y] - 1, mindam / 2, maxdam / 2, 0, TrapMissile, GetMissileData(TrapMissile).damageType(), false); Player *player = PlayerAtPosition({ x, y }, true); if (player != nullptr) { bool unused; PlayerMHit(*player, nullptr, 0, mindam, maxdam, TrapMissile, GetMissileData(TrapMissile).damageType(), false, DeathReason::MonsterOrTrap, &unused); } if (trap._oAnimFrame == trap._oAnimLen) trap._oAnimFrame = 11; if (trap._oAnimFrame <= 5) ChangeLightRadius(trap._olid, trap._oAnimFrame); } } void UpdateBurningCrossDamage(Object &cross) { int damage[6] = { 6, 8, 10, 12, 10, 12 }; Player &myPlayer = *MyPlayer; if (myPlayer._pmode == PM_DEATH) return; const int8_t fireResist = myPlayer._pFireResist; if (fireResist > 0) damage[leveltype - 1] -= fireResist * damage[leveltype - 1] / 100; if (myPlayer.position.tile != cross.position + Displacement { 0, -1 }) return; ApplyPlrDamage(DamageType::Fire, myPlayer, 0, 0, damage[leveltype - 1]); if (myPlayer._pHitPoints >> 6 > 0) { myPlayer.Say(HeroSpeech::Argh); } } void ObjSetMini(Point position, int v) { const MegaTile mega = pMegaTiles[v - 1]; const Point megaOrigin = position.megaToWorld(); ObjSetMicro(megaOrigin, SDL_SwapLE16(mega.micro1)); ObjSetMicro(megaOrigin + Direction::SouthEast, SDL_SwapLE16(mega.micro2)); ObjSetMicro(megaOrigin + Direction::SouthWest, SDL_SwapLE16(mega.micro3)); ObjSetMicro(megaOrigin + Direction::South, SDL_SwapLE16(mega.micro4)); } void ObjL1Special(int x1, int y1, int x2, int y2) { for (int i = y1; i <= y2; ++i) { for (int j = x1; j <= x2; ++j) { dSpecial[j][i] = 0; if (dPiece[j][i] == 11) dSpecial[j][i] = 1; if (dPiece[j][i] == 10) dSpecial[j][i] = 2; if (dPiece[j][i] == 70) dSpecial[j][i] = 1; if (dPiece[j][i] == 252) dSpecial[j][i] = 3; if (dPiece[j][i] == 266) dSpecial[j][i] = 6; if (dPiece[j][i] == 258) dSpecial[j][i] = 5; if (dPiece[j][i] == 248) dSpecial[j][i] = 2; if (dPiece[j][i] == 324) dSpecial[j][i] = 2; if (dPiece[j][i] == 320) dSpecial[j][i] = 1; if (dPiece[j][i] == 254) dSpecial[j][i] = 4; if (dPiece[j][i] == 210) dSpecial[j][i] = 1; if (dPiece[j][i] == 343) dSpecial[j][i] = 2; if (dPiece[j][i] == 340) dSpecial[j][i] = 1; if (dPiece[j][i] == 330) dSpecial[j][i] = 2; if (dPiece[j][i] == 417) dSpecial[j][i] = 1; if (dPiece[j][i] == 420) dSpecial[j][i] = 2; } } } void ObjL2Special(int x1, int y1, int x2, int y2) { for (int j = y1; j <= y2; j++) { for (int i = x1; i <= x2; i++) { dSpecial[i][j] = 0; if (dPiece[i][j] == 540) dSpecial[i][j] = 5; if (dPiece[i][j] == 177) dSpecial[i][j] = 5; if (dPiece[i][j] == 550) dSpecial[i][j] = 5; if (dPiece[i][j] == 541) dSpecial[i][j] = 6; if (dPiece[i][j] == 552) dSpecial[i][j] = 6; } } for (int j = y1; j <= y2; j++) { for (int i = x1; i <= x2; i++) { if (dPiece[i][j] == 131) { dSpecial[i][j + 1] = 2; dSpecial[i][j + 2] = 1; } if (dPiece[i][j] == 134 || dPiece[i][j] == 138) { dSpecial[i + 1][j] = 3; dSpecial[i + 2][j] = 4; } } } } void OpenDoor(Object &door) { door._oAnimFrame += 2; SetDoorStateOpen(door); } void CloseDoor(Object &door) { door._oAnimFrame -= 2; SetDoorStateClosed(door); } void OperateDoor(Object &door, bool sendflag) { const bool isCrypt = IsAnyOf(door._otype, OBJ_L5LDOOR, OBJ_L5RDOOR); const bool openDoor = door._oVar4 == DOOR_CLOSED; if (!openDoor && !IsDoorClear(door)) { PlaySfxLoc(isCrypt ? SfxID::CryptDoorClose : SfxID::DoorClose, door.position); door._oVar4 = DOOR_BLOCKED; return; } if (openDoor) { PlaySfxLoc(isCrypt ? SfxID::CryptDoorOpen : SfxID::DoorOpen, door.position); OpenDoor(door); } else { PlaySfxLoc(isCrypt ? SfxID::CryptDoorClose : SfxID::DoorClose, door.position); CloseDoor(door); } RedoPlayerVision(); if (sendflag) NetSendCmdLoc(MyPlayerId, true, openDoor ? CMD_OPENDOOR : CMD_CLOSEDOOR, door.position); } bool AreAllLeversActivated(int leverId) { for (int j = 0; j < ActiveObjectCount; j++) { const Object &lever = Objects[ActiveObjects[j]]; if (lever._otype == OBJ_SWITCHSKL && lever._oVar8 == leverId && lever.canInteractWith()) { return false; } } return true; } void UpdateLeverState(Object &object) { if (!object.canInteractWith()) { return; } object.selectionRegion = SelectionRegion::None; object._oAnimFrame++; if (currlevel == 16 && !AreAllLeversActivated(object._oVar8)) return; if (currlevel == 24) { SyncNakrulRoom(); IsUberLeverActivated = true; return; } if (setlevel && setlvlnum == SL_VILEBETRAYER) ObjectAtPosition({ 35, 36 })._oVar5++; ObjChangeMap(object._oVar1, object._oVar2, object._oVar3, object._oVar4); } void OperateLever(Object &object, bool sendmsg) { if (!object.canInteractWith()) { return; } PlaySfxLoc(SfxID::OperateLever, object.position); UpdateLeverState(object); if (currlevel == 24) { PlaySfxLoc(SfxID::CryptDoorOpen, { UberRow, UberCol }); Quests[Q_NAKRUL]._qactive = QUEST_DONE; NetSendCmdQuest(true, Quests[Q_NAKRUL]); } if (sendmsg) NetSendCmdLoc(MyPlayerId, false, CMD_OPERATEOBJ, object.position); } void OperateBook(Player &player, Object &book, bool sendmsg) { if (!book.canInteractWith()) { return; } if (setlevel && setlvlnum == SL_VILEBETRAYER) { Point target {}; if (book.position == Point { 26, 45 }) { target = { 27, 29 }; } else if (book.position == Point { 45, 46 }) { target = { 43, 29 }; } else { return; } Object &circle = ObjectAtPosition(book.position + Direction::SouthWest); assert(circle._otype == OBJ_MCIRCLE2); // Only verify that the player stands on the circle when it's the local player (sendmsg), because for remote players the position could be desynced if (sendmsg && circle.position != player.position.tile) { return; } circle._oVar6 = 4; ObjectAtPosition({ 35, 36 })._oVar5++; AddMissile(player.position.tile, target, Direction::South, MissileID::Phasing, TARGET_BOTH, player, 0, 0); } book.selectionRegion = SelectionRegion::None; book._oAnimFrame++; if (sendmsg) NetSendCmdLoc(MyPlayerId, false, CMD_OPERATEOBJ, book.position); if (!setlevel) { return; } if (setlvlnum == SL_BONECHAMB) { if (sendmsg) { const uint8_t newSpellLevel = player._pSplLvl[static_cast(SpellID::Guardian)] + 1; if (newSpellLevel <= MaxSpellLevel) { player._pSplLvl[static_cast(SpellID::Guardian)] = newSpellLevel; NetSendCmdParam2(true, CMD_CHANGE_SPELL_LEVEL, static_cast(SpellID::Guardian), newSpellLevel); } if (&player == MyPlayer) { for (Item &item : InventoryPlayerItemsRange { player }) { item.updateRequiredStatsCacheForPlayer(player); } if (IsStashOpen) { Stash.RefreshItemStatFlags(); } } Quests[Q_SCHAMB]._qactive = QUEST_DONE; NetSendCmdQuest(true, Quests[Q_SCHAMB]); } PlaySfxLoc(SfxID::QuestDone, book.position); InitDiabloMsg(EMSG_BONECHAMB); AddMissile( player.position.tile, book.position + Displacement { -2, -4 }, player._pdir, MissileID::Guardian, TARGET_MONSTERS, player, 0, 0); } if (setlvlnum == SL_VILEBETRAYER) { ObjChangeMap( book._oVar1, book._oVar2, book._oVar3, book._oVar4); for (int j = 0; j < ActiveObjectCount; j++) SyncObjectAnim(Objects[ActiveObjects[j]]); } } void OperateBookLever(Object &questBook, bool sendmsg) { if (ActiveItemCount >= MAXITEMS) { return; } if (questBook.canInteractWith() && !qtextflag) { if (questBook._otype == OBJ_BLINDBOOK && Quests[Q_BLIND]._qvar1 == 0) { Quests[Q_BLIND]._qactive = QUEST_ACTIVE; Quests[Q_BLIND]._qlog = true; Quests[Q_BLIND]._qvar1 = 1; NetSendCmdQuest(true, Quests[Q_BLIND]); } if (questBook._otype == OBJ_BLOODBOOK && Quests[Q_BLOOD]._qvar1 == 0) { Quests[Q_BLOOD]._qactive = QUEST_ACTIVE; Quests[Q_BLOOD]._qlog = true; Quests[Q_BLOOD]._qvar1 = 1; NetSendCmdQuest(true, Quests[Q_BLOOD]); if (sendmsg) SpawnQuestItem(IDI_BLDSTONE, SetPiece.position.megaToWorld() + Displacement { 9, 17 }, 0, SelectionRegion::Bottom, true); } if (questBook._otype == OBJ_STEELTOME && Quests[Q_WARLORD]._qvar1 == QS_WARLORD_INIT) { Quests[Q_WARLORD]._qactive = QUEST_ACTIVE; Quests[Q_WARLORD]._qlog = true; Quests[Q_WARLORD]._qvar1 = QS_WARLORD_STEELTOME_READ; NetSendCmdQuest(true, Quests[Q_WARLORD]); } if (questBook._oAnimFrame != questBook._oVar6) { if (questBook._otype != OBJ_BLOODBOOK) ObjChangeMap(questBook._oVar1, questBook._oVar2, questBook._oVar3, questBook._oVar4); if (questBook._otype == OBJ_BLINDBOOK) { if (sendmsg) SpawnUnique(UITEM_OPTAMULET, SetPiece.position.megaToWorld() + Displacement { 5, 5 }, std::nullopt, true, true); auto tren = TransVal; TransVal = 9; DRLG_MRectTrans(WorldTilePosition(questBook._oVar1, questBook._oVar2), WorldTilePosition(questBook._oVar3, questBook._oVar4)); TransVal = tren; } } questBook._oAnimFrame = questBook._oVar6; InitQTextMsg(questBook.bookMessage); if (sendmsg) NetSendCmdLoc(MyPlayerId, false, CMD_OPERATEOBJ, questBook.position); } } void OperateChamberOfBoneBook(Object &questBook, bool sendmsg) { if (!questBook.canInteractWith() || qtextflag) { return; } if (questBook._oAnimFrame != questBook._oVar6) { ObjChangeMapResync(questBook._oVar1, questBook._oVar2, questBook._oVar3, questBook._oVar4); for (int j = 0; j < ActiveObjectCount; j++) { SyncObjectAnim(Objects[ActiveObjects[j]]); } } questBook._oAnimFrame = questBook._oVar6; if (Quests[Q_SCHAMB]._qactive == QUEST_INIT) { Quests[Q_SCHAMB]._qactive = QUEST_ACTIVE; Quests[Q_SCHAMB]._qlog = true; } _speech_id textdef; switch (MyPlayer->_pClass) { case HeroClass::Warrior: textdef = TEXT_BONER; break; case HeroClass::Rogue: textdef = TEXT_RBONER; break; case HeroClass::Sorcerer: textdef = TEXT_MBONER; break; case HeroClass::Monk: textdef = TEXT_HBONER; break; case HeroClass::Bard: textdef = TEXT_RBONER; break; case HeroClass::Barbarian: textdef = TEXT_BONER; break; } if (sendmsg) { Quests[Q_SCHAMB]._qmsg = textdef; NetSendCmdQuest(true, Quests[Q_SCHAMB]); NetSendCmdLoc(MyPlayerId, false, CMD_OPERATEOBJ, questBook.position); InitQTextMsg(textdef); } } void OperateChest(const Player &player, Object &chest, bool sendLootMsg) { if (!chest.canInteractWith()) { return; } PlaySfxLoc(SfxID::ChestOpen, chest.position); chest.selectionRegion = SelectionRegion::None; chest._oAnimFrame += 2; SetRndSeed(chest._oRndSeed); if (setlevel) { for (int j = 0; j < chest._oVar1; j++) { CreateRndItem(chest.position, true, sendLootMsg, false); } } else { for (int j = 0; j < chest._oVar1; j++) { if (chest._oVar2 != 0) CreateRndItem(chest.position, false, sendLootMsg, false); else CreateRndUseful(chest.position, sendLootMsg); } } if (chest.IsTrappedChest()) { const Direction mdir = GetDirection(chest.position, player.position.tile); MissileID mtype; switch (chest._oVar4) { case 0: mtype = MissileID::Arrow; break; case 1: mtype = MissileID::FireArrow; break; case 2: mtype = MissileID::Nova; break; case 3: mtype = MissileID::RingOfFire; break; case 4: mtype = MissileID::StealPotions; break; case 5: mtype = MissileID::StealMana; break; default: mtype = MissileID::Arrow; } AddMissile(chest.position, player.position.tile, mdir, mtype, TARGET_PLAYERS, -1, 0, 0); PlaySfxLoc(SfxID::TriggerTrap, chest.position); chest._oTrapFlag = false; } if (&player == MyPlayer) NetSendCmdLoc(MyPlayerId, false, CMD_OPERATEOBJ, chest.position); } void OperateMushroomPatch(const Player &player, Object &mushroomPatch) { if (ActiveItemCount >= MAXITEMS) { return; } if (Quests[Q_MUSHROOM]._qactive != QUEST_ACTIVE) { if (&player == MyPlayer) { player.Say(HeroSpeech::ICantUseThisYet); } return; } if (!mushroomPatch.canInteractWith()) { return; } mushroomPatch.selectionRegion = SelectionRegion::None; mushroomPatch._oAnimFrame++; PlaySfxLoc(SfxID::ChestOpen, mushroomPatch.position); const Point pos = GetSuperItemLoc(mushroomPatch.position); if (&player == MyPlayer) { SpawnQuestItem(IDI_MUSHROOM, pos, 0, SelectionRegion::None, true); Quests[Q_MUSHROOM]._qvar1 = QS_MUSHSPAWNED; NetSendCmdQuest(true, Quests[Q_MUSHROOM]); NetSendCmdLoc(MyPlayerId, false, CMD_OPERATEOBJ, mushroomPatch.position); } } void OperateInnSignChest(const Player &player, Object &questContainer, bool sendmsg) { if (ActiveItemCount >= MAXITEMS) { return; } if (Quests[Q_LTBANNER]._qvar1 != 2) { if (&player == MyPlayer) { player.Say(HeroSpeech::ICantOpenThisYet); } return; } if (!questContainer.canInteractWith()) { return; } questContainer.selectionRegion = SelectionRegion::None; questContainer._oAnimFrame += 2; PlaySfxLoc(SfxID::ChestOpen, questContainer.position); if (sendmsg) { const Point pos = GetSuperItemLoc(questContainer.position); SpawnQuestItem(IDI_BANNER, pos, 0, SelectionRegion::None, true); NetSendCmdLoc(MyPlayerId, true, CMD_OPERATEOBJ, questContainer.position); } } void OperateSlainHero(const Player &player, Object &corpse, bool sendmsg) { if (!corpse.canInteractWith()) { return; } corpse.selectionRegion = SelectionRegion::None; SetRndSeed(corpse._oRndSeed); if (player._pClass == HeroClass::Warrior) { CreateMagicArmor(corpse.position, ItemType::HeavyArmor, ICURS_BREAST_PLATE, sendmsg, false); } else if (player._pClass == HeroClass::Rogue) { CreateMagicWeapon(corpse.position, ItemType::Bow, ICURS_LONG_BATTLE_BOW, sendmsg, false); } else if (player._pClass == HeroClass::Sorcerer) { CreateSpellBook(corpse.position, SpellID::Lightning, sendmsg, false); } else if (player._pClass == HeroClass::Monk) { CreateMagicWeapon(corpse.position, ItemType::Staff, ICURS_WAR_STAFF, sendmsg, false); } else if (player._pClass == HeroClass::Bard) { CreateMagicWeapon(corpse.position, ItemType::Sword, ICURS_BASTARD_SWORD, sendmsg, false); } else if (player._pClass == HeroClass::Barbarian) { CreateMagicWeapon(corpse.position, ItemType::Axe, ICURS_BATTLE_AXE, sendmsg, false); } MyPlayer->Say(HeroSpeech::RestInPeaceMyFriend); if (sendmsg) NetSendCmdLoc(MyPlayerId, false, CMD_OPERATEOBJ, corpse.position); } void OperateTrapLever(Object &flameLever) { PlaySfxLoc(SfxID::OperateLever, flameLever.position); if (flameLever._oAnimFrame == 1) { flameLever._oAnimFrame = 2; for (int j = 0; j < ActiveObjectCount; j++) { Object &target = Objects[ActiveObjects[j]]; if (target._otype == flameLever._oVar2 && target._oVar1 == flameLever._oVar1) { target._oVar2 = 1; target._oAnimFlag = false; } } return; } flameLever._oAnimFrame--; for (int j = 0; j < ActiveObjectCount; j++) { Object &target = Objects[ActiveObjects[j]]; if (target._otype == flameLever._oVar2 && target._oVar1 == flameLever._oVar1) { target._oVar2 = 0; if (target._oVar4 != 0) { target._oAnimFlag = true; } } } } void OperateSarcophagus(Object &sarcophagus, bool sendMsg, bool sendLootMsg) { if (!sarcophagus.canInteractWith()) { return; } PlaySfxLoc(SfxID::Sarcophagus, sarcophagus.position); sarcophagus.selectionRegion = SelectionRegion::None; sarcophagus._oAnimFlag = true; sarcophagus._oAnimDelay = 3; SetRndSeed(sarcophagus._oRndSeed); if (sarcophagus._oVar1 <= 2) CreateRndItem(sarcophagus.position, false, sendLootMsg, false); if (sarcophagus._oVar1 >= 8 && sarcophagus._oVar2 >= 0) ActivateSkeleton(Monsters[sarcophagus._oVar2], sarcophagus.position); if (sendMsg) NetSendCmdLoc(MyPlayerId, false, CMD_OPERATEOBJ, sarcophagus.position); } void OperatePedestal(Player &player, Object &pedestal, bool sendmsg) { if (ActiveItemCount >= MAXITEMS) { return; } if (pedestal._oVar6 == 3 || (sendmsg && !RemoveInventoryItemById(player, IDI_BLDSTONE))) { return; } if (sendmsg) { NetSendCmdLoc(MyPlayerId, false, CMD_OPERATEOBJ, pedestal.position); if (gbIsMultiplayer) { // Store added stones to pedestal in qvar2, because we get only one CMD_OPERATEOBJ from DeltaLoadLevel even if we add multiple stones Quests[Q_BLOOD]._qvar2++; NetSendCmdQuest(true, Quests[Q_BLOOD]); } } pedestal._oAnimFrame++; pedestal._oVar6++; if (pedestal._oVar6 == 1) { PlaySfxLoc(SfxID::SpellPuddle, pedestal.position); ObjChangeMap(SetPiece.position.x, SetPiece.position.y + 3, SetPiece.position.x + 2, SetPiece.position.y + 7); if (sendmsg) SpawnQuestItem(IDI_BLDSTONE, SetPiece.position.megaToWorld() + Displacement { 3, 10 }, 0, SelectionRegion::Bottom, true); } if (pedestal._oVar6 == 2) { PlaySfxLoc(SfxID::SpellPuddle, pedestal.position); ObjChangeMap(SetPiece.position.x + 6, SetPiece.position.y + 3, SetPiece.position.x + SetPiece.size.width, SetPiece.position.y + 7); if (sendmsg) SpawnQuestItem(IDI_BLDSTONE, SetPiece.position.megaToWorld() + Displacement { 15, 10 }, 0, SelectionRegion::Bottom, true); } if (pedestal._oVar6 == 3) { PlaySfxLoc(SfxID::SpellBloodStar, pedestal.position); ObjChangeMap(pedestal._oVar1, pedestal._oVar2, pedestal._oVar3, pedestal._oVar4); LoadMapObjects("levels\\l2data\\blood2.dun", SetPiece.position.megaToWorld()); if (sendmsg) SpawnUnique(UITEM_ARMOFVAL, SetPiece.position.megaToWorld() + Displacement { 9, 3 }, std::nullopt, true, true); pedestal.selectionRegion = SelectionRegion::None; } } void OperateShrineMysterious(DiabloGenerator &rng, Player &player) { if (&player != MyPlayer) return; ModifyPlrStr(player, -1); ModifyPlrMag(player, -1); ModifyPlrDex(player, -1); ModifyPlrVit(player, -1); switch (static_cast(rng.generateRnd(4))) { case CharacterAttribute::Strength: ModifyPlrStr(player, 6); break; case CharacterAttribute::Magic: ModifyPlrMag(player, 6); break; case CharacterAttribute::Dexterity: ModifyPlrDex(player, 6); break; case CharacterAttribute::Vitality: ModifyPlrVit(player, 6); break; } CheckStats(player); CalcPlrInv(player, true); RedrawEverything(); InitDiabloMsg(EMSG_SHRINE_MYSTERIOUS); } void OperateShrineHidden(DiabloGenerator &rng, Player &player) { if (&player != MyPlayer) return; int cnt = 0; for (const auto &item : player.InvBody) { if (!item.isEmpty()) cnt++; } if (cnt > 0) { for (auto &item : player.InvBody) { if (!item.isEmpty() && item._iMaxDur != DUR_INDESTRUCTIBLE && item._iMaxDur != 0) { item._iDurability += 10; item._iMaxDur += 10; if (item._iDurability > item._iMaxDur) item._iDurability = item._iMaxDur; } } while (true) { cnt = 0; for (auto &item : player.InvBody) { if (!item.isEmpty() && item._iMaxDur != DUR_INDESTRUCTIBLE && item._iMaxDur != 0) { cnt++; } } if (cnt == 0) break; const int r = rng.generateRnd(NUM_INVLOC); if (player.InvBody[r].isEmpty() || player.InvBody[r]._iMaxDur == DUR_INDESTRUCTIBLE || player.InvBody[r]._iMaxDur == 0) continue; player.InvBody[r]._iDurability -= 20; player.InvBody[r]._iMaxDur -= 20; if (player.InvBody[r]._iDurability <= 0) player.InvBody[r]._iDurability = 1; if (player.InvBody[r]._iMaxDur <= 0) player.InvBody[r]._iMaxDur = 1; break; } } InitDiabloMsg(EMSG_SHRINE_HIDDEN); } void OperateShrineGloomy(Player &player) { if (&player != MyPlayer) return; // Increment armor class by 2 and decrements max damage by 1. for (Item &item : PlayerItemsRange(player)) { switch (item._itype) { case ItemType::Sword: case ItemType::Axe: case ItemType::Bow: case ItemType::Mace: case ItemType::Staff: item._iMaxDam--; if (item._iMaxDam < item._iMinDam) item._iMaxDam = item._iMinDam; break; case ItemType::Shield: case ItemType::Helm: case ItemType::LightArmor: case ItemType::MediumArmor: case ItemType::HeavyArmor: item._iAC += 2; break; default: break; } } CalcPlrInv(player, true); InitDiabloMsg(EMSG_SHRINE_GLOOMY); } void OperateShrineWeird(Player &player) { if (&player != MyPlayer) return; if (!player.InvBody[INVLOC_HAND_LEFT].isEmpty() && player.InvBody[INVLOC_HAND_LEFT]._itype != ItemType::Shield) player.InvBody[INVLOC_HAND_LEFT]._iMaxDam++; if (!player.InvBody[INVLOC_HAND_RIGHT].isEmpty() && player.InvBody[INVLOC_HAND_RIGHT]._itype != ItemType::Shield) player.InvBody[INVLOC_HAND_RIGHT]._iMaxDam++; for (Item &item : InventoryPlayerItemsRange { player }) { switch (item._itype) { case ItemType::Sword: case ItemType::Axe: case ItemType::Bow: case ItemType::Mace: case ItemType::Staff: item._iMaxDam++; break; default: break; } } CalcPlrInv(player, true); InitDiabloMsg(EMSG_SHRINE_WEIRD); } void OperateShrineMagical(const Player &player) { AddMissile( player.position.tile, player.position.tile, player._pdir, MissileID::ManaShield, TARGET_MONSTERS, player, 0, 2 * leveltype); if (&player != MyPlayer) return; InitDiabloMsg(EMSG_SHRINE_MAGICAL); } void OperateShrineStone(Player &player) { if (&player != MyPlayer) return; for (Item &item : PlayerItemsRange { player }) { if (item._itype == ItemType::Staff) item._iCharges = item._iMaxCharges; } CalcPlrInv(player, true); RedrawEverything(); InitDiabloMsg(EMSG_SHRINE_STONE); } void OperateShrineReligious(Player &player) { if (&player != MyPlayer) return; for (Item &item : PlayerItemsRange { player }) { item._iDurability = item._iMaxDur; } InitDiabloMsg(EMSG_SHRINE_RELIGIOUS); } void OperateShrineEnchanted(DiabloGenerator &rng, Player &player) { if (&player != MyPlayer) return; int cnt = 0; uint64_t spell = 1; const uint64_t spells = player._pMemSpells; for (uint16_t j = 0; j < SpellsData.size(); j++) { if ((spell & spells) != 0) cnt++; spell *= 2; } if (cnt > 1) { int spellToReduce; do { spellToReduce = rng.generateRnd(static_cast(SpellsData.size())) + 1; } while ((player._pMemSpells & GetSpellBitmask(static_cast(spellToReduce))) == 0); spell = 1; for (uint8_t j = static_cast(SpellID::Firebolt); j < SpellsData.size(); j++) { if ((player._pMemSpells & spell) != 0 && player._pSplLvl[j] < MaxSpellLevel && j != spellToReduce) { const uint8_t newSpellLevel = static_cast(player._pSplLvl[j] + 1); player._pSplLvl[j] = newSpellLevel; NetSendCmdParam2(true, CMD_CHANGE_SPELL_LEVEL, j, newSpellLevel); } spell *= 2; } if (player._pSplLvl[spellToReduce] > 0) { const uint8_t newSpellLevel = static_cast(player._pSplLvl[spellToReduce] - 1); player._pSplLvl[spellToReduce] = newSpellLevel; NetSendCmdParam2(true, CMD_CHANGE_SPELL_LEVEL, spellToReduce, newSpellLevel); } if (&player == MyPlayer) { for (Item &item : InventoryPlayerItemsRange { player }) { item.updateRequiredStatsCacheForPlayer(player); } if (IsStashOpen) { Stash.RefreshItemStatFlags(); } } } InitDiabloMsg(EMSG_SHRINE_ENCHANTED); } void OperateShrineThaumaturgic(DiabloGenerator &rng, const Player &player) { for (int j = 0; j < ActiveObjectCount; j++) { Object &object = Objects[ActiveObjects[j]]; if (object.IsChest() && !object.canInteractWith()) { object._oRndSeed = rng.advanceRndSeed(); object.selectionRegion = SelectionRegion::Bottom; object._oAnimFrame -= 2; } } if (&player != MyPlayer) return; InitDiabloMsg(EMSG_SHRINE_THAUMATURGIC); } void OperateShrineCostOfWisdom(Player &player, SpellID spellId, diablo_message message) { if (&player != MyPlayer) return; player._pMemSpells |= GetSpellBitmask(spellId); const uint8_t curSpellLevel = player._pSplLvl[static_cast(spellId)]; if (curSpellLevel < MaxSpellLevel) { const uint8_t newSpellLevel = std::min(static_cast(curSpellLevel + 2), MaxSpellLevel); player._pSplLvl[static_cast(spellId)] = newSpellLevel; NetSendCmdParam2(true, CMD_CHANGE_SPELL_LEVEL, static_cast(spellId), newSpellLevel); } if (&player == MyPlayer) { for (Item &item : InventoryPlayerItemsRange { player }) { item.updateRequiredStatsCacheForPlayer(player); } if (IsStashOpen) { Stash.RefreshItemStatFlags(); } } const uint32_t t = player._pMaxManaBase / 10; const int v1 = player._pMana - player._pManaBase; const int v2 = player._pMaxMana - player._pMaxManaBase; player._pManaBase -= t; player._pMana -= t; player._pMaxMana -= t; player._pMaxManaBase -= t; if (player._pMana >> 6 <= 0) { player._pMana = v1; player._pManaBase = 0; } if (player._pMaxMana >> 6 <= 0) { player._pMaxMana = v2; player._pMaxManaBase = 0; } RedrawEverything(); InitDiabloMsg(message); } void OperateShrineCryptic(Player &player) { AddMissile( player.position.tile, player.position.tile, player._pdir, MissileID::Nova, TARGET_MONSTERS, player, 0, 2 * leveltype); if (&player != MyPlayer) return; player._pMana = player._pMaxMana; player._pManaBase = player._pMaxManaBase; InitDiabloMsg(EMSG_SHRINE_CRYPTIC); RedrawEverything(); } void OperateShrineEldritch(Player &player) { if (&player != MyPlayer) return; for (Item &item : InventoryAndBeltPlayerItemsRange { player }) { if (item._itype != ItemType::Misc) { continue; } if (IsAnyOf(item._iMiscId, IMISC_HEAL, IMISC_MANA)) { // Reinitializing the item zeroes out the seed, we save and restore here to avoid triggering false // positives on duplicated item checks (e.g. when picking up the item). auto seed = item._iSeed; InitializeItem(item, ItemMiscIdIdx(IMISC_REJUV)); item._iSeed = seed; item._iStatFlag = true; continue; } if (IsAnyOf(item._iMiscId, IMISC_FULLHEAL, IMISC_FULLMANA)) { // As above. auto seed = item._iSeed; InitializeItem(item, ItemMiscIdIdx(IMISC_FULLREJUV)); item._iSeed = seed; item._iStatFlag = true; continue; } } RedrawEverything(); InitDiabloMsg(EMSG_SHRINE_ELDRITCH); } void OperateShrineEerie(Player &player) { if (&player != MyPlayer) return; ModifyPlrMag(player, 2); CheckStats(player); CalcPlrInv(player, true); RedrawEverything(); InitDiabloMsg(EMSG_SHRINE_EERIE); } /** * @brief Fully restores HP and Mana of the active player and spawns a pair of potions * in response to the player activating a Divine shrine * @param player The player who activated the shrine * @param spawnPosition The map tile where the potions will be spawned */ void OperateShrineDivine(Player &player, Point spawnPosition) { if (&player != MyPlayer) return; if (currlevel < 4) { CreateTypeItem(spawnPosition, false, ItemType::Misc, IMISC_FULLMANA, false, false, true); CreateTypeItem(spawnPosition, false, ItemType::Misc, IMISC_FULLHEAL, false, false, true); } else { CreateTypeItem(spawnPosition, false, ItemType::Misc, IMISC_FULLREJUV, false, false, true); CreateTypeItem(spawnPosition, false, ItemType::Misc, IMISC_FULLREJUV, false, false, true); } player._pMana = player._pMaxMana; player._pManaBase = player._pMaxManaBase; player._pHitPoints = player._pMaxHP; player._pHPBase = player._pMaxHPBase; RedrawEverything(); InitDiabloMsg(EMSG_SHRINE_DIVINE); } void OperateShrineHoly(const Player &player) { AddMissile(player.position.tile, { 0, 0 }, Direction::South, MissileID::Phasing, TARGET_MONSTERS, player, 0, 2 * leveltype); if (&player != MyPlayer) return; InitDiabloMsg(EMSG_SHRINE_HOLY); } void OperateShrineSpiritual(DiabloGenerator &rng, Player &player) { if (&player != MyPlayer) return; for (int8_t &itemIndex : player.InvGrid) { if (itemIndex == 0) { Item &goldItem = player.InvList[player._pNumInv]; MakeGoldStack(goldItem, 5 * leveltype + rng.generateRnd(10 * leveltype)); player._pNumInv++; itemIndex = player._pNumInv; player._pGold += goldItem._ivalue; } } InitDiabloMsg(EMSG_SHRINE_SPIRITUAL); } void OperateShrineSpooky(const Player &player) { if (&player == MyPlayer) { InitDiabloMsg(EMSG_SHRINE_SPOOKY1); return; } Player &myPlayer = *MyPlayer; myPlayer._pHitPoints = myPlayer._pMaxHP; myPlayer._pHPBase = myPlayer._pMaxHPBase; myPlayer._pMana = myPlayer._pMaxMana; myPlayer._pManaBase = myPlayer._pMaxManaBase; RedrawEverything(); InitDiabloMsg(EMSG_SHRINE_SPOOKY2); } void OperateShrineAbandoned(Player &player) { if (&player != MyPlayer) return; ModifyPlrDex(player, 2); CheckStats(player); CalcPlrInv(player, true); RedrawEverything(); InitDiabloMsg(EMSG_SHRINE_ABANDONED); } void OperateShrineCreepy(Player &player) { if (&player != MyPlayer) return; ModifyPlrStr(player, 2); CheckStats(player); CalcPlrInv(player, true); RedrawEverything(); InitDiabloMsg(EMSG_SHRINE_CREEPY); } void OperateShrineQuiet(Player &player) { if (&player != MyPlayer) return; ModifyPlrVit(player, 2); CheckStats(player); CalcPlrInv(player, true); RedrawEverything(); InitDiabloMsg(EMSG_SHRINE_QUIET); } void OperateShrineSecluded(const Player &player) { if (&player != MyPlayer) return; for (int x = 0; x < DMAXX; x++) for (int y = 0; y < DMAXY; y++) UpdateAutomapExplorer({ x, y }, MAP_EXP_SHRINE); InitDiabloMsg(EMSG_SHRINE_SECLUDED); } void OperateShrineGlimmering(Player &player) { if (&player != MyPlayer) return; for (Item &item : PlayerItemsRange { player }) { if (item._iMagical != ITEM_QUALITY_NORMAL && !item._iIdentified) { item._iIdentified = true; } } CalcPlrInv(player, true); RedrawEverything(); InitDiabloMsg(EMSG_SHRINE_GLIMMERING); } void OperateShrineTainted(DiabloGenerator &rng, const Player &player) { if (&player == MyPlayer) { InitDiabloMsg(EMSG_SHRINE_TAINTED1); return; } const int r = rng.generateRnd(4); const int v1 = r == 0 ? 1 : -1; const int v2 = r == 1 ? 1 : -1; const int v3 = r == 2 ? 1 : -1; const int v4 = r == 3 ? 1 : -1; Player &myPlayer = *MyPlayer; ModifyPlrStr(myPlayer, v1); ModifyPlrMag(myPlayer, v2); ModifyPlrDex(myPlayer, v3); ModifyPlrVit(myPlayer, v4); CheckStats(myPlayer); CalcPlrInv(myPlayer, true); RedrawEverything(); InitDiabloMsg(EMSG_SHRINE_TAINTED2); } /** * @brief Oily shrines increase the players primary stat(s) by a total of two, but spawn a * firewall near the shrine that will spread towards the player * @param player The player that will be affected by the shrine * @param spawnPosition Start location for the firewall */ void OperateShrineOily(Player &player, Point spawnPosition) { if (&player != MyPlayer) return; switch (player._pClass) { case HeroClass::Warrior: ModifyPlrStr(player, 2); break; case HeroClass::Rogue: ModifyPlrDex(player, 2); break; case HeroClass::Sorcerer: ModifyPlrMag(player, 2); break; case HeroClass::Barbarian: ModifyPlrVit(player, 2); break; case HeroClass::Monk: ModifyPlrStr(player, 1); ModifyPlrDex(player, 1); break; case HeroClass::Bard: ModifyPlrDex(player, 1); ModifyPlrMag(player, 1); break; } CheckStats(player); CalcPlrInv(player, true); RedrawEverything(); AddMissile( spawnPosition, player.position.tile, player._pdir, MissileID::FireWall, TARGET_PLAYERS, -1, 2 * currlevel + 2, 0); InitDiabloMsg(EMSG_SHRINE_OILY); } void OperateShrineGlowing(Player &player) { if (&player != MyPlayer) return; // Add 0-5 points to Magic (0.1% of the players XP) ModifyPlrMag(player, static_cast(std::min(player._pExperience / 1000, 5))); // Take 5% of the players experience to offset the bonus, unless they're very low level in which case take all their experience. if (player._pExperience > 5000) player._pExperience = static_cast(player._pExperience * 0.95); else player._pExperience = 0; CheckStats(player); RedrawEverything(); InitDiabloMsg(EMSG_SHRINE_GLOWING); } void OperateShrineMendicant(Player &player) { if (&player != MyPlayer) return; const int gold = player._pGold / 2; player.addExperience(gold); TakePlrsMoney(gold); RedrawEverything(); InitDiabloMsg(EMSG_SHRINE_MENDICANT); } /** * @brief Grants experience to the player based on the current dungeon level while also triggering a magic trap * @param player The player that will be affected by the shrine * @param spawnPosition The trap results in casting flash from this location targeting the player */ void OperateShrineSparkling(Player &player, Point spawnPosition) { if (&player != MyPlayer) return; player.addExperience(1000 * currlevel); AddMissile( spawnPosition, player.position.tile, player._pdir, MissileID::FlashBottom, TARGET_PLAYERS, -1, 3 * currlevel + 2, 0); RedrawEverything(); InitDiabloMsg(EMSG_SHRINE_SPARKLING); } /** * @brief Spawns a town portal near the active player * @param pnum The player that activated the shrine * @param spawnPosition The position of the shrine, the portal will be placed on the side closest to the player */ void OperateShrineTown(const Player &player, Point spawnPosition) { if (&player != MyPlayer) return; AddMissile( spawnPosition, player.position.tile, player._pdir, MissileID::TownPortal, TARGET_MONSTERS, player, 0, 0); InitDiabloMsg(EMSG_SHRINE_TOWN); } void OperateShrineShimmering(Player &player) { if (&player != MyPlayer) return; player._pMana = player._pMaxMana; player._pManaBase = player._pMaxManaBase; RedrawEverything(); InitDiabloMsg(EMSG_SHRINE_SHIMMERING); } void OperateShrineSolar(Player &player) { if (&player != MyPlayer) return; const time_t timeResult = time(nullptr); const std::tm *localtimeResult = localtime(&timeResult); const int hour = localtimeResult != nullptr ? localtimeResult->tm_hour : 20; if (hour >= 20 || hour < 4) { InitDiabloMsg(EMSG_SHRINE_SOLAR4); ModifyPlrVit(player, 2); } else if (hour >= 18) { InitDiabloMsg(EMSG_SHRINE_SOLAR3); ModifyPlrMag(player, 2); } else if (hour >= 12) { InitDiabloMsg(EMSG_SHRINE_SOLAR2); ModifyPlrStr(player, 2); } else /* 4:00 to 11:59 */ { InitDiabloMsg(EMSG_SHRINE_SOLAR1); ModifyPlrDex(player, 2); } CheckStats(player); CalcPlrInv(player, true); RedrawEverything(); } void OperateShrineMurphys(DiabloGenerator &rng, Player &player) { if (&player != MyPlayer) return; bool broke = false; for (auto &item : player.InvBody) { if (!item.isEmpty() && rng.flipCoin(3)) { if (item._iDurability != DUR_INDESTRUCTIBLE) { if (item._iDurability > 0) { item._iDurability /= 2; broke = true; break; } } } } if (!broke) { TakePlrsMoney(player._pGold / 3); } InitDiabloMsg(EMSG_SHRINE_MURPHYS); } void OperateShrine(Player &player, Object &shrine, SfxID sType) { if (!shrine.canInteractWith()) return; CloseGoldDrop(); DiabloGenerator rng(shrine._oRndSeed); shrine.selectionRegion = SelectionRegion::None; PlaySfxLoc(sType, shrine.position); shrine._oAnimFlag = true; shrine._oAnimDelay = 1; switch (shrine._oVar1) { case ShrineMysterious: OperateShrineMysterious(rng, player); break; case ShrineHidden: OperateShrineHidden(rng, player); break; case ShrineGloomy: OperateShrineGloomy(player); break; case ShrineWeird: OperateShrineWeird(player); break; case ShrineMagical: case ShrineMagicaL2: OperateShrineMagical(player); break; case ShrineStone: OperateShrineStone(player); break; case ShrineReligious: OperateShrineReligious(player); break; case ShrineEnchanted: OperateShrineEnchanted(rng, player); break; case ShrineThaumaturgic: OperateShrineThaumaturgic(rng, player); break; case ShrineFascinating: OperateShrineCostOfWisdom(player, SpellID::Firebolt, EMSG_SHRINE_FASCINATING); break; case ShrineCryptic: OperateShrineCryptic(player); break; case ShrineEldritch: OperateShrineEldritch(player); break; case ShrineEerie: OperateShrineEerie(player); break; case ShrineDivine: OperateShrineDivine(player, shrine.position); break; case ShrineHoly: OperateShrineHoly(player); break; case ShrineSacred: OperateShrineCostOfWisdom(player, SpellID::ChargedBolt, EMSG_SHRINE_SACRED); break; case ShrineSpiritual: OperateShrineSpiritual(rng, player); break; case ShrineSpooky: OperateShrineSpooky(player); break; case ShrineAbandoned: OperateShrineAbandoned(player); break; case ShrineCreepy: OperateShrineCreepy(player); break; case ShrineQuiet: OperateShrineQuiet(player); break; case ShrineSecluded: OperateShrineSecluded(player); break; case ShrineOrnate: OperateShrineCostOfWisdom(player, SpellID::HolyBolt, EMSG_SHRINE_ORNATE); break; case ShrineGlimmering: OperateShrineGlimmering(player); break; case ShrineTainted: OperateShrineTainted(rng, player); break; case ShrineOily: OperateShrineOily(player, shrine.position); break; case ShrineGlowing: OperateShrineGlowing(player); break; case ShrineMendicant: OperateShrineMendicant(player); break; case ShrineSparkling: OperateShrineSparkling(player, shrine.position); break; case ShrineTown: OperateShrineTown(player, shrine.position); break; case ShrineShimmering: OperateShrineShimmering(player); break; case ShrineSolar: OperateShrineSolar(player); break; case ShrineMurphys: OperateShrineMurphys(rng, player); break; } if (&player == MyPlayer) NetSendCmdLoc(MyPlayerId, false, CMD_OPERATEOBJ, shrine.position); } void OperateBookStand(Object &bookStand, bool sendmsg, bool sendLootMsg) { if (!bookStand.canInteractWith()) { return; } PlaySfxLoc(SfxID::ItemScroll, bookStand.position); bookStand.selectionRegion = SelectionRegion::None; bookStand._oAnimFrame += 2; SetRndSeed(bookStand._oRndSeed); if (FlipCoin(5)) CreateTypeItem(bookStand.position, false, ItemType::Misc, IMISC_BOOK, sendLootMsg, false); else CreateTypeItem(bookStand.position, false, ItemType::Misc, IMISC_SCROLL, sendLootMsg, false); if (sendmsg) NetSendCmdLoc(MyPlayerId, false, CMD_OPERATEOBJ, bookStand.position); } void OperateBookcase(Object &bookcase, bool sendmsg, bool sendLootMsg) { if (!bookcase.canInteractWith()) { return; } PlaySfxLoc(SfxID::ItemScroll, bookcase.position); bookcase.selectionRegion = SelectionRegion::None; bookcase._oAnimFrame -= 2; SetRndSeed(bookcase._oRndSeed); CreateTypeItem(bookcase.position, false, ItemType::Misc, IMISC_BOOK, sendLootMsg, false); if (Quests[Q_ZHAR].IsAvailable()) { Monster &zhar = Monsters[MAX_PLRS]; if (zhar.mode == MonsterMode::Stand // prevents playing the "angry" message for the second time if zhar got aggroed by losing vision and talking again && zhar.uniqueType == UniqueMonsterType::Zhar && zhar.activeForTicks == UINT8_MAX && zhar.hitPoints > 0) { zhar.talkMsg = TEXT_ZHAR2; M_StartStand(zhar, zhar.direction); // BUGFIX: first parameter in call to M_StartStand should be MAX_PLRS, not 0. (fixed) zhar.goal = MonsterGoal::Attack; if (sendmsg) zhar.mode = MonsterMode::Talk; } } if (sendmsg) NetSendCmdLoc(MyPlayerId, false, CMD_OPERATEOBJ, bookcase.position); } void OperateDecapitatedBody(Object &corpse, bool sendmsg, bool sendLootMsg) { if (!corpse.canInteractWith()) { return; } corpse.selectionRegion = SelectionRegion::None; SetRndSeed(corpse._oRndSeed); CreateRndItem(corpse.position, false, sendLootMsg, false); if (sendmsg) NetSendCmdLoc(MyPlayerId, false, CMD_OPERATEOBJ, corpse.position); } void OperateArmorStand(Object &armorStand, bool sendmsg, bool sendLootMsg) { if (!armorStand.canInteractWith()) { return; } armorStand.selectionRegion = SelectionRegion::None; armorStand._oAnimFrame++; SetRndSeed(armorStand._oRndSeed); const bool uniqueRnd = !FlipCoin(); if (currlevel <= 5) { CreateTypeItem(armorStand.position, true, ItemType::LightArmor, IMISC_NONE, sendLootMsg, false); } else if (currlevel >= 6 && currlevel <= 9) { CreateTypeItem(armorStand.position, uniqueRnd, ItemType::MediumArmor, IMISC_NONE, sendLootMsg, false); } else if (currlevel >= 10 && currlevel <= 12) { CreateTypeItem(armorStand.position, false, ItemType::HeavyArmor, IMISC_NONE, sendLootMsg, false); } else if (currlevel >= 13) { CreateTypeItem(armorStand.position, true, ItemType::HeavyArmor, IMISC_NONE, sendLootMsg, false); } if (sendmsg) NetSendCmdLoc(MyPlayerId, false, CMD_OPERATEOBJ, armorStand.position); } int FindValidShrine() { for (;;) { const int rv = GenerateRnd(gbIsHellfire ? NumberOfShrineTypes : 26); if ((rv == ShrineEnchanted && !IsAnyOf(leveltype, DTYPE_CATHEDRAL, DTYPE_CATACOMBS)) || rv == ShrineThaumaturgic) continue; if (gbIsMultiplayer && shrineavail[rv] == ShrineTypeSingle) continue; if (!gbIsMultiplayer && shrineavail[rv] == ShrineTypeMulti) continue; return rv; } } void OperateGoatShrine(Player &player, Object &object, SfxID sType) { SetRndSeed(object._oRndSeed); object._oVar1 = FindValidShrine(); OperateShrine(player, object, sType); object._oAnimDelay = 2; RedrawEverything(); } void OperateCauldron(Player &player, Object &object, SfxID sType) { SetRndSeed(object._oRndSeed); object._oVar1 = FindValidShrine(); OperateShrine(player, object, sType); object._oAnimFrame = 3; object._oAnimFlag = false; RedrawEverything(); } bool OperateFountains(Player &player, Object &fountain) { bool applied = false; switch (fountain._otype) { case OBJ_BLOODFTN: if (&player != MyPlayer) return false; if (player._pHitPoints < player._pMaxHP) { PlaySfxLoc(SfxID::OperateFountain, fountain.position); player._pHitPoints += 64; player._pHPBase += 64; if (player._pHitPoints > player._pMaxHP) { player._pHitPoints = player._pMaxHP; player._pHPBase = player._pMaxHPBase; } applied = true; } else PlaySfxLoc(SfxID::OperateFountain, fountain.position); break; case OBJ_PURIFYINGFTN: if (&player != MyPlayer) return false; if (player._pMana < player._pMaxMana) { PlaySfxLoc(SfxID::OperateFountain, fountain.position); player._pMana += 64; player._pManaBase += 64; if (player._pMana > player._pMaxMana) { player._pMana = player._pMaxMana; player._pManaBase = player._pMaxManaBase; } applied = true; } else PlaySfxLoc(SfxID::OperateFountain, fountain.position); break; case OBJ_MURKYFTN: if (!fountain.canInteractWith()) break; PlaySfxLoc(SfxID::OperateFountain, fountain.position); fountain.selectionRegion = SelectionRegion::None; AddMissile( player.position.tile, player.position.tile, player._pdir, MissileID::Infravision, TARGET_MONSTERS, player, 0, 2 * leveltype); applied = true; if (&player == MyPlayer) NetSendCmdLoc(MyPlayerId, false, CMD_OPERATEOBJ, fountain.position); break; case OBJ_TEARFTN: { if (!fountain.canInteractWith()) break; PlaySfxLoc(SfxID::OperateFountain, fountain.position); fountain.selectionRegion = SelectionRegion::None; if (&player != MyPlayer) return false; const unsigned randomValue = (fountain._oRndSeed >> 16) % 12; const unsigned fromStat = randomValue / 3; unsigned toStat = randomValue % 3; if (toStat >= fromStat) toStat++; const std::pair alterations[] = { { fromStat, -1 }, { toStat, 1 } }; for (const auto &[stat, delta] : alterations) { switch (stat) { case 0: ModifyPlrStr(player, delta); break; case 1: ModifyPlrMag(player, delta); break; case 2: ModifyPlrDex(player, delta); break; case 3: ModifyPlrVit(player, delta); break; } } CheckStats(player); applied = true; if (&player == MyPlayer) NetSendCmdLoc(MyPlayerId, false, CMD_OPERATEOBJ, fountain.position); } break; default: break; } RedrawEverything(); return applied; } void OperateWeaponRack(Object &weaponRack, bool sendmsg, bool sendLootMsg) { if (!weaponRack.canInteractWith()) return; SetRndSeed(weaponRack._oRndSeed); const ItemType weaponType { PickRandomlyAmong({ ItemType::Sword, ItemType::Axe, ItemType::Bow, ItemType::Mace }) }; weaponRack.selectionRegion = SelectionRegion::None; weaponRack._oAnimFrame++; CreateTypeItem(weaponRack.position, leveltype != DTYPE_CATHEDRAL, weaponType, IMISC_NONE, sendLootMsg, false); if (sendmsg) NetSendCmdLoc(MyPlayerId, false, CMD_OPERATEOBJ, weaponRack.position); } /** * @brief Checks whether the player is activating Na-Krul's spell tomes in the correct order * * Used as part of the final Diablo: Hellfire quest (from the hints provided to the player in the * reconstructed note). This function both updates the state of the variable that tracks progress * and also determines whether the spawn conditions are met (i.e. all tomes have been triggered * in the correct order). * * @param s the id of the spell tome * @return true if the player has activated all three tomes in the correct order, false otherwise */ bool OperateNakrulBook(int s) { switch (s) { case 6: NaKrulTomeSequence = 1; break; case 7: if (NaKrulTomeSequence == 1) { NaKrulTomeSequence = 2; } else { NaKrulTomeSequence = 0; } break; case 8: if (NaKrulTomeSequence == 2) return true; NaKrulTomeSequence = 0; break; } return false; } void OperateStoryBook(Object &storyBook) { if (!storyBook.canInteractWith() || qtextflag) { return; } storyBook._oAnimFrame = storyBook._oVar4; PlaySfxLoc(SfxID::ItemScroll, storyBook.position); auto msg = static_cast<_speech_id>(storyBook._oVar2); if (storyBook._oVar8 != 0 && currlevel == 24) { if (!IsUberLeverActivated && Quests[Q_NAKRUL]._qactive != QUEST_DONE && OperateNakrulBook(storyBook._oVar8)) { NetSendCmd(false, CMD_NAKRUL); return; } } else if (leveltype == DTYPE_CRYPT) { Quests[Q_NAKRUL]._qactive = QUEST_ACTIVE; Quests[Q_NAKRUL]._qlog = true; Quests[Q_NAKRUL]._qmsg = msg; NetSendCmdQuest(true, Quests[Q_NAKRUL]); } InitQTextMsg(msg); NetSendCmdLoc(MyPlayerId, false, CMD_OPERATEOBJ, storyBook.position); } void OperateLazStand(Object &stand) { if (ActiveItemCount >= MAXITEMS) { return; } if (!stand.canInteractWith() || qtextflag) { return; } stand._oAnimFrame++; stand.selectionRegion = SelectionRegion::None; const Point pos = GetSuperItemLoc(stand.position); SpawnQuestItem(IDI_LAZSTAFF, pos, 0, SelectionRegion::None, true); NetSendCmdLoc(MyPlayerId, false, CMD_OPERATEOBJ, stand.position); } /** * @brief Checks if all active crux objects of the given type have been broken. * * Called by BreakCrux and SyncCrux to see if the linked map area needs to be updated. In practice I think this is * always true when called by BreakCrux as there *should* only be one instance of each crux with a given _oVar8 value? * * @param cruxType Discriminator/type (_oVar8 value) of the crux object which is currently changing state * @return true if all active cruxes of that type on the level are broken, false if at least one remains unbroken */ bool AreAllCruxesOfTypeBroken(int cruxType) { for (int j = 0; j < ActiveObjectCount; j++) { const auto &testObject = Objects[ActiveObjects[j]]; if (!testObject.IsCrux()) continue; // Not a Crux object, keep searching if (cruxType != testObject._oVar8 || testObject._oBreak == -1) continue; // Found either a different crux or a previously broken crux, keep searching // Found an unbroken crux of this type return false; } return true; } void BreakCrux(Object &crux, bool sendmsg) { if (!crux.canInteractWith()) return; crux._oAnimFlag = true; crux._oAnimFrame = 1; crux._oAnimDelay = 1; crux._oSolidFlag = true; crux._oMissFlag = true; crux._oBreak = -1; crux.selectionRegion = SelectionRegion::None; if (sendmsg) NetSendCmdLoc(MyPlayerId, false, CMD_BREAKOBJ, crux.position); if (!AreAllCruxesOfTypeBroken(crux._oVar8)) return; PlaySfxLoc(SfxID::OperateLever, crux.position); ObjChangeMap(crux._oVar1, crux._oVar2, crux._oVar3, crux._oVar4); } void BreakBarrel(const Player &player, Object &barrel, bool forcebreak, bool sendmsg) { if (!barrel.canInteractWith()) return; if (!forcebreak && &player != MyPlayer) { return; } barrel._oAnimFlag = true; barrel._oAnimFrame = 1; barrel._oAnimDelay = 1; barrel._oSolidFlag = false; barrel._oMissFlag = true; barrel._oBreak = -1; barrel.selectionRegion = SelectionRegion::None; barrel._oPreFlag = true; if (barrel.isExplosive()) { if (barrel._otype == _object_id::OBJ_URNEX) PlaySfxLoc(SfxID::UrnExpload, barrel.position); else if (barrel._otype == _object_id::OBJ_PODEX) PlaySfxLoc(SfxID::PodExpload, barrel.position); else PlaySfxLoc(SfxID::BarrelExpload, barrel.position); for (int yp = barrel.position.y - 1; yp <= barrel.position.y + 1; yp++) { for (int xp = barrel.position.x - 1; xp <= barrel.position.x + 1; xp++) { constexpr MissileID TrapMissile = MissileID::Firebolt; if (dMonster[xp][yp] > 0) { MonsterTrapHit(dMonster[xp][yp] - 1, 1, 4, 0, TrapMissile, GetMissileData(TrapMissile).damageType(), false); } Player *adjacentPlayer = PlayerAtPosition({ xp, yp }, true); if (adjacentPlayer != nullptr) { bool unused; PlayerMHit(*adjacentPlayer, nullptr, 0, 8, 16, TrapMissile, GetMissileData(TrapMissile).damageType(), false, DeathReason::MonsterOrTrap, &unused); } // don't really need to exclude large objects as explosive barrels are single tile objects, but using considerLargeObjects == false as this matches the old logic. Object *adjacentObject = FindObjectAtPosition({ xp, yp }, false); if (adjacentObject != nullptr && adjacentObject->isExplosive() && !adjacentObject->IsBroken()) { BreakBarrel(player, *adjacentObject, true, sendmsg); } } } } else { if (barrel._otype == _object_id::OBJ_URN) PlaySfxLoc(SfxID::UrnBreak, barrel.position); else if (barrel._otype == _object_id::OBJ_POD) PlaySfxLoc(SfxID::PodPop, barrel.position); else PlaySfxLoc(SfxID::BarrelBreak, barrel.position); SetRndSeed(barrel._oRndSeed); if (barrel._oVar2 <= 1) { if (barrel._oVar3 == 0) CreateRndUseful(barrel.position, sendmsg); else CreateRndItem(barrel.position, false, sendmsg, false); } if (barrel._oVar2 >= 8 && barrel._oVar4 >= 0) ActivateSkeleton(Monsters[barrel._oVar4], barrel.position); } if (&player == MyPlayer) { NetSendCmdLoc(MyPlayerId, false, CMD_BREAKOBJ, barrel.position); } } void SyncCrux(const Object &crux) { if (AreAllCruxesOfTypeBroken(crux._oVar8)) ObjChangeMap(crux._oVar1, crux._oVar2, crux._oVar3, crux._oVar4); } void SyncLever(const Object &lever) { if (lever.canInteractWith()) return; if (currlevel == 16 && !AreAllLeversActivated(lever._oVar8)) return; ObjChangeMap(lever._oVar1, lever._oVar2, lever._oVar3, lever._oVar4); } void SyncQSTLever(const Object &qstLever) { if (qstLever._oAnimFrame == qstLever._oVar6) { if (qstLever._otype != OBJ_BLOODBOOK) ObjChangeMapResync(qstLever._oVar1, qstLever._oVar2, qstLever._oVar3, qstLever._oVar4); if (qstLever._otype == OBJ_BLINDBOOK) { auto tren = TransVal; TransVal = 9; DRLG_MRectTrans(WorldTilePosition(qstLever._oVar1, qstLever._oVar2), WorldTilePosition(qstLever._oVar3, qstLever._oVar4)); TransVal = tren; } } } void SyncPedestal(const Object &pedestal) { if (pedestal._oVar6 == 1) ObjChangeMapResync(SetPiece.position.x, SetPiece.position.y + 3, SetPiece.position.x + 2, SetPiece.position.y + 7); if (pedestal._oVar6 == 2) { ObjChangeMapResync(SetPiece.position.x, SetPiece.position.y + 3, SetPiece.position.x + 2, SetPiece.position.y + 7); ObjChangeMapResync(SetPiece.position.x + 6, SetPiece.position.y + 3, SetPiece.position.x + SetPiece.size.width, SetPiece.position.y + 7); } if (pedestal._oVar6 >= 3) { ObjChangeMapResync(pedestal._oVar1, pedestal._oVar2, pedestal._oVar3, pedestal._oVar4); LoadMapObjects("levels\\l2data\\blood2.dun", SetPiece.position.megaToWorld()); } } void UpdatePedestalState(Object &pedestal) { const int addedStones = Quests[Q_BLOOD]._qvar2; pedestal._oAnimFrame += addedStones; pedestal._oVar6 += addedStones; SyncPedestal(pedestal); if (pedestal._oVar6 >= 3) pedestal.selectionRegion = SelectionRegion::None; } void SyncDoor(Object &door) { if (door._oVar4 == DOOR_CLOSED) { SetDoorStateClosed(door); } else { SetDoorStateOpen(door); } } void ResyncDoors(WorldTilePosition p1, WorldTilePosition p2, bool sendmsg) { const WorldTileSize size { static_cast(p2.x - p1.x), static_cast(p2.y - p1.y) }; const WorldTileRectangle area { p1, size }; for (const WorldTilePosition p : PointsInRectangle { area }) { Object *obj = FindObjectAtPosition(p); if (obj == nullptr) continue; if (IsNoneOf(obj->_otype, OBJ_L1LDOOR, OBJ_L1RDOOR, OBJ_L2LDOOR, OBJ_L2RDOOR, OBJ_L3LDOOR, OBJ_L3RDOOR, OBJ_L5LDOOR, OBJ_L5RDOOR)) continue; SyncDoor(*obj); if (sendmsg) { const bool isOpen = obj->_oVar4 == DOOR_OPEN; NetSendCmdLoc(MyPlayerId, true, isOpen ? CMD_OPENDOOR : CMD_CLOSEDOOR, obj->position); } } } void UpdateState(Object &object, int frame) { if (!object.canInteractWith()) { return; } object.selectionRegion = SelectionRegion::None; object._oAnimFrame = frame; object._oAnimFlag = false; } } // namespace unsigned int Object::GetId() const { return std::abs(dObject[position.x][position.y]) - 1; } bool Object::IsDisabled() const { if (!*GetOptions().Gameplay.disableCripplingShrines) { return false; } if (IsAnyOf(_otype, _object_id::OBJ_GOATSHRINE, _object_id::OBJ_CAULDRON)) { return true; } if (!IsShrine()) { return false; } return IsAnyOf(static_cast(_oVar1), shrine_type::ShrineFascinating, shrine_type::ShrineOrnate, shrine_type::ShrineSacred, shrine_type::ShrineMurphys); } Object *FindObjectAtPosition(Point position, bool considerLargeObjects) { if (!InDungeonBounds(position)) { return nullptr; } auto objectId = dObject[position.x][position.y]; if (objectId > 0 || (considerLargeObjects && objectId != 0)) { return &Objects[std::abs(objectId) - 1]; } // nothing at this position, return a nullptr return nullptr; } bool IsItemBlockingObjectAtPosition(Point position) { Object *object = FindObjectAtPosition(position); if (object != nullptr && object->_oSolidFlag) { // solid object return true; } object = FindObjectAtPosition(position + Direction::South); if (object != nullptr && object->canInteractWith()) { // An unopened container or breakable object exists which potentially overlaps this tile, the player might not be able to pick up an item dropped here. return true; } object = FindObjectAtPosition(position + Direction::SouthEast, false); if (object != nullptr) { Object *otherDoor = FindObjectAtPosition(position + Direction::SouthWest, false); if (otherDoor != nullptr && object->canInteractWith() && otherDoor->canInteractWith()) { // Two interactive objects potentially overlap both sides of this tile, as above the player might not be able to pick up an item which is dropped here. return true; } } return false; } tl::expected LoadLevelObjects(uint16_t filesWidths[65]) { if (HeadlessMode) return {}; for (const ObjectData objectData : AllObjects) { if (leveltype == objectData.olvltype) { filesWidths[objectData.ofindex] = objectData.animWidth; } } for (size_t i = 0, n = ObjMasterLoadList.size(); i < n; ++i) { if (filesWidths[i] == 0) { continue; } ObjFileList[numobjfiles] = static_cast(i); char filestr[32]; *BufCopy(filestr, "objects\\", ObjMasterLoadList[i]) = '\0'; ASSIGN_OR_RETURN(pObjCels[numobjfiles], LoadCelWithStatus(filestr, filesWidths[i])); numobjfiles++; } return {}; } tl::expected InitObjectGFX() { uint16_t filesWidths[65] = {}; if (IsAnyOf(currlevel, 4, 8, 12)) { for (const auto id : { OBJ_STORYBOOK, OBJ_STORYCANDLE }) { const ObjectData &obj = AllObjects[id]; filesWidths[obj.ofindex] = obj.animWidth; } } for (size_t id = 0, n = AllObjects.size(); id < n; ++id) { const ObjectData &objectData = AllObjects[id]; if (objectData.minlvl != 0 && currlevel >= objectData.minlvl && currlevel <= objectData.maxlvl) { if (IsAnyOf(static_cast<_object_id>(id), OBJ_TRAPL, OBJ_TRAPR) && leveltype == DTYPE_HELL) { continue; } filesWidths[objectData.ofindex] = objectData.animWidth; } if (objectData.otheme != THEME_NONE) { for (int j = 0; j < numthemes; j++) { if (themes[j].ttype == objectData.otheme) { filesWidths[objectData.ofindex] = objectData.animWidth; } } } if (objectData.oquest != Q_INVALID && Quests[objectData.oquest].IsAvailable()) { filesWidths[objectData.ofindex] = objectData.animWidth; } } return LoadLevelObjects(filesWidths); } void FreeObjectGFX() { for (int i = 0; i < numobjfiles; i++) { pObjCels[i] = std::nullopt; } numobjfiles = 0; } void AddL1Objs(int x1, int y1, int x2, int y2) { for (int j = y1; j < y2; j++) { for (int i = x1; i < x2; i++) { const int pn = dPiece[i][j]; if (pn == 269) AddObject(OBJ_L1LIGHT, { i, j }); if (pn == 43 || pn == 50 || pn == 213) AddObject(OBJ_L1LDOOR, { i, j }); if (pn == 45 || pn == 55) AddObject(OBJ_L1RDOOR, { i, j }); } } } void AddL2Objs(int x1, int y1, int x2, int y2) { for (int j = y1; j < y2; j++) { for (int i = x1; i < x2; i++) { const int pn = dPiece[i][j]; if (pn == 12 || pn == 540) AddObject(OBJ_L2LDOOR, { i, j }); if (pn == 16 || pn == 541) AddObject(OBJ_L2RDOOR, { i, j }); } } } void AddL3Objs(int x1, int y1, int x2, int y2) { for (int j = y1; j < y2; j++) { for (int i = x1; i < x2; i++) { const int pn = dPiece[i][j]; if (pn == 530) AddObject(OBJ_L3LDOOR, { i, j }); if (pn == 533) AddObject(OBJ_L3RDOOR, { i, j }); } } } void AddCryptObjects(int x1, int y1, int x2, int y2) { for (int j = y1; j < y2; j++) { for (int i = x1; i < x2; i++) { const int pn = dPiece[i][j]; if (pn == 76) AddObject(OBJ_L5LDOOR, { i, j }); if (pn == 79) AddObject(OBJ_L5RDOOR, { i, j }); } } } void AddSlainHero() { const Point rndObjLoc = GetRndObjLoc(5); AddObject(OBJ_SLAINHERO, rndObjLoc + Displacement { 2, 2 }); } void InitObjects() { ClrAllObjects(); NaKrulTomeSequence = 0; if (currlevel == 16) { AddDiabObjs(); } else { DiscardRandomValues(1); if (currlevel == 9 && !UseMultiplayerQuests()) AddSlainHero(); if (Quests[Q_MUSHROOM].IsAvailable()) AddMushPatch(); if (currlevel == 4 || currlevel == 8 || currlevel == 12) AddStoryBooks(); if (currlevel == 21) { AddCryptStoryBook(1); } else if (currlevel == 22) { AddCryptStoryBook(2); AddCryptStoryBook(3); } else if (currlevel == 23) { AddCryptStoryBook(4); AddCryptStoryBook(5); } if (currlevel == 24) { AddNakrulGate(); } if (leveltype == DTYPE_CATHEDRAL) { if (Quests[Q_BUTCHER].IsAvailable()) AddTortures(); if (Quests[Q_PWATER].IsAvailable()) AddCandles(); if (Quests[Q_LTBANNER].IsAvailable()) AddObject(OBJ_SIGNCHEST, SetPiece.position.megaToWorld() + Displacement { 10, 3 }); InitRndLocBigObj(10, 15, OBJ_SARC); AddL1Objs(0, 0, MAXDUNX, MAXDUNY); InitRndBarrels(); } if (leveltype == DTYPE_CATACOMBS) { if (Quests[Q_ROCK].IsAvailable()) InitRndLocObj5x5(1, 1, OBJ_STAND); if (Quests[Q_SCHAMB].IsAvailable()) InitRndLocObj5x5(1, 1, OBJ_BOOK2R); AddL2Objs(0, 0, MAXDUNX, MAXDUNY); AddL2Torches(); if (Quests[Q_BLIND].IsAvailable()) { _speech_id spId; switch (MyPlayer->_pClass) { case HeroClass::Warrior: spId = TEXT_BLINDING; break; case HeroClass::Rogue: spId = TEXT_RBLINDING; break; case HeroClass::Sorcerer: spId = TEXT_MBLINDING; break; case HeroClass::Monk: spId = TEXT_HBLINDING; break; case HeroClass::Bard: spId = TEXT_RBLINDING; break; case HeroClass::Barbarian: spId = TEXT_BLINDING; break; } Quests[Q_BLIND]._qmsg = spId; AddBookLever(OBJ_BLINDBOOK, { SetPiece.position, SetPiece.size + 1 }, spId); LoadMapObjects("levels\\l2data\\blind2.dun", SetPiece.position.megaToWorld()); } if (Quests[Q_BLOOD].IsAvailable()) { _speech_id spId; switch (MyPlayer->_pClass) { case HeroClass::Warrior: spId = TEXT_BLOODY; break; case HeroClass::Rogue: spId = TEXT_RBLOODY; break; case HeroClass::Sorcerer: spId = TEXT_MBLOODY; break; case HeroClass::Monk: spId = TEXT_HBLOODY; break; case HeroClass::Bard: spId = TEXT_RBLOODY; break; case HeroClass::Barbarian: spId = TEXT_BLOODY; break; } Quests[Q_BLOOD]._qmsg = spId; AddBookLever(OBJ_BLOODBOOK, { SetPiece.position + Displacement { 0, 3 }, { 2, 4 } }, spId); AddObject(OBJ_PEDESTAL, SetPiece.position.megaToWorld() + Displacement { 9, 16 }); } InitRndBarrels(); } if (leveltype == DTYPE_CAVES) { AddL3Objs(0, 0, MAXDUNX, MAXDUNY); InitRndBarrels(); } if (leveltype == DTYPE_HELL) { if (Quests[Q_WARLORD].IsAvailable()) { _speech_id spId; switch (MyPlayer->_pClass) { case HeroClass::Warrior: spId = TEXT_BLOODWAR; break; case HeroClass::Rogue: spId = TEXT_RBLOODWAR; break; case HeroClass::Sorcerer: spId = TEXT_MBLOODWAR; break; case HeroClass::Monk: spId = TEXT_HBLOODWAR; break; case HeroClass::Bard: spId = TEXT_RBLOODWAR; break; case HeroClass::Barbarian: spId = TEXT_BLOODWAR; break; } Quests[Q_WARLORD]._qmsg = spId; AddBookLever(OBJ_STEELTOME, SetPiece, spId); LoadMapObjects("levels\\l4data\\warlord.dun", SetPiece.position.megaToWorld()); } if (Quests[Q_BETRAYER].IsAvailable() && !UseMultiplayerQuests()) AddLazStand(); InitRndBarrels(); AddL4Goodies(); } if (leveltype == DTYPE_NEST) { InitRndBarrels(); } if (leveltype == DTYPE_CRYPT) { InitRndLocBigObj(10, 15, OBJ_L5SARC); AddCryptObjects(0, 0, MAXDUNX, MAXDUNY); InitRndBarrels(); } InitRndLocObj(5, 10, OBJ_CHEST1); InitRndLocObj(3, 6, OBJ_CHEST2); InitRndLocObj(1, 5, OBJ_CHEST3); if (leveltype != DTYPE_HELL) AddObjTraps(); if (IsAnyOf(leveltype, DTYPE_CATACOMBS, DTYPE_CAVES, DTYPE_HELL, DTYPE_NEST)) AddChestTraps(); } } void SetMapObjects(const uint16_t *dunData, int startx, int starty) { uint16_t filesWidths[65] = {}; ClrAllObjects(); WorldTileSize size = GetDunSize(dunData); const int layer2Offset = 2 + size.width * size.height; // The rest of the layers are at dPiece scale size *= static_cast(2); const uint16_t *objectLayer = &dunData[layer2Offset + size.width * size.height * 2]; for (WorldTileCoord j = 0; j < size.height; j++) { for (WorldTileCoord i = 0; i < size.width; i++) { auto objectId = static_cast(SDL_SwapLE16(objectLayer[j * size.width + i])); if (objectId != 0) { const ObjectData &objectData = AllObjects[ObjTypeConv[objectId]]; filesWidths[objectData.ofindex] = objectData.animWidth; } } } LoadLevelObjects(filesWidths); for (WorldTileCoord j = 0; j < size.height; j++) { for (WorldTileCoord i = 0; i < size.width; i++) { auto objectId = static_cast(SDL_SwapLE16(objectLayer[j * size.width + i])); if (objectId != 0) { AddObject(ObjTypeConv[objectId], { startx + 16 + i, starty + 16 + j }); } } } } Object *AddObject(_object_id objType, Point objPos) { if (ActiveObjectCount >= MAXOBJECTS) return nullptr; const int oi = AvailableObjects[0]; AvailableObjects[0] = AvailableObjects[MAXOBJECTS - 1 - ActiveObjectCount]; ActiveObjects[ActiveObjectCount] = oi; dObject[objPos.x][objPos.y] = oi + 1; Object &object = Objects[oi]; SetupObject(object, objPos, objType); switch (object._otype) { case OBJ_L1LDOOR: case OBJ_L1RDOOR: case OBJ_L2LDOOR: case OBJ_L2RDOOR: case OBJ_L3LDOOR: case OBJ_L3RDOOR: case OBJ_L5LDOOR: case OBJ_L5RDOOR: AddDoor(object); break; case OBJ_BOOK2R: object.InitializeBook({ SetPiece.position, WorldTileSize(SetPiece.size.width + 1, SetPiece.size.height + 1) }); break; case OBJ_CHEST1: case OBJ_CHEST2: case OBJ_CHEST3: AddChest(object); break; case OBJ_TCHEST1: case OBJ_TCHEST2: case OBJ_TCHEST3: AddChest(object); object._oTrapFlag = true; if (leveltype == DTYPE_CATACOMBS) { object._oVar4 = GenerateRnd(2); } else { object._oVar4 = GenerateRnd(3); } break; case OBJ_SARC: case OBJ_L5SARC: AddSarcophagus(object); break; case OBJ_FLAMEHOLE: AddFlameTrap(object); break; case OBJ_FLAMELVR: AddFlameLever(object); break; case OBJ_WATER: object._oAnimFrame = 1; break; case OBJ_TRAPL: case OBJ_TRAPR: AddTrap(object); break; case OBJ_BARREL: case OBJ_BARRELEX: case OBJ_POD: case OBJ_PODEX: case OBJ_URN: case OBJ_URNEX: AddBarrel(object); break; case OBJ_SHRINEL: case OBJ_SHRINER: AddShrine(object); break; case OBJ_BOOKCASEL: case OBJ_BOOKCASER: AddBookcase(object); break; case OBJ_SKELBOOK: case OBJ_BOOKSTAND: case OBJ_BLOODFTN: case OBJ_GOATSHRINE: case OBJ_CAULDRON: case OBJ_TEARFTN: case OBJ_SLAINHERO: object._oRndSeed = AdvanceRndSeed(); break; case OBJ_DECAP: AddDecapitatedBody(object); break; case OBJ_PURIFYINGFTN: case OBJ_MURKYFTN: AddLargeFountain(object); break; case OBJ_ARMORSTAND: case OBJ_WARARMOR: AddArmorStand(object); break; case OBJ_BOOK2L: AddBookOfVileness(object); break; case OBJ_MCIRCLE1: case OBJ_MCIRCLE2: AddMagicCircle(object); break; case OBJ_STORYBOOK: case OBJ_L5BOOKS: AddStoryBook(object); break; case OBJ_BCROSS: case OBJ_TBCROSS: object._oRndSeed = AdvanceRndSeed(); break; case OBJ_PEDESTAL: AddPedestalOfBlood(object); break; case OBJ_WARWEAP: case OBJ_WEAPONRACK: AddWeaponRack(object); break; case OBJ_TNUDEM2: AddTorturedBody(object); break; default: break; } AddObjectLight(object); ActiveObjectCount++; return &object; } bool UpdateTrapState(Object &trap) { if (trap._oVar4 != 0) return false; Object &trigger = ObjectAtPosition({ trap._oVar1, trap._oVar2 }); switch (trigger._otype) { case OBJ_L1LDOOR: case OBJ_L1RDOOR: case OBJ_L2LDOOR: case OBJ_L2RDOOR: case OBJ_L3LDOOR: case OBJ_L3RDOOR: case OBJ_L5LDOOR: case OBJ_L5RDOOR: if (trigger._oVar4 == DOOR_CLOSED && trigger._oTrapFlag) return false; break; case OBJ_LEVER: case OBJ_CHEST1: case OBJ_CHEST2: case OBJ_CHEST3: case OBJ_SWITCHSKL: case OBJ_SARC: case OBJ_L5LEVER: case OBJ_L5SARC: if (trigger.canInteractWith() && trigger._oTrapFlag) return false; break; default: return false; } trap._oVar4 = 1; trigger._oTrapFlag = false; return true; } void OperateTrap(Object &trap) { if (!UpdateTrapState(trap)) return; // default to firing at the trigger object const Point triggerPosition = { trap._oVar1, trap._oVar2 }; Point target = triggerPosition; auto searchArea = PointsInRectangle(Rectangle { target, 1 }); // look for a player near the trigger (using a reverse search to match vanilla behaviour) auto foundPosition = std::find_if(searchArea.crbegin(), searchArea.crend(), [](Point testPosition) { return InDungeonBounds(testPosition) && dPlayer[testPosition.x][testPosition.y] != 0; }); if (foundPosition != searchArea.crend()) { // if a player is standing near the trigger then target them instead target = *foundPosition; } const Direction dir = GetDirection(trap.position, target); AddMissile(trap.position, target, dir, static_cast(trap._oVar3), TARGET_PLAYERS, -1, 0, 0); PlaySfxLoc(SfxID::TriggerTrap, triggerPosition); } void ProcessObjects() { for (int i = 0; i < ActiveObjectCount; ++i) { Object &object = Objects[ActiveObjects[i]]; switch (object._otype) { case OBJ_L1LIGHT: case OBJ_SKFIRE: case OBJ_CANDLE1: case OBJ_CANDLE2: case OBJ_BOOKCANDLE: UpdateObjectLight(object, 5); break; case OBJ_STORYCANDLE: case OBJ_L5CANDLE: UpdateObjectLight(object, 3); break; case OBJ_CRUX1: case OBJ_CRUX2: case OBJ_CRUX3: case OBJ_BARREL: case OBJ_BARRELEX: case OBJ_POD: case OBJ_PODEX: case OBJ_URN: case OBJ_URNEX: case OBJ_SHRINEL: case OBJ_SHRINER: ObjectStopAnim(object); break; case OBJ_L1LDOOR: case OBJ_L1RDOOR: case OBJ_L2LDOOR: case OBJ_L2RDOOR: case OBJ_L3LDOOR: case OBJ_L3RDOOR: case OBJ_L5LDOOR: case OBJ_L5RDOOR: UpdateDoor(object); break; case OBJ_TORCHL: case OBJ_TORCHR: case OBJ_TORCHL2: case OBJ_TORCHR2: UpdateObjectLight(object, 8); break; case OBJ_SARC: case OBJ_L5SARC: UpdateSarcophagus(object); break; case OBJ_FLAMEHOLE: UpdateFlameTrap(object); break; case OBJ_TRAPL: case OBJ_TRAPR: OperateTrap(object); break; case OBJ_MCIRCLE1: case OBJ_MCIRCLE2: UpdateCircle(object); break; case OBJ_BCROSS: case OBJ_TBCROSS: UpdateObjectLight(object, 5); UpdateBurningCrossDamage(object); break; default: break; } if (!object._oAnimFlag) continue; object._oAnimCnt++; if (object._oAnimCnt < object._oAnimDelay) continue; object._oAnimCnt = 0; object._oAnimFrame++; if (object._oAnimFrame > object._oAnimLen) object._oAnimFrame = 1; } for (int i = 0; i < ActiveObjectCount;) { const int oi = ActiveObjects[i]; if (Objects[oi]._oDelFlag) { DeleteObject(oi, i); } else { i++; } } } void RedoPlayerVision() { for (const Player &player : Players) { if (player.plractive && player.isOnActiveLevel()) { ChangeVisionXY(player.getId(), player.position.tile); } } } void MonstCheckDoors(const Monster &monster) { for (const Direction dir : { Direction::NorthEast, Direction::SouthWest, Direction::North, Direction::East, Direction::South, Direction::West, Direction::NorthWest, Direction::SouthEast }) { Object *object = FindObjectAtPosition(monster.position.tile + dir); if (object == nullptr) continue; Object &door = *object; // Doors use _oVar4 to track open/closed state, non-zero values indicate an open door if (!door.isDoor() || door._oVar4 != DOOR_CLOSED) continue; OperateDoor(door, true); } } void ObjChangeMap(int x1, int y1, int x2, int y2) { for (int j = y1; j <= y2; j++) { for (int i = x1; i <= x2; i++) { ObjSetMini({ i, j }, pdungeon[i][j]); dungeon[i][j] = pdungeon[i][j]; } } const WorldTilePosition mega1 { static_cast(x1), static_cast(y1) }; const WorldTilePosition mega2 { static_cast(x2), static_cast(y2) }; const WorldTilePosition world1 = mega1.megaToWorld(); const WorldTilePosition world2 = mega2.megaToWorld() + Displacement { 1, 1 }; if (leveltype == DTYPE_CATHEDRAL) { ObjL1Special(world1.x, world1.y, world2.x, world2.y); AddL1Objs(world1.x, world1.y, world2.x, world2.y); } if (leveltype == DTYPE_CATACOMBS) { ObjL2Special(world1.x, world1.y, world2.x, world2.y); AddL2Objs(world1.x, world1.y, world2.x, world2.y); } if (leveltype == DTYPE_CAVES) { AddL3Objs(world1.x, world1.y, world2.x, world2.y); } if (leveltype == DTYPE_CRYPT) { AddCryptObjects(world1.x, world1.y, world2.x, world2.y); } ResyncDoors(world1, world2, true); } void ObjChangeMapResync(int x1, int y1, int x2, int y2) { for (int j = y1; j <= y2; j++) { for (int i = x1; i <= x2; i++) { ObjSetMini({ i, j }, pdungeon[i][j]); dungeon[i][j] = pdungeon[i][j]; } } const WorldTilePosition mega1 { static_cast(x1), static_cast(y1) }; const WorldTilePosition mega2 { static_cast(x2), static_cast(y2) }; const WorldTilePosition world1 = mega1.megaToWorld(); const WorldTilePosition world2 = mega2.megaToWorld() + Displacement { 1, 1 }; if (leveltype == DTYPE_CATHEDRAL) { ObjL1Special(world1.x, world1.y, world2.x, world2.y); } if (leveltype == DTYPE_CATACOMBS) { ObjL2Special(world1.x, world1.y, world2.x, world2.y); } ResyncDoors(world1, world2, false); } _item_indexes ItemMiscIdIdx(item_misc_id imiscid) { std::underlying_type_t<_item_indexes> i = IDI_GOLD; while (AllItemsList[i].dropRate == 0 || AllItemsList[i].iMiscId != imiscid) { i++; } return static_cast<_item_indexes>(i); } void OperateObject(Player &player, Object &object) { const bool sendmsg = &player == MyPlayer; switch (object._otype) { case OBJ_L1LDOOR: case OBJ_L1RDOOR: case OBJ_L2LDOOR: case OBJ_L2RDOOR: case OBJ_L3LDOOR: case OBJ_L3RDOOR: case OBJ_L5LDOOR: case OBJ_L5RDOOR: if (sendmsg) OperateDoor(object, sendmsg); break; case OBJ_LEVER: case OBJ_L5LEVER: case OBJ_SWITCHSKL: OperateLever(object, sendmsg); break; case OBJ_BOOK2L: if (sendmsg) OperateBook(player, object, sendmsg); break; case OBJ_BOOK2R: OperateChamberOfBoneBook(object, sendmsg); break; case OBJ_CHEST1: case OBJ_CHEST2: case OBJ_CHEST3: case OBJ_TCHEST1: case OBJ_TCHEST2: case OBJ_TCHEST3: OperateChest(player, object, sendmsg); break; case OBJ_SARC: case OBJ_L5SARC: OperateSarcophagus(object, sendmsg, sendmsg); break; case OBJ_FLAMELVR: OperateTrapLever(object); break; case OBJ_BLINDBOOK: case OBJ_BLOODBOOK: case OBJ_STEELTOME: if (sendmsg) OperateBookLever(object, sendmsg); break; case OBJ_SHRINEL: case OBJ_SHRINER: OperateShrine(player, object, SfxID::OperateShrine); break; case OBJ_SKELBOOK: case OBJ_BOOKSTAND: OperateBookStand(object, sendmsg, sendmsg); break; case OBJ_BOOKCASEL: case OBJ_BOOKCASER: OperateBookcase(object, sendmsg, sendmsg); break; case OBJ_DECAP: OperateDecapitatedBody(object, sendmsg, sendmsg); break; case OBJ_ARMORSTAND: case OBJ_WARARMOR: OperateArmorStand(object, sendmsg, sendmsg); break; case OBJ_GOATSHRINE: OperateGoatShrine(player, object, SfxID::OperateGoatShrine); break; case OBJ_CAULDRON: OperateCauldron(player, object, SfxID::OperateCaldron); break; case OBJ_BLOODFTN: case OBJ_PURIFYINGFTN: case OBJ_MURKYFTN: case OBJ_TEARFTN: OperateFountains(player, object); break; case OBJ_STORYBOOK: case OBJ_L5BOOKS: if (sendmsg) OperateStoryBook(object); break; case OBJ_PEDESTAL: if (sendmsg) OperatePedestal(player, object, sendmsg); break; case OBJ_WARWEAP: case OBJ_WEAPONRACK: OperateWeaponRack(object, sendmsg, sendmsg); break; case OBJ_MUSHPATCH: OperateMushroomPatch(player, object); break; case OBJ_LAZSTAND: if (sendmsg) OperateLazStand(object); break; case OBJ_SLAINHERO: OperateSlainHero(player, object, sendmsg); break; case OBJ_SIGNCHEST: OperateInnSignChest(player, object, sendmsg); break; default: break; } } void DeltaSyncOpObject(Object &object) { switch (object._otype) { case OBJ_L1LDOOR: case OBJ_L1RDOOR: case OBJ_L2LDOOR: case OBJ_L2RDOOR: case OBJ_L3LDOOR: case OBJ_L3RDOOR: case OBJ_L5LDOOR: case OBJ_L5RDOOR: OpenDoor(object); break; case OBJ_LEVER: case OBJ_L5LEVER: case OBJ_SWITCHSKL: case OBJ_BOOK2L: UpdateLeverState(object); break; case OBJ_CHEST1: case OBJ_CHEST2: case OBJ_CHEST3: case OBJ_TCHEST1: case OBJ_TCHEST2: case OBJ_TCHEST3: case OBJ_SKELBOOK: case OBJ_BOOKSTAND: UpdateState(object, object._oAnimFrame + 2); break; case OBJ_SARC: case OBJ_L5SARC: case OBJ_GOATSHRINE: case OBJ_SHRINEL: case OBJ_SHRINER: UpdateState(object, object._oAnimLen); break; case OBJ_BLINDBOOK: case OBJ_BLOODBOOK: case OBJ_STEELTOME: case OBJ_BOOK2R: object._oAnimFrame = object._oVar6; SyncQSTLever(object); break; case OBJ_BOOKCASEL: case OBJ_BOOKCASER: UpdateState(object, object._oAnimFrame - 2); break; case OBJ_DECAP: case OBJ_MURKYFTN: case OBJ_TEARFTN: case OBJ_SLAINHERO: UpdateState(object, object._oAnimFrame); break; case OBJ_ARMORSTAND: case OBJ_WARARMOR: case OBJ_WARWEAP: case OBJ_WEAPONRACK: case OBJ_LAZSTAND: UpdateState(object, object._oAnimFrame + 1); break; case OBJ_CAULDRON: UpdateState(object, 3); break; case OBJ_STORYBOOK: case OBJ_L5BOOKS: object._oAnimFrame = object._oVar4; break; case OBJ_MUSHPATCH: if (Quests[Q_MUSHROOM]._qvar1 >= QS_MUSHSPAWNED) { UpdateState(object, object._oAnimFrame + 1); } break; case OBJ_SIGNCHEST: if (Quests[Q_LTBANNER]._qvar1 >= 2) { UpdateState(object, object._oAnimFrame + 2); } break; case OBJ_PEDESTAL: UpdatePedestalState(object); break; default: break; } } void DeltaSyncCloseObj(Object &object) { // Object was closed. // That means it was opened once, so all traps have been activated. object._oTrapFlag = false; } void SyncOpObject(Player &player, int cmd, Object &object) { const bool sendmsg = &player == MyPlayer; switch (object._otype) { case OBJ_L1LDOOR: case OBJ_L1RDOOR: case OBJ_L2LDOOR: case OBJ_L2RDOOR: case OBJ_L3LDOOR: case OBJ_L3RDOOR: case OBJ_L5LDOOR: case OBJ_L5RDOOR: if (sendmsg) break; if (cmd == CMD_CLOSEDOOR && object._oVar4 == DOOR_CLOSED) break; if (cmd == CMD_OPENDOOR && object._oVar4 == DOOR_OPEN) break; OperateDoor(object, false); break; case OBJ_LEVER: case OBJ_L5LEVER: case OBJ_SWITCHSKL: OperateLever(object, sendmsg); break; case OBJ_BOOK2L: if (!sendmsg) OperateBook(player, object, sendmsg); break; case OBJ_CHEST1: case OBJ_CHEST2: case OBJ_CHEST3: case OBJ_TCHEST1: case OBJ_TCHEST2: case OBJ_TCHEST3: OperateChest(player, object, false); break; case OBJ_SARC: case OBJ_L5SARC: OperateSarcophagus(object, sendmsg, false); break; case OBJ_BLINDBOOK: case OBJ_BLOODBOOK: case OBJ_STEELTOME: if (sendmsg) break; object._oAnimFrame = object._oVar6; SyncQSTLever(object); break; case OBJ_SHRINEL: case OBJ_SHRINER: OperateShrine(player, object, SfxID::OperateShrine); break; case OBJ_SKELBOOK: case OBJ_BOOKSTAND: OperateBookStand(object, sendmsg, false); break; case OBJ_BOOKCASEL: case OBJ_BOOKCASER: OperateBookcase(object, sendmsg, false); break; case OBJ_DECAP: OperateDecapitatedBody(object, sendmsg, false); break; case OBJ_ARMORSTAND: case OBJ_WARARMOR: OperateArmorStand(object, sendmsg, false); break; case OBJ_GOATSHRINE: OperateGoatShrine(player, object, SfxID::OperateGoatShrine); break; case OBJ_LAZSTAND: if (!sendmsg) UpdateState(object, object._oAnimFrame + 1); break; case OBJ_CAULDRON: OperateCauldron(player, object, SfxID::OperateCaldron); break; case OBJ_MURKYFTN: case OBJ_TEARFTN: OperateFountains(player, object); break; case OBJ_STORYBOOK: case OBJ_L5BOOKS: if (sendmsg) OperateStoryBook(object); break; case OBJ_PEDESTAL: if (!sendmsg) OperatePedestal(player, object, sendmsg); break; case OBJ_WARWEAP: case OBJ_WEAPONRACK: OperateWeaponRack(object, sendmsg, false); break; case OBJ_MUSHPATCH: OperateMushroomPatch(player, object); break; case OBJ_SLAINHERO: OperateSlainHero(player, object, sendmsg); break; case OBJ_SIGNCHEST: OperateInnSignChest(player, object, sendmsg); break; default: break; } } void BreakObjectMissile(const Player *player, Object &object) { if (object.IsCrux()) BreakCrux(object, true); } void BreakObject(const Player &player, Object &object) { if (object.IsBarrel()) { BreakBarrel(player, object, false, true); } else if (object.IsCrux()) { BreakCrux(object, true); } } void DeltaSyncBreakObj(Object &object) { if (!object.IsBreakable() || !object.canInteractWith()) return; object._oMissFlag = true; object._oBreak = -1; object.selectionRegion = SelectionRegion::None; object._oPreFlag = true; object._oAnimFlag = false; object._oAnimFrame = object._oAnimLen; if (object.IsBarrel()) { object._oSolidFlag = false; } else if (object.IsCrux() && AreAllCruxesOfTypeBroken(object._oVar8)) { ObjChangeMap(object._oVar1, object._oVar2, object._oVar3, object._oVar4); } } void SyncBreakObj(const Player &player, Object &object) { if (object.IsBarrel()) { BreakBarrel(player, object, true, false); } else if (object.IsCrux()) { BreakCrux(object, false); } } void SyncObjectAnim(Object &object) { object_graphic_id index = AllObjects[object._otype].ofindex; if (!HeadlessMode) { const auto &found = c_find(ObjFileList, index); if (found == std::end(ObjFileList)) { LogCritical("Unable to find object_graphic_id {} in list of objects to load, level generation error.", static_cast(index)); return; } const size_t i = std::distance(std::begin(ObjFileList), found); if (pObjCels[i]) { object._oAnimData.emplace(*pObjCels[i]); } else { object._oAnimData = std::nullopt; } } switch (object._otype) { case OBJ_L1LDOOR: case OBJ_L1RDOOR: case OBJ_L2LDOOR: case OBJ_L2RDOOR: case OBJ_L3LDOOR: case OBJ_L3RDOOR: case OBJ_L5LDOOR: case OBJ_L5RDOOR: SyncDoor(object); break; case OBJ_CRUX1: case OBJ_CRUX2: case OBJ_CRUX3: SyncCrux(object); break; case OBJ_LEVER: case OBJ_L5LEVER: case OBJ_BOOK2L: case OBJ_SWITCHSKL: SyncLever(object); break; case OBJ_BOOK2R: case OBJ_BLINDBOOK: case OBJ_STEELTOME: SyncQSTLever(object); break; case OBJ_PEDESTAL: SyncPedestal(object); break; default: break; } } StringOrView Object::name() const { switch (_otype) { case OBJ_CRUX1: case OBJ_CRUX2: case OBJ_CRUX3: return _("Crucified Skeleton"); case OBJ_LEVER: case OBJ_L5LEVER: case OBJ_FLAMELVR: return _("Lever"); case OBJ_L1LDOOR: case OBJ_L1RDOOR: case OBJ_L2LDOOR: case OBJ_L2RDOOR: case OBJ_L3LDOOR: case OBJ_L3RDOOR: case OBJ_L5LDOOR: case OBJ_L5RDOOR: if (_oVar4 == DOOR_OPEN) return _("Open Door"); if (_oVar4 == DOOR_CLOSED) return _("Closed Door"); if (_oVar4 == DOOR_BLOCKED) return _("Blocked Door"); break; case OBJ_BOOK2L: if (setlevel) { if (setlvlnum == SL_BONECHAMB) { return _("Ancient Tome"); } else if (setlvlnum == SL_VILEBETRAYER) { return _("Book of Vileness"); } } break; case OBJ_SWITCHSKL: return _("Skull Lever"); case OBJ_BOOK2R: return _("Mythical Book"); case OBJ_CHEST1: case OBJ_TCHEST1: return _("Small Chest"); case OBJ_CHEST2: case OBJ_TCHEST2: return _("Chest"); case OBJ_CHEST3: case OBJ_TCHEST3: case OBJ_SIGNCHEST: return _("Large Chest"); case OBJ_SARC: case OBJ_L5SARC: return _("Sarcophagus"); case OBJ_BOOKSHELF: return _("Bookshelf"); case OBJ_BOOKCASEL: case OBJ_BOOKCASER: return _("Bookcase"); case OBJ_BARREL: case OBJ_BARRELEX: return _("Barrel"); case OBJ_POD: case OBJ_PODEX: return _("Pod"); case OBJ_URN: case OBJ_URNEX: return _("Urn"); case OBJ_SHRINEL: case OBJ_SHRINER: return fmt::format(fmt::runtime(_(/* TRANSLATORS: {:s} will be a name from the Shrine block above */ "{:s} Shrine")), _(ShrineNames[_oVar1])); case OBJ_SKELBOOK: return _("Skeleton Tome"); case OBJ_BOOKSTAND: return _("Library Book"); case OBJ_BLOODFTN: return _("Blood Fountain"); case OBJ_DECAP: return _("Decapitated Body"); case OBJ_BLINDBOOK: return _("Book of the Blind"); case OBJ_BLOODBOOK: return _("Book of Blood"); case OBJ_PURIFYINGFTN: return _("Purifying Spring"); case OBJ_ARMORSTAND: case OBJ_WARARMOR: return _("Armor"); case OBJ_WARWEAP: return _("Weapon Rack"); case OBJ_GOATSHRINE: return _("Goat Shrine"); case OBJ_CAULDRON: return _("Cauldron"); case OBJ_MURKYFTN: return _("Murky Pool"); case OBJ_TEARFTN: return _("Fountain of Tears"); case OBJ_STEELTOME: return _("Steel Tome"); case OBJ_PEDESTAL: return _("Pedestal of Blood"); case OBJ_STORYBOOK: case OBJ_L5BOOKS: return _(StoryBookName[_oVar3]); case OBJ_WEAPONRACK: return _("Weapon Rack"); case OBJ_MUSHPATCH: return _("Mushroom Patch"); case OBJ_LAZSTAND: return _("Vile Stand"); case OBJ_SLAINHERO: return _("Slain Hero"); default: break; } return std::string_view(); } void GetObjectStr(const Object &object) { InfoString = object.name(); if (MyPlayer->_pClass == HeroClass::Rogue) { if (object._oTrapFlag) { InfoString = fmt::format(fmt::runtime(_(/* TRANSLATORS: {:s} will either be a chest or a door */ "Trapped {:s}")), InfoString.str()); InfoColor = UiFlags::ColorRed; } } if (object.IsDisabled()) { InfoString = fmt::format(fmt::runtime(_(/* TRANSLATORS: If user enabled diablo.ini setting "Disable Crippling Shrines" is set to 1; also used for Na-Kruls lever */ "{:s} (disabled)")), InfoString.str()); InfoColor = UiFlags::ColorRed; } } void SyncNakrulRoom() { dPiece[UberRow][UberCol] = 297; dPiece[UberRow][UberCol - 1] = 300; dPiece[UberRow][UberCol - 2] = 299; dPiece[UberRow][UberCol + 1] = 298; } } // namespace devilution