Files
DevilutionX/Source/objects.cpp
T
Gleb MazovetskiyandClaude Opus 5.5 debe33ca7b Extract baked sprite shadows and draw them separately
The original sprites have their shadow drawn into them as black pixels.
All of these shadows were made by one function: the sprite squashed to
a third of its height and sheared to the left, measured from a baseline
row B:

    v  = (B - y) / 3
    ys = B - 2 - v
    xs = x - (3 * v) / 4

Only the choice of baseline differs between sprites. It is now recorded
per sprite in a new `shadowRule` column of the monster, object, towner
and class sprite data files:

 * `FirstFrame`: the lowest opaque row of the first frame
 * `EachFrame`: the lowest opaque row of each frame
 * `FirstFrame+N`: the same plus N rows, for flying monsters
 * `None`: no shadow
 * `Baked`: a shadow that cannot be told apart from black parts of the
   sprite, so it is left in the sprite

A rule can be followed by `|Unexplained` and `|Stray` for the few
sprites that the function does not reproduce exactly. The rule is empty
for the Hellfire bard and barbarian, whose sprites were not available.

When sprites are loaded, `ExtractClxShadows` removes the shadow from
each sprite and stores it as a mask of horizontal spans in the CLX
frame header, so it is available wherever the sprite is. The shadow is
drawn right before its sprite, which gives the same picture as before.

A new "Transparent Shadows" graphics option blends the shadows with
what is underneath instead of covering it. It is off by default.
In debug builds, `dev.display.shadows()` toggles drawing them.

Transparent shadows are not perfect. A black pixel of a sprite that
lies inside the sprite's own shadow looks exactly like shadow, so it is
extracted with it and becomes see-through. For example, the warrior in
light armor with a sword and shield, facing southwest, is missing a few
pixels from his left knee. Getting this perfect will likely require
overriding the shadow masks of a handful of sprites, which is something
that can be done with AI in the future.

The rules were found and checked with `tools/shadow_search`, against
all player, monster, towner and object sprites of Diablo and Hellfire:
139,943 frames in 1,438 files. Of the 136,918 frames that have an
extractable shadow, 99.77% are reproduced pixel for pixel.

Extraction adds about 14 us per frame when a sprite is loaded
(about 14 ms for all animations of a character). For the original
player sprites, the sprites with their shadow masks take 0.3% less
memory than before.

Drawing a sprite with its shadow, per sprite, for all sprites of each
kind (`shadow_extract_check <mpq> render`, release build, fully visible
sprites, Ryzen AI Max+ 395):

                          Players    Monsters   Monk
    Without lighting
      Before              2,111 ns   2,444 ns   2,109 ns
      Opaque shadow       -9.5%      -10.1%     -6.1%
      Transparent shadow  +10.0%     +7.5%      +9.0%
    With a light table
      Before              2,659 ns   2,983 ns   2,548 ns
      Opaque shadow       -5.2%      -4.5%      -2.3%
      Transparent shadow  +10.9%     +10.2%     +10.4%

Opaque shadows are a little faster to draw than before: a shadow is now
a short list of spans instead of many commands in the sprite. Blending
a shadow costs about as much again as that saves. Either way the frame
rate is practically unchanged: with about 10 such sprites per frame,
it comes to about +0.2% FPS with opaque shadows and -0.3% with
transparent ones in an unthrottled demo playback, where the 3 modes
measured within noise of each other.

Selection outlines can differ by a pixel where a shadow touches the
sprite: their shape used to depend on how the black pixels of the
shadow were encoded.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-11 21:18:52 +01:00

4942 lines
128 KiB
C++

/**
* @file objects.cpp
*
* Implementation of object functionality, interaction, spawning, loading, etc.
*/
#include <climits>
#include <cmath>
#include <cstdint>
#include <ctime>
#include <expected>
#include <string>
#include <algorithm>
#include "DiabloUI/ui_flags.hpp"
#include "automap.h"
#include "cursor.h"
#include "cursor_defs.hpp"
#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 "options.h"
#include "qol/stash.h"
#include "stores.h"
#include "tables/objdat.h"
#include "towners.h"
#include "track.h"
#include "utils/algorithm/container.hpp"
#include "utils/clx_shadow.hpp"
#include "utils/endian_swap.hpp"
#include "utils/format.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;
int NaKrulTomeSequence;
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;
/** 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<Point> 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<Point> 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<Point> 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<uint16_t>(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<WorldTileCoord>(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<uint8_t>(Swap16LE(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;
default:
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;
default:
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;
default:
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<int>(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;
MarkWalkabilityChanged();
Object &object = Objects[oi];
SetupObject(object, position, ot);
AddCryptObject(object, v2);
ActiveObjectCount++;
}
void AddCryptStoryBook(int s)
{
std::optional<Point> 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<Point> 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;
MarkWalkabilityChanged();
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;
MarkWalkabilityChanged();
}
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<int8_t>(sarcophagus.GetId()) + 1);
sarcophagus._oVar1 = GenerateRnd(10);
sarcophagus._oRndSeed = AdvanceRndSeed();
if (sarcophagus._oVar1 >= 8) {
Monster *monster = PreSpawnSkeleton();
if (monster != nullptr) {
sarcophagus._oVar2 = static_cast<int>(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<int8_t>(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<int8_t>(MissileID::Arrow);
if (missileType == 1)
trap._oVar3 = static_cast<int8_t>(MissileID::Firebolt);
if (missileType == 2)
trap._oVar3 = static_cast<int8_t>(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<int>(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<int8_t>(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;
constexpr MissileID TrapMissile = MissileID::FireWallControl;
Monster *monster = FindMonsterAtPosition(trap.position, true);
if (monster != nullptr)
MonsterTrapHit(*monster, mindam / 2, maxdam / 2, 0, TrapMissile, GetMissileData(TrapMissile).damageType(), false);
Player *player = PlayerAtPosition(trap.position, 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.hasNoLife()) {
myPlayer.Say(HeroSpeech::Argh);
}
}
void ObjSetMini(Point position, int v)
{
const MegaTile mega = pMegaTiles[v - 1];
const Point megaOrigin = position.megaToWorld();
ObjSetMicro(megaOrigin, Swap16LE(mega.micro1));
ObjSetMicro(megaOrigin + Direction::SouthEast, Swap16LE(mega.micro2));
ObjSetMicro(megaOrigin + Direction::SouthWest, Swap16LE(mega.micro3));
ObjSetMicro(megaOrigin + Direction::South, Swap16LE(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<int8_t>(SpellID::Guardian)] + 1;
if (newSpellLevel <= MaxSpellLevel) {
player._pSplLvl[static_cast<int8_t>(SpellID::Guardian)] = newSpellLevel;
NetSendCmdParam2(true, CMD_CHANGE_SPELL_LEVEL, static_cast<uint16_t>(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;
default:
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<CharacterAttribute>(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<int32_t>(SpellsData.size())) + 1;
} while ((player._pMemSpells & GetSpellBitmask(static_cast<SpellID>(spellToReduce))) == 0);
spell = 1;
for (auto j = static_cast<uint8_t>(SpellID::Firebolt); j < SpellsData.size(); j++) {
if ((player._pMemSpells & spell) != 0 && player._pSplLvl[j] < MaxSpellLevel && j != spellToReduce) {
const auto newSpellLevel = static_cast<uint8_t>(player._pSplLvl[j] + 1);
player._pSplLvl[j] = newSpellLevel;
NetSendCmdParam2(true, CMD_CHANGE_SPELL_LEVEL, j, newSpellLevel);
}
spell *= 2;
}
if (player._pSplLvl[spellToReduce] > 0) {
const auto newSpellLevel = static_cast<uint8_t>(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<int8_t>(spellId)];
if (curSpellLevel < MaxSpellLevel) {
const uint8_t newSpellLevel = std::min(static_cast<uint8_t>(curSpellLevel + 2), MaxSpellLevel);
player._pSplLvl[static_cast<int8_t>(spellId)] = newSpellLevel;
NetSendCmdParam2(true, CMD_CHANGE_SPELL_LEVEL, static_cast<uint16_t>(spellId), newSpellLevel);
}
if (&player == MyPlayer) {
for (Item &item : InventoryPlayerItemsRange { player }) {
item.updateRequiredStatsCacheForPlayer(player);
}
if (IsStashOpen) {
Stash.RefreshItemStatFlags();
}
}
int maxBase = player._pMaxManaBase;
if (maxBase < 0) {
// Fix bugged state; do not turn this into a "negative penalty" mana boost.
player._pMaxManaBase = 0;
maxBase = 0;
}
const int penalty = maxBase / 10; // 10% of max base mana (>= 0)
player._pMaxManaBase -= penalty; // will remain >= 0
player._pManaBase -= penalty; // may go negative, allowed
player._pMaxMana -= penalty; // may go negative, allowed
player._pMana -= penalty; // may go negative, allowed
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;
default:
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<int>(std::min<uint32_t>(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<uint32_t>(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<unsigned, int> 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_DONE) {
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;
MarkWalkabilityChanged();
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;
MarkWalkabilityChanged();
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;
Monster *monster = FindMonsterAtPosition({ xp, yp }, true);
if (monster != nullptr) {
MonsterTrapHit(*monster, 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<WorldTileCoord>(p2.x - p1.x), static_cast<WorldTileCoord>(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<shrine_type>(_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;
}
std::expected<void, std::string> 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<object_graphic_id>(i);
char filestr[32];
*BufCopy(filestr, "objects\\", ObjMasterLoadList[i]) = '\0';
ASSIGN_OR_RETURN(pObjCels[numobjfiles], LoadCelWithStatus(filestr, filesWidths[i]));
// Objects that share a sprite file share its shadow rule.
const auto objectDataIt = std::find_if(AllObjects.begin(), AllObjects.end(), [i](const ObjectData &objectData) { return objectData.ofindex == i; });
if (objectDataIt != AllObjects.end())
ExtractClxShadowsInPlace(*pObjCels[numobjfiles], objectDataIt->shadowRule);
numobjfiles++;
}
return {};
}
std::expected<void, std::string> 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;
default:
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;
default:
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;
default:
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();
}
}
std::expected<void, std::string> 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<WorldTileCoord>(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<uint8_t>(Swap16LE(objectLayer[(j * size.width) + i]));
if (objectId != 0) {
const ObjectData &objectData = AllObjects[ObjTypeConv[objectId]];
filesWidths[objectData.ofindex] = objectData.animWidth;
}
}
}
RETURN_IF_ERROR(LoadLevelObjects(filesWidths));
for (WorldTileCoord j = 0; j < size.height; j++) {
for (WorldTileCoord i = 0; i < size.width; i++) {
auto objectId = static_cast<uint8_t>(Swap16LE(objectLayer[(j * size.width) + i]));
if (objectId != 0) {
AddObject(ObjTypeConv[objectId], { startx + 16 + i, starty + 16 + j });
}
}
}
return {};
}
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;
MarkWalkabilityChanged();
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<MissileID>(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<WorldTileCoord>(x1), static_cast<WorldTileCoord>(y1) };
const WorldTilePosition mega2 { static_cast<WorldTileCoord>(x2), static_cast<WorldTileCoord>(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<WorldTileCoord>(x1), static_cast<WorldTileCoord>(y1) };
const WorldTilePosition mega2 { static_cast<WorldTileCoord>(x2), static_cast<WorldTileCoord>(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(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;
MarkWalkabilityChanged();
} 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<int>(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 FormatRuntime(_(/* 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();
const ClassAttributes &classAttributes = GetClassAttributes(MyPlayer->_pClass);
if (HasAnyOf(classAttributes.classFlags, PlayerClassFlag::TrapSense)) {
if (object._oTrapFlag) {
InfoString = FormatRuntime(_(/* TRANSLATORS: {:s} will either be a chest or a door */ "Trapped {:s}"), InfoString.str());
InfoColor = UiFlags::ColorRed;
}
}
if (object.IsDisabled()) {
InfoString = FormatRuntime(_(/* 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;
MarkWalkabilityChanged();
}
} // namespace devilution