You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
4931 lines
128 KiB
4931 lines
128 KiB
/** |
|
* @file objects.cpp |
|
* |
|
* Implementation of object functionality, interaction, spawning, loading, etc. |
|
*/ |
|
#include <climits> |
|
#include <cmath> |
|
#include <cstdint> |
|
#include <ctime> |
|
#include <string> |
|
|
|
#include <algorithm> |
|
|
|
#include <expected.hpp> |
|
#include <fmt/core.h> |
|
|
|
#include "DiabloUI/ui_flags.hpp" |
|
#include "automap.h" |
|
#include "cursor.h" |
|
#ifdef _DEBUG |
|
#include "debug.h" |
|
#endif |
|
#include "diablo_msg.hpp" |
|
#include "engine/backbuffer_state.hpp" |
|
#include "engine/load_cel.hpp" |
|
#include "engine/load_file.hpp" |
|
#include "engine/points_in_rectangle_range.hpp" |
|
#include "engine/random.hpp" |
|
#include "headless_mode.hpp" |
|
#include "inv.h" |
|
#include "inv_iterators.hpp" |
|
#include "levels/crypt.h" |
|
#include "levels/drlg_l4.h" |
|
#include "levels/setmaps.h" |
|
#include "levels/themes.h" |
|
#include "levels/tile_properties.hpp" |
|
#include "lighting.h" |
|
#include "minitext.h" |
|
#include "missiles.h" |
|
#include "monster.h" |
|
#include "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/endian_swap.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; |
|
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; |
|
AvailableObjects[-ActiveObjectCount + MAXOBJECTS] = oi; |
|
ActiveObjectCount--; |
|
if (ObjectUnderCursor == &object) // Unselect object if this was highlighted by player |
|
ObjectUnderCursor = nullptr; |
|
if (ActiveObjectCount > 0 && i != ActiveObjectCount) |
|
ActiveObjects[i] = ActiveObjects[ActiveObjectCount]; |
|
} |
|
|
|
void AddChest(Object &chest) |
|
{ |
|
if (FlipCoin()) |
|
chest._oAnimFrame += 3; |
|
chest._oRndSeed = AdvanceRndSeed(); |
|
switch (chest._otype) { |
|
case OBJ_CHEST1: |
|
case OBJ_TCHEST1: |
|
if (setlevel) { |
|
chest._oVar1 = 1; |
|
break; |
|
} |
|
chest._oVar1 = GenerateRnd(2); |
|
break; |
|
case OBJ_TCHEST2: |
|
case OBJ_CHEST2: |
|
if (setlevel) { |
|
chest._oVar1 = 2; |
|
break; |
|
} |
|
chest._oVar1 = GenerateRnd(3); |
|
break; |
|
case OBJ_TCHEST3: |
|
case OBJ_CHEST3: |
|
if (setlevel) { |
|
chest._oVar1 = 3; |
|
break; |
|
} |
|
chest._oVar1 = GenerateRnd(4); |
|
break; |
|
default: |
|
break; |
|
} |
|
chest._oVar2 = GenerateRnd(8); |
|
} |
|
|
|
void ObjSetMicro(Point position, int pn) |
|
{ |
|
dPiece[position.x][position.y] = pn; |
|
} |
|
|
|
void DoorSet(Point position, bool isLeftDoor) |
|
{ |
|
const int pn = dPiece[position.x][position.y]; |
|
switch (pn) { |
|
case 42: |
|
ObjSetMicro(position, 391); |
|
break; |
|
case 44: |
|
ObjSetMicro(position, 393); |
|
break; |
|
case 49: |
|
ObjSetMicro(position, isLeftDoor ? 410 : 411); |
|
break; |
|
case 53: |
|
ObjSetMicro(position, 396); |
|
break; |
|
case 54: |
|
ObjSetMicro(position, 397); |
|
break; |
|
case 60: |
|
ObjSetMicro(position, 398); |
|
break; |
|
case 66: |
|
ObjSetMicro(position, 399); |
|
break; |
|
case 67: |
|
ObjSetMicro(position, 400); |
|
break; |
|
case 68: |
|
ObjSetMicro(position, 402); |
|
break; |
|
case 69: |
|
ObjSetMicro(position, 403); |
|
break; |
|
case 71: |
|
ObjSetMicro(position, 405); |
|
break; |
|
case 211: |
|
ObjSetMicro(position, 406); |
|
break; |
|
case 353: |
|
ObjSetMicro(position, 408); |
|
break; |
|
case 354: |
|
ObjSetMicro(position, 409); |
|
break; |
|
case 410: |
|
case 411: |
|
ObjSetMicro(position, 395); |
|
break; |
|
} |
|
} |
|
|
|
void CryptDoorSet(Point position, bool isLeftDoor) |
|
{ |
|
const int pn = dPiece[position.x][position.y]; |
|
switch (pn) { |
|
case 74: |
|
ObjSetMicro(position, 203); |
|
break; |
|
case 78: |
|
ObjSetMicro(position, 207); |
|
break; |
|
case 85: |
|
ObjSetMicro(position, isLeftDoor ? 231 : 233); |
|
break; |
|
case 90: |
|
ObjSetMicro(position, 214); |
|
break; |
|
case 92: |
|
ObjSetMicro(position, 217); |
|
break; |
|
case 98: |
|
ObjSetMicro(position, 219); |
|
break; |
|
case 110: |
|
ObjSetMicro(position, 221); |
|
break; |
|
case 112: |
|
ObjSetMicro(position, 223); |
|
break; |
|
case 114: |
|
ObjSetMicro(position, 225); |
|
break; |
|
case 116: |
|
ObjSetMicro(position, 227); |
|
break; |
|
case 118: |
|
ObjSetMicro(position, 229); |
|
break; |
|
case 231: |
|
case 233: |
|
ObjSetMicro(position, 211); |
|
break; |
|
} |
|
} |
|
|
|
void SetDoorStateOpen(Object &door) |
|
{ |
|
door._oVar4 = DOOR_OPEN; |
|
door._oPreFlag = true; |
|
door._oMissFlag = true; |
|
door.selectionRegion = SelectionRegion::Middle; |
|
|
|
switch (door._otype) { |
|
case OBJ_L1LDOOR: |
|
// 214: blood splater |
|
// 407: blood pool |
|
// 392: open door (no frame) |
|
ObjSetMicro(door.position, door._oVar1 == 214 ? 407 : 392); |
|
dSpecial[door.position.x][door.position.y] = 7; |
|
DoorSet(door.position + Direction::NorthEast, true); |
|
break; |
|
case OBJ_L1RDOOR: |
|
ObjSetMicro(door.position, 394); |
|
dSpecial[door.position.x][door.position.y] = 8; |
|
DoorSet(door.position + Direction::NorthWest, false); |
|
break; |
|
case OBJ_L2LDOOR: |
|
ObjSetMicro(door.position, 12); |
|
dSpecial[door.position.x][door.position.y] = 5; |
|
break; |
|
case OBJ_L2RDOOR: |
|
ObjSetMicro(door.position, 16); |
|
dSpecial[door.position.x][door.position.y] = 6; |
|
break; |
|
case OBJ_L3LDOOR: |
|
ObjSetMicro(door.position, 537); |
|
break; |
|
case OBJ_L3RDOOR: |
|
ObjSetMicro(door.position, 540); |
|
break; |
|
case OBJ_L5LDOOR: |
|
ObjSetMicro(door.position, 205); |
|
CryptDoorSet(door.position + Direction::NorthEast, true); |
|
break; |
|
case OBJ_L5RDOOR: |
|
ObjSetMicro(door.position, 208); |
|
CryptDoorSet(door.position + Direction::NorthWest, false); |
|
break; |
|
default: |
|
break; |
|
} |
|
} |
|
|
|
void SetDoorStateClosed(Object &door) |
|
{ |
|
door._oVar4 = DOOR_CLOSED; |
|
door._oPreFlag = false; |
|
door._oMissFlag = false; |
|
door.selectionRegion = SelectionRegion::Bottom | SelectionRegion::Middle; |
|
|
|
switch (door._otype) { |
|
case OBJ_L1LDOOR: { |
|
// Clear overlapping arches |
|
dSpecial[door.position.x][door.position.y] = 0; |
|
ObjSetMicro(door.position, door._oVar1 - 1); |
|
|
|
// Restore the normal tile where the open door used to be |
|
auto openPosition = door.position + Direction::NorthEast; |
|
if (door._oVar2 == 50 && dPiece[openPosition.x][openPosition.y] == 395) |
|
ObjSetMicro(openPosition, 411); |
|
else |
|
ObjSetMicro(openPosition, door._oVar2 - 1); |
|
break; |
|
} break; |
|
case OBJ_L1RDOOR: { |
|
// Clear overlapping arches |
|
dSpecial[door.position.x][door.position.y] = 0; |
|
ObjSetMicro(door.position, door._oVar1 - 1); |
|
|
|
// Restore the normal tile where the open door used to be |
|
auto openPosition = door.position + Direction::NorthWest; |
|
if (door._oVar2 == 50 && dPiece[openPosition.x][openPosition.y] == 395) |
|
ObjSetMicro(openPosition, 410); |
|
else |
|
ObjSetMicro(openPosition, door._oVar2 - 1); |
|
break; |
|
} break; |
|
case OBJ_L2LDOOR: |
|
// Clear overlapping arches |
|
dSpecial[door.position.x][door.position.y] = 0; |
|
ObjSetMicro(door.position, 537); |
|
break; |
|
case OBJ_L2RDOOR: |
|
// Clear overlapping arches |
|
dSpecial[door.position.x][door.position.y] = 0; |
|
ObjSetMicro(door.position, 539); |
|
break; |
|
case OBJ_L3LDOOR: |
|
ObjSetMicro(door.position, 530); |
|
break; |
|
case OBJ_L3RDOOR: |
|
ObjSetMicro(door.position, 533); |
|
break; |
|
case OBJ_L5LDOOR: { |
|
ObjSetMicro(door.position, door._oVar1 - 1); |
|
|
|
// Restore the normal tile where the open door used to be |
|
auto openPosition = door.position + Direction::NorthEast; |
|
if (door._oVar2 == 86 && dPiece[openPosition.x][openPosition.y] == 209) |
|
ObjSetMicro(openPosition, 233); |
|
else |
|
ObjSetMicro(openPosition, door._oVar2 - 1); |
|
} break; |
|
case OBJ_L5RDOOR: { |
|
ObjSetMicro(door.position, door._oVar1 - 1); |
|
|
|
// Restore the normal tile where the open door used to be |
|
auto openPosition = door.position + Direction::NorthWest; |
|
if (door._oVar2 == 86 && dPiece[openPosition.x][openPosition.y] == 209) |
|
ObjSetMicro(openPosition, 231); |
|
else |
|
ObjSetMicro(openPosition, door._oVar2 - 1); |
|
} break; |
|
default: |
|
break; |
|
} |
|
} |
|
|
|
void AddDoor(Object &door) |
|
{ |
|
door._oDoorFlag = true; |
|
|
|
switch (door._otype) { |
|
case OBJ_L1LDOOR: |
|
case OBJ_L5LDOOR: |
|
door._oVar1 = dPiece[door.position.x][door.position.y] + 1; |
|
door._oVar2 = dPiece[door.position.x][door.position.y - 1] + 1; |
|
break; |
|
case OBJ_L1RDOOR: |
|
case OBJ_L5RDOOR: |
|
door._oVar1 = dPiece[door.position.x][door.position.y] + 1; |
|
door._oVar2 = dPiece[door.position.x - 1][door.position.y] + 1; |
|
break; |
|
default: |
|
break; |
|
} |
|
|
|
SetDoorStateClosed(door); |
|
} |
|
|
|
void AddSarcophagus(Object &sarcophagus) |
|
{ |
|
dObject[sarcophagus.position.x][sarcophagus.position.y - 1] = -(static_cast<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; |
|
} |
|
} |
|
|
|
int ClampU8(int v) |
|
{ |
|
return std::clamp(v, 0, 255); |
|
} |
|
|
|
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; |
|
|
|
for (Item &item : PlayerItemsRange(player)) { |
|
switch (item._itype) { |
|
case ItemType::Sword: |
|
case ItemType::Axe: |
|
case ItemType::Bow: |
|
case ItemType::Mace: |
|
case ItemType::Staff: { |
|
const int minDam = static_cast<int>(item._iMinDam); |
|
const int maxDam = static_cast<int>(item._iMaxDam); |
|
const int newMax = std::max(maxDam - 1, minDam); |
|
item._iMaxDam = static_cast<uint8_t>(ClampU8(newMax)); |
|
} break; |
|
case ItemType::Shield: |
|
case ItemType::Helm: |
|
case ItemType::LightArmor: |
|
case ItemType::MediumArmor: |
|
case ItemType::HeavyArmor: |
|
item._iAC = ClampU8(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 (uint8_t j = static_cast<uint8_t>(SpellID::Firebolt); j < SpellsData.size(); j++) { |
|
if ((player._pMemSpells & spell) != 0 && player._pSplLvl[j] < MaxSpellLevel && j != spellToReduce) { |
|
const uint8_t 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 uint8_t 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; |
|
crux._oMissFlag = true; |
|
crux._oBreak = -1; |
|
crux.selectionRegion = SelectionRegion::None; |
|
|
|
if (sendmsg) |
|
NetSendCmdLoc(MyPlayerId, false, CMD_BREAKOBJ, crux.position); |
|
|
|
if (!AreAllCruxesOfTypeBroken(crux._oVar8)) |
|
return; |
|
|
|
PlaySfxLoc(SfxID::OperateLever, crux.position); |
|
ObjChangeMap(crux._oVar1, crux._oVar2, crux._oVar3, crux._oVar4); |
|
} |
|
|
|
void BreakBarrel(const Player &player, Object &barrel, bool forcebreak, bool sendmsg) |
|
{ |
|
if (!barrel.canInteractWith()) |
|
return; |
|
if (!forcebreak && &player != MyPlayer) { |
|
return; |
|
} |
|
|
|
barrel._oAnimFlag = true; |
|
barrel._oAnimFrame = 1; |
|
barrel._oAnimDelay = 1; |
|
barrel._oSolidFlag = false; |
|
barrel._oMissFlag = true; |
|
barrel._oBreak = -1; |
|
barrel.selectionRegion = SelectionRegion::None; |
|
barrel._oPreFlag = true; |
|
|
|
if (barrel.isExplosive()) { |
|
if (barrel._otype == _object_id::OBJ_URNEX) |
|
PlaySfxLoc(SfxID::UrnExpload, barrel.position); |
|
else if (barrel._otype == _object_id::OBJ_PODEX) |
|
PlaySfxLoc(SfxID::PodExpload, barrel.position); |
|
else |
|
PlaySfxLoc(SfxID::BarrelExpload, barrel.position); |
|
for (int yp = barrel.position.y - 1; yp <= barrel.position.y + 1; yp++) { |
|
for (int xp = barrel.position.x - 1; xp <= barrel.position.x + 1; xp++) { |
|
constexpr MissileID TrapMissile = MissileID::Firebolt; |
|
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; |
|
} |
|
|
|
tl::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])); |
|
numobjfiles++; |
|
} |
|
return {}; |
|
} |
|
|
|
tl::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(); |
|
} |
|
} |
|
|
|
void SetMapObjects(const uint16_t *dunData, int startx, int starty) |
|
{ |
|
uint16_t filesWidths[65] = {}; |
|
|
|
ClrAllObjects(); |
|
|
|
WorldTileSize size = GetDunSize(dunData); |
|
|
|
const int layer2Offset = 2 + size.width * size.height; |
|
|
|
// The rest of the layers are at dPiece scale |
|
size *= static_cast<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; |
|
} |
|
} |
|
} |
|
|
|
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 }); |
|
} |
|
} |
|
} |
|
} |
|
|
|
Object *AddObject(_object_id objType, Point objPos) |
|
{ |
|
if (ActiveObjectCount >= MAXOBJECTS) |
|
return nullptr; |
|
|
|
const int oi = AvailableObjects[0]; |
|
AvailableObjects[0] = AvailableObjects[MAXOBJECTS - 1 - ActiveObjectCount]; |
|
ActiveObjects[ActiveObjectCount] = oi; |
|
dObject[objPos.x][objPos.y] = oi + 1; |
|
Object &object = Objects[oi]; |
|
SetupObject(object, objPos, objType); |
|
switch (object._otype) { |
|
case OBJ_L1LDOOR: |
|
case OBJ_L1RDOOR: |
|
case OBJ_L2LDOOR: |
|
case OBJ_L2RDOOR: |
|
case OBJ_L3LDOOR: |
|
case OBJ_L3RDOOR: |
|
case OBJ_L5LDOOR: |
|
case OBJ_L5RDOOR: |
|
AddDoor(object); |
|
break; |
|
case OBJ_BOOK2R: |
|
object.InitializeBook({ SetPiece.position, WorldTileSize(SetPiece.size.width + 1, SetPiece.size.height + 1) }); |
|
break; |
|
case OBJ_CHEST1: |
|
case OBJ_CHEST2: |
|
case OBJ_CHEST3: |
|
AddChest(object); |
|
break; |
|
case OBJ_TCHEST1: |
|
case OBJ_TCHEST2: |
|
case OBJ_TCHEST3: |
|
AddChest(object); |
|
object._oTrapFlag = true; |
|
if (leveltype == DTYPE_CATACOMBS) { |
|
object._oVar4 = GenerateRnd(2); |
|
} else { |
|
object._oVar4 = GenerateRnd(3); |
|
} |
|
break; |
|
case OBJ_SARC: |
|
case OBJ_L5SARC: |
|
AddSarcophagus(object); |
|
break; |
|
case OBJ_FLAMEHOLE: |
|
AddFlameTrap(object); |
|
break; |
|
case OBJ_FLAMELVR: |
|
AddFlameLever(object); |
|
break; |
|
case OBJ_WATER: |
|
object._oAnimFrame = 1; |
|
break; |
|
case OBJ_TRAPL: |
|
case OBJ_TRAPR: |
|
AddTrap(object); |
|
break; |
|
case OBJ_BARREL: |
|
case OBJ_BARRELEX: |
|
case OBJ_POD: |
|
case OBJ_PODEX: |
|
case OBJ_URN: |
|
case OBJ_URNEX: |
|
AddBarrel(object); |
|
break; |
|
case OBJ_SHRINEL: |
|
case OBJ_SHRINER: |
|
AddShrine(object); |
|
break; |
|
case OBJ_BOOKCASEL: |
|
case OBJ_BOOKCASER: |
|
AddBookcase(object); |
|
break; |
|
case OBJ_SKELBOOK: |
|
case OBJ_BOOKSTAND: |
|
case OBJ_BLOODFTN: |
|
case OBJ_GOATSHRINE: |
|
case OBJ_CAULDRON: |
|
case OBJ_TEARFTN: |
|
case OBJ_SLAINHERO: |
|
object._oRndSeed = AdvanceRndSeed(); |
|
break; |
|
case OBJ_DECAP: |
|
AddDecapitatedBody(object); |
|
break; |
|
case OBJ_PURIFYINGFTN: |
|
case OBJ_MURKYFTN: |
|
AddLargeFountain(object); |
|
break; |
|
case OBJ_ARMORSTAND: |
|
case OBJ_WARARMOR: |
|
AddArmorStand(object); |
|
break; |
|
case OBJ_BOOK2L: |
|
AddBookOfVileness(object); |
|
break; |
|
case OBJ_MCIRCLE1: |
|
case OBJ_MCIRCLE2: |
|
AddMagicCircle(object); |
|
break; |
|
case OBJ_STORYBOOK: |
|
case OBJ_L5BOOKS: |
|
AddStoryBook(object); |
|
break; |
|
case OBJ_BCROSS: |
|
case OBJ_TBCROSS: |
|
object._oRndSeed = AdvanceRndSeed(); |
|
break; |
|
case OBJ_PEDESTAL: |
|
AddPedestalOfBlood(object); |
|
break; |
|
case OBJ_WARWEAP: |
|
case OBJ_WEAPONRACK: |
|
AddWeaponRack(object); |
|
break; |
|
case OBJ_TNUDEM2: |
|
AddTorturedBody(object); |
|
break; |
|
default: |
|
break; |
|
} |
|
|
|
AddObjectLight(object); |
|
|
|
ActiveObjectCount++; |
|
return &object; |
|
} |
|
|
|
bool UpdateTrapState(Object &trap) |
|
{ |
|
if (trap._oVar4 != 0) |
|
return false; |
|
|
|
Object &trigger = ObjectAtPosition({ trap._oVar1, trap._oVar2 }); |
|
switch (trigger._otype) { |
|
case OBJ_L1LDOOR: |
|
case OBJ_L1RDOOR: |
|
case OBJ_L2LDOOR: |
|
case OBJ_L2RDOOR: |
|
case OBJ_L3LDOOR: |
|
case OBJ_L3RDOOR: |
|
case OBJ_L5LDOOR: |
|
case OBJ_L5RDOOR: |
|
if (trigger._oVar4 == DOOR_CLOSED && trigger._oTrapFlag) |
|
return false; |
|
break; |
|
case OBJ_LEVER: |
|
case OBJ_CHEST1: |
|
case OBJ_CHEST2: |
|
case OBJ_CHEST3: |
|
case OBJ_SWITCHSKL: |
|
case OBJ_SARC: |
|
case OBJ_L5LEVER: |
|
case OBJ_L5SARC: |
|
if (trigger.canInteractWith() && trigger._oTrapFlag) |
|
return false; |
|
break; |
|
default: |
|
return false; |
|
} |
|
|
|
trap._oVar4 = 1; |
|
trigger._oTrapFlag = false; |
|
return true; |
|
} |
|
|
|
void OperateTrap(Object &trap) |
|
{ |
|
if (!UpdateTrapState(trap)) |
|
return; |
|
|
|
// default to firing at the trigger object |
|
const Point triggerPosition = { trap._oVar1, trap._oVar2 }; |
|
Point target = triggerPosition; |
|
|
|
auto searchArea = PointsInRectangle(Rectangle { target, 1 }); |
|
// look for a player near the trigger (using a reverse search to match vanilla behaviour) |
|
auto foundPosition = std::find_if(searchArea.crbegin(), searchArea.crend(), [](Point testPosition) { return InDungeonBounds(testPosition) && dPlayer[testPosition.x][testPosition.y] != 0; }); |
|
if (foundPosition != searchArea.crend()) { |
|
// if a player is standing near the trigger then target them instead |
|
target = *foundPosition; |
|
} |
|
|
|
const Direction dir = GetDirection(trap.position, target); |
|
AddMissile(trap.position, target, dir, static_cast<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(const Player *player, Object &object) |
|
{ |
|
if (object.IsCrux()) |
|
BreakCrux(object, true); |
|
} |
|
void BreakObject(const Player &player, Object &object) |
|
{ |
|
if (object.IsBarrel()) { |
|
BreakBarrel(player, object, false, true); |
|
} else if (object.IsCrux()) { |
|
BreakCrux(object, true); |
|
} |
|
} |
|
|
|
void DeltaSyncBreakObj(Object &object) |
|
{ |
|
if (!object.IsBreakable() || !object.canInteractWith()) |
|
return; |
|
|
|
object._oMissFlag = true; |
|
object._oBreak = -1; |
|
object.selectionRegion = SelectionRegion::None; |
|
object._oPreFlag = true; |
|
object._oAnimFlag = false; |
|
object._oAnimFrame = object._oAnimLen; |
|
|
|
if (object.IsBarrel()) { |
|
object._oSolidFlag = false; |
|
} else if (object.IsCrux() && AreAllCruxesOfTypeBroken(object._oVar8)) { |
|
ObjChangeMap(object._oVar1, object._oVar2, object._oVar3, object._oVar4); |
|
} |
|
} |
|
|
|
void SyncBreakObj(const Player &player, Object &object) |
|
{ |
|
if (object.IsBarrel()) { |
|
BreakBarrel(player, object, true, false); |
|
} else if (object.IsCrux()) { |
|
BreakCrux(object, false); |
|
} |
|
} |
|
|
|
void SyncObjectAnim(Object &object) |
|
{ |
|
object_graphic_id index = AllObjects[object._otype].ofindex; |
|
|
|
if (!HeadlessMode) { |
|
const auto &found = c_find(ObjFileList, index); |
|
if (found == std::end(ObjFileList)) { |
|
LogCritical("Unable to find object_graphic_id {} in list of objects to load, level generation error.", static_cast<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 fmt::format(fmt::runtime(_(/* TRANSLATORS: {:s} will be a name from the Shrine block above */ "{:s} Shrine")), _(ShrineNames[_oVar1])); |
|
case OBJ_SKELBOOK: |
|
return _("Skeleton Tome"); |
|
case OBJ_BOOKSTAND: |
|
return _("Library Book"); |
|
case OBJ_BLOODFTN: |
|
return _("Blood Fountain"); |
|
case OBJ_DECAP: |
|
return _("Decapitated Body"); |
|
case OBJ_BLINDBOOK: |
|
return _("Book of the Blind"); |
|
case OBJ_BLOODBOOK: |
|
return _("Book of Blood"); |
|
case OBJ_PURIFYINGFTN: |
|
return _("Purifying Spring"); |
|
case OBJ_ARMORSTAND: |
|
case OBJ_WARARMOR: |
|
return _("Armor"); |
|
case OBJ_WARWEAP: |
|
return _("Weapon Rack"); |
|
case OBJ_GOATSHRINE: |
|
return _("Goat Shrine"); |
|
case OBJ_CAULDRON: |
|
return _("Cauldron"); |
|
case OBJ_MURKYFTN: |
|
return _("Murky Pool"); |
|
case OBJ_TEARFTN: |
|
return _("Fountain of Tears"); |
|
case OBJ_STEELTOME: |
|
return _("Steel Tome"); |
|
case OBJ_PEDESTAL: |
|
return _("Pedestal of Blood"); |
|
case OBJ_STORYBOOK: |
|
case OBJ_L5BOOKS: |
|
return _(StoryBookName[_oVar3]); |
|
case OBJ_WEAPONRACK: |
|
return _("Weapon Rack"); |
|
case OBJ_MUSHPATCH: |
|
return _("Mushroom Patch"); |
|
case OBJ_LAZSTAND: |
|
return _("Vile Stand"); |
|
case OBJ_SLAINHERO: |
|
return _("Slain Hero"); |
|
default: |
|
break; |
|
} |
|
return std::string_view(); |
|
} |
|
|
|
void GetObjectStr(const Object &object) |
|
{ |
|
InfoString = object.name(); |
|
const ClassAttributes &classAttributes = GetClassAttributes(MyPlayer->_pClass); |
|
if (HasAnyOf(classAttributes.classFlags, PlayerClassFlag::TrapSense)) { |
|
if (object._oTrapFlag) { |
|
InfoString = fmt::format(fmt::runtime(_(/* TRANSLATORS: {:s} will either be a chest or a door */ "Trapped {:s}")), InfoString.str()); |
|
InfoColor = UiFlags::ColorRed; |
|
} |
|
} |
|
if (object.IsDisabled()) { |
|
InfoString = fmt::format(fmt::runtime(_(/* TRANSLATORS: If user enabled diablo.ini setting "Disable Crippling Shrines" is set to 1; also used for Na-Kruls lever */ "{:s} (disabled)")), InfoString.str()); |
|
InfoColor = UiFlags::ColorRed; |
|
} |
|
} |
|
|
|
void SyncNakrulRoom() |
|
{ |
|
dPiece[UberRow][UberCol] = 297; |
|
dPiece[UberRow][UberCol - 1] = 300; |
|
dPiece[UberRow][UberCol - 2] = 299; |
|
dPiece[UberRow][UberCol + 1] = 298; |
|
} |
|
|
|
} // namespace devilution
|
|
|