mirror of
https://github.com/diasurgical/DevilutionX.git
synced 2026-05-21 05:40:35 +00:00
5069 lines
148 KiB
C++
5069 lines
148 KiB
C++
/**
|
|
* @file items.cpp
|
|
*
|
|
* Implementation of item functionality.
|
|
*/
|
|
#include "items.h"
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cassert>
|
|
#include <climits>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <cstring>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <type_traits>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#ifdef USE_SDL3
|
|
#include <SDL3/SDL_timer.h>
|
|
#else
|
|
#include <SDL.h>
|
|
#endif
|
|
|
|
#include <fmt/core.h>
|
|
|
|
#include "DiabloUI/ui_flags.hpp"
|
|
#include "control/control.hpp"
|
|
#include "controls/control_mode.hpp"
|
|
#include "controls/controller_buttons.h"
|
|
#include "cursor.h"
|
|
#include "diablo.h"
|
|
#include "doom.h"
|
|
#include "effects.h"
|
|
#include "engine/animationinfo.h"
|
|
#include "engine/backbuffer_state.hpp"
|
|
#include "engine/clx_sprite.hpp"
|
|
#include "engine/load_cel.hpp"
|
|
#include "engine/path.h"
|
|
#include "engine/point.hpp"
|
|
#include "engine/random.hpp"
|
|
#include "engine/render/clx_render.hpp"
|
|
#include "engine/render/primitive_render.hpp"
|
|
#include "engine/render/text_render.hpp"
|
|
#include "engine/surface.hpp"
|
|
#include "engine/world_tile.hpp"
|
|
#include "function_ref.hpp"
|
|
#include "game_mode.hpp"
|
|
#include "headless_mode.hpp"
|
|
#include "inv.h"
|
|
#include "inv_iterators.hpp"
|
|
#include "items/validation.h"
|
|
#include "levels/gendung.h"
|
|
#include "levels/gendung_defs.hpp"
|
|
#include "levels/tile_properties.hpp"
|
|
#include "levels/town.h"
|
|
#include "lighting.h"
|
|
#include "minitext.h"
|
|
#include "monster.h"
|
|
#include "msg.h"
|
|
#include "multi.h"
|
|
#include "objects.h"
|
|
#include "options.h"
|
|
#include "pack.h"
|
|
#include "panels/info_box.hpp"
|
|
#include "panels/ui_panels.hpp"
|
|
#include "player.h"
|
|
#include "qol/stash.h"
|
|
#include "quests.h"
|
|
#include "sound_effect_enums.h"
|
|
#include "spells.h"
|
|
#include "stores.h"
|
|
#include "tables/itemdat.h"
|
|
#include "tables/monstdat.h"
|
|
#include "tables/objdat.h"
|
|
#include "tables/playerdat.hpp"
|
|
#include "tables/spelldat.h"
|
|
#include "tables/textdat.h"
|
|
#include "utils/enum_traits.h"
|
|
#include "utils/format_int.hpp"
|
|
#include "utils/is_of.hpp"
|
|
#include "utils/language.h"
|
|
#include "utils/log.hpp"
|
|
#include "utils/math.h"
|
|
#include "utils/sdl_geometry.h"
|
|
#include "utils/static_vector.hpp"
|
|
#include "utils/str_cat.hpp"
|
|
#include "utils/str_split.hpp"
|
|
#include "utils/string_or_view.hpp"
|
|
#include "utils/utf8.hpp"
|
|
|
|
namespace devilution {
|
|
|
|
Item Items[MAXITEMS + 1];
|
|
uint8_t ActiveItems[MAXITEMS];
|
|
uint8_t ActiveItemCount;
|
|
int8_t dItem[MAXDUNX][MAXDUNY];
|
|
bool ShowUniqueItemInfoBox;
|
|
CornerStoneStruct CornerStone;
|
|
bool UniqueItemFlags[128];
|
|
int MaxGold = GOLD_MAX_LIMIT;
|
|
|
|
/** Maps from item_cursor_graphic to in-memory item type. */
|
|
int8_t ItemCAnimTbl[] = {
|
|
20, 16, 16, 16, 4, 4, 4, 12, 12, 12,
|
|
12, 12, 12, 12, 12, 21, 21, 25, 12, 28,
|
|
28, 28, 38, 38, 38, 32, 38, 38, 38, 24,
|
|
24, 26, 2, 25, 22, 23, 24, 21, 27, 27,
|
|
29, 0, 0, 0, 12, 12, 12, 12, 12, 0,
|
|
8, 8, 0, 8, 8, 8, 8, 8, 8, 6,
|
|
8, 8, 8, 6, 8, 8, 6, 8, 8, 6,
|
|
6, 6, 8, 8, 8, 5, 9, 13, 13, 13,
|
|
5, 5, 5, 15, 5, 5, 18, 18, 18, 30,
|
|
5, 5, 14, 5, 14, 13, 16, 18, 5, 5,
|
|
7, 1, 3, 17, 1, 15, 10, 14, 3, 11,
|
|
8, 0, 1, 7, 0, 7, 15, 7, 3, 3,
|
|
3, 6, 6, 11, 11, 11, 31, 14, 14, 14,
|
|
6, 6, 7, 3, 8, 14, 0, 14, 14, 0,
|
|
33, 1, 1, 1, 1, 1, 7, 7, 7, 14,
|
|
14, 17, 17, 17, 0, 34, 1, 0, 3, 17,
|
|
8, 8, 6, 1, 3, 3, 11, 3, 12, 12,
|
|
12, 12, 12, 12, 12, 12, 12, 12, 12, 12,
|
|
12, 12, 12, 12, 12, 12, 12, 35, 39, 36,
|
|
36, 36, 37, 38, 38, 38, 38, 38, 41, 42,
|
|
8, 8, 8, 17, 0, 6, 8, 11, 11, 3,
|
|
3, 1, 6, 6, 6, 1, 8, 6, 11, 3,
|
|
6, 8, 1, 6, 6, 17, 40, 0, 0
|
|
};
|
|
|
|
/** Maps of drop sounds effect of placing the item in the inventory. */
|
|
SfxID ItemInvSnds[] = {
|
|
SfxID::ItemArmor,
|
|
SfxID::ItemAxe,
|
|
SfxID::ItemPotion,
|
|
SfxID::ItemBow,
|
|
SfxID::ItemGold,
|
|
SfxID::ItemCap,
|
|
SfxID::ItemSword,
|
|
SfxID::ItemShield,
|
|
SfxID::ItemSword,
|
|
SfxID::ItemRock,
|
|
SfxID::ItemAxe,
|
|
SfxID::ItemStaff,
|
|
SfxID::ItemRing,
|
|
SfxID::ItemCap,
|
|
SfxID::ItemLeather,
|
|
SfxID::ItemShield,
|
|
SfxID::ItemScroll,
|
|
SfxID::ItemArmor,
|
|
SfxID::ItemBook,
|
|
SfxID::ItemArmor,
|
|
SfxID::ItemPotion,
|
|
SfxID::ItemPotion,
|
|
SfxID::ItemPotion,
|
|
SfxID::ItemPotion,
|
|
SfxID::ItemPotion,
|
|
SfxID::ItemPotion,
|
|
SfxID::ItemPotion,
|
|
SfxID::ItemPotion,
|
|
SfxID::ItemBodyPart,
|
|
SfxID::ItemBodyPart,
|
|
SfxID::ItemMushroom,
|
|
SfxID::ItemSign,
|
|
SfxID::ItemBloodStone,
|
|
SfxID::ItemAnvil,
|
|
SfxID::ItemStaff,
|
|
SfxID::ItemRock,
|
|
SfxID::ItemScroll,
|
|
SfxID::ItemScroll,
|
|
SfxID::ItemRock,
|
|
SfxID::ItemMushroom,
|
|
SfxID::ItemArmor,
|
|
SfxID::ItemLeather,
|
|
SfxID::ItemLeather,
|
|
};
|
|
|
|
namespace {
|
|
|
|
OptionalOwnedClxSpriteList itemanims[ITEMTYPES];
|
|
|
|
enum class PlayerArmorGraphic : uint8_t {
|
|
// clang-format off
|
|
Light = 0,
|
|
Medium = 1 << 4,
|
|
Heavy = 1 << 5,
|
|
// clang-format on
|
|
};
|
|
|
|
Item curruitem;
|
|
|
|
/** Holds item get records, tracking items being recently looted. This is in an effort to prevent items being picked up more than once. */
|
|
ItemGetRecordStruct itemrecord[MAXITEMS];
|
|
|
|
bool itemhold[3][3];
|
|
|
|
/** Specifies the number of active item get records. */
|
|
int gnNumGetRecords;
|
|
|
|
int OilLevels[] = { 1, 10, 1, 10, 4, 1, 5, 17, 1, 10 };
|
|
int OilValues[] = { 500, 2500, 500, 2500, 1500, 100, 2500, 15000, 500, 2500 };
|
|
item_misc_id OilMagic[] = {
|
|
IMISC_OILACC,
|
|
IMISC_OILMAST,
|
|
IMISC_OILSHARP,
|
|
IMISC_OILDEATH,
|
|
IMISC_OILSKILL,
|
|
IMISC_OILBSMTH,
|
|
IMISC_OILFORT,
|
|
IMISC_OILPERM,
|
|
IMISC_OILHARD,
|
|
IMISC_OILIMP,
|
|
};
|
|
char OilNames[10][25] = {
|
|
N_("Oil of Accuracy"),
|
|
N_("Oil of Mastery"),
|
|
N_("Oil of Sharpness"),
|
|
N_("Oil of Death"),
|
|
N_("Oil of Skill"),
|
|
N_("Blacksmith Oil"),
|
|
N_("Oil of Fortitude"),
|
|
N_("Oil of Permanence"),
|
|
N_("Oil of Hardening"),
|
|
N_("Oil of Imperviousness")
|
|
};
|
|
|
|
/** Map of item type .cel file names. */
|
|
const char *const ItemDropNames[] = {
|
|
"armor2",
|
|
"axe",
|
|
"fbttle",
|
|
"bow",
|
|
"goldflip",
|
|
"helmut",
|
|
"mace",
|
|
"shield",
|
|
"swrdflip",
|
|
"rock",
|
|
"cleaver",
|
|
"staff",
|
|
"ring",
|
|
"crownf",
|
|
"larmor",
|
|
"wshield",
|
|
"scroll",
|
|
"fplatear",
|
|
"fbook",
|
|
"food",
|
|
"fbttlebb",
|
|
"fbttledy",
|
|
"fbttleor",
|
|
"fbttlebr",
|
|
"fbttlebl",
|
|
"fbttleby",
|
|
"fbttlewh",
|
|
"fbttledb",
|
|
"fear",
|
|
"fbrain",
|
|
"fmush",
|
|
"innsign",
|
|
"bldstn",
|
|
"fanvil",
|
|
"flazstaf",
|
|
"bombs1",
|
|
"halfps1",
|
|
"wholeps1",
|
|
"runes1",
|
|
"teddys1",
|
|
"cows1",
|
|
"donkys1",
|
|
"mooses1",
|
|
};
|
|
/** Maps of item drop animation length. */
|
|
int8_t ItemAnimLs[] = {
|
|
15,
|
|
13,
|
|
16,
|
|
13,
|
|
10,
|
|
13,
|
|
13,
|
|
13,
|
|
13,
|
|
20,
|
|
13,
|
|
13,
|
|
13,
|
|
13,
|
|
13,
|
|
13,
|
|
13,
|
|
13,
|
|
13,
|
|
1,
|
|
16,
|
|
16,
|
|
16,
|
|
16,
|
|
16,
|
|
16,
|
|
16,
|
|
16,
|
|
13,
|
|
12,
|
|
12,
|
|
13,
|
|
13,
|
|
13,
|
|
8,
|
|
10,
|
|
16,
|
|
16,
|
|
10,
|
|
10,
|
|
15,
|
|
15,
|
|
15,
|
|
};
|
|
/** Maps of drop sounds effect of dropping the item on ground. */
|
|
SfxID ItemDropSnds[] = {
|
|
SfxID::ItemArmorFlip,
|
|
SfxID::ItemAxeFlip,
|
|
SfxID::ItemPotionFlip,
|
|
SfxID::ItemBowFlip,
|
|
SfxID::ItemGold,
|
|
SfxID::ItemCapFlip,
|
|
SfxID::ItemSwordFlip,
|
|
SfxID::ItemShieldFlip,
|
|
SfxID::ItemSwordFlip,
|
|
SfxID::ItemRockFlip,
|
|
SfxID::ItemAxeFlip,
|
|
SfxID::ItemStaffFlip,
|
|
SfxID::ItemRingFlip,
|
|
SfxID::ItemCapFlip,
|
|
SfxID::ItemLeatherFlip,
|
|
SfxID::ItemShieldFlip,
|
|
SfxID::ItemScrollFlip,
|
|
SfxID::ItemArmorFlip,
|
|
SfxID::ItemBookFlip,
|
|
SfxID::ItemLeatherFlip,
|
|
SfxID::ItemPotionFlip,
|
|
SfxID::ItemPotionFlip,
|
|
SfxID::ItemPotionFlip,
|
|
SfxID::ItemPotionFlip,
|
|
SfxID::ItemPotionFlip,
|
|
SfxID::ItemPotionFlip,
|
|
SfxID::ItemPotionFlip,
|
|
SfxID::ItemPotionFlip,
|
|
SfxID::ItemBodyPartFlip,
|
|
SfxID::ItemBodyPartFlip,
|
|
SfxID::ItemMushroomFlip,
|
|
SfxID::ItemSignFlip,
|
|
SfxID::ItemBloodStoneFlip,
|
|
SfxID::ItemAnvilFlip,
|
|
SfxID::ItemStaffFlip,
|
|
SfxID::ItemRockFlip,
|
|
SfxID::ItemScrollFlip,
|
|
SfxID::ItemScrollFlip,
|
|
SfxID::ItemRockFlip,
|
|
SfxID::ItemMushroomFlip,
|
|
SfxID::ItemArmorFlip,
|
|
SfxID::ItemLeatherFlip,
|
|
SfxID::ItemLeatherFlip,
|
|
};
|
|
/** Maps from Griswold premium item number to a quality level delta as added to the base quality level. */
|
|
int itemLevelAdd[] = {
|
|
// clang-format off
|
|
-1,
|
|
-1,
|
|
0,
|
|
0,
|
|
1,
|
|
2,
|
|
// clang-format on
|
|
};
|
|
/** Maps from Griswold premium item number to a quality level delta as added to the base quality level. */
|
|
int itemLevelAddHf[] = {
|
|
// clang-format off
|
|
-1,
|
|
-1,
|
|
-1,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
1,
|
|
1,
|
|
1,
|
|
1,
|
|
2,
|
|
2,
|
|
3,
|
|
3,
|
|
// clang-format on
|
|
};
|
|
|
|
int ItemsGetCurrlevel()
|
|
{
|
|
if (setlevel) {
|
|
switch (setlvlnum) {
|
|
case SL_SKELKING:
|
|
return Quests[Q_SKELKING]._qlevel;
|
|
case SL_BONECHAMB:
|
|
return Quests[Q_SCHAMB]._qlevel;
|
|
case SL_POISONWATER:
|
|
return Quests[Q_PWATER]._qlevel;
|
|
case SL_VILEBETRAYER:
|
|
return Quests[Q_BETRAYER]._qlevel;
|
|
default:
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
if (leveltype == DTYPE_NEST)
|
|
return currlevel - 8;
|
|
|
|
if (leveltype == DTYPE_CRYPT)
|
|
return currlevel - 7;
|
|
|
|
return currlevel;
|
|
}
|
|
|
|
bool ItemPlace(Point position)
|
|
{
|
|
if (dMonster[position.x][position.y] != 0)
|
|
return false;
|
|
if (dPlayer[position.x][position.y] != 0)
|
|
return false;
|
|
if (dItem[position.x][position.y] != 0)
|
|
return false;
|
|
if (IsObjectAtPosition(position))
|
|
return false;
|
|
if (TileContainsSetPiece(position))
|
|
return false;
|
|
if (IsTileSolid(position))
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
Point GetRandomAvailableItemPosition()
|
|
{
|
|
Point position = {};
|
|
do {
|
|
position = Point { GenerateRnd(80), GenerateRnd(80) } + Displacement { 16, 16 };
|
|
} while (!ItemPlace(position));
|
|
|
|
return position;
|
|
}
|
|
|
|
void AddInitItems()
|
|
{
|
|
const int curlv = ItemsGetCurrlevel();
|
|
const int rnd = GenerateRnd(3) + 3;
|
|
for (int j = 0; j < rnd; j++) {
|
|
const int ii = AllocateItem();
|
|
auto &item = Items[ii];
|
|
|
|
const Point position = GetRandomAvailableItemPosition();
|
|
item.position = position;
|
|
|
|
dItem[position.x][position.y] = ii + 1;
|
|
|
|
item._iSeed = AdvanceRndSeed();
|
|
SetRndSeed(item._iSeed);
|
|
|
|
GetItemAttrs(item, PickRandomlyAmong({ IDI_MANA, IDI_HEAL }), curlv);
|
|
|
|
item._iCreateInfo = curlv | CF_PREGEN;
|
|
SetupItem(item);
|
|
item.AnimInfo.currentFrame = static_cast<int8_t>(item.AnimInfo.numberOfFrames - 1);
|
|
item._iAnimFlag = false;
|
|
item.selectionRegion = SelectionRegion::Bottom;
|
|
DeltaAddItem(ii);
|
|
}
|
|
}
|
|
|
|
void SpawnNote()
|
|
{
|
|
_item_indexes id;
|
|
|
|
switch (currlevel) {
|
|
case 22:
|
|
id = IDI_NOTE2;
|
|
break;
|
|
case 23:
|
|
id = IDI_NOTE3;
|
|
break;
|
|
default:
|
|
id = IDI_NOTE1;
|
|
break;
|
|
}
|
|
|
|
const Point position = GetRandomAvailableItemPosition();
|
|
SpawnQuestItem(id, position, 0, SelectionRegion::Bottom, false);
|
|
}
|
|
|
|
void CalcSelfItems(Player &player)
|
|
{
|
|
int sa = 0;
|
|
int ma = 0;
|
|
int da = 0;
|
|
|
|
// first iteration is used for collecting stat bonuses from items
|
|
for (Item &equipment : EquippedPlayerItemsRange(player)) {
|
|
equipment._iStatFlag = true;
|
|
if (equipment._iIdentified) {
|
|
sa += equipment._iPLStr;
|
|
ma += equipment._iPLMag;
|
|
da += equipment._iPLDex;
|
|
}
|
|
}
|
|
|
|
bool changeflag;
|
|
do {
|
|
// cap stats to 0
|
|
const int currstr = std::max(0, sa + player._pBaseStr);
|
|
const int currmag = std::max(0, ma + player._pBaseMag);
|
|
const int currdex = std::max(0, da + player._pBaseDex);
|
|
|
|
changeflag = false;
|
|
// Iterate over equipped items and remove stat bonuses if they are not valid
|
|
for (Item &equipment : EquippedPlayerItemsRange(player)) {
|
|
if (!equipment._iStatFlag)
|
|
continue;
|
|
|
|
bool isValid = IsItemValid(player, equipment);
|
|
|
|
if (currstr < equipment._iMinStr
|
|
|| currmag < equipment._iMinMag
|
|
|| currdex < equipment._iMinDex)
|
|
isValid = false;
|
|
|
|
if (isValid)
|
|
continue;
|
|
|
|
changeflag = true;
|
|
equipment._iStatFlag = false;
|
|
if (equipment._iIdentified) {
|
|
sa -= equipment._iPLStr;
|
|
ma -= equipment._iPLMag;
|
|
da -= equipment._iPLDex;
|
|
}
|
|
}
|
|
} while (changeflag);
|
|
}
|
|
|
|
bool GetItemSpace(Point position, int8_t inum)
|
|
{
|
|
int xx = 0;
|
|
int yy = 0;
|
|
for (int j = position.y - 1; j <= position.y + 1; j++) {
|
|
xx = 0;
|
|
for (int i = position.x - 1; i <= position.x + 1; i++) {
|
|
itemhold[xx][yy] = ItemSpaceOk({ i, j });
|
|
xx++;
|
|
}
|
|
yy++;
|
|
}
|
|
|
|
bool savail = false;
|
|
for (int j = 0; j < 3; j++) {
|
|
for (int i = 0; i < 3; i++) { // NOLINT(modernize-loop-convert)
|
|
if (itemhold[i][j])
|
|
savail = true;
|
|
}
|
|
}
|
|
|
|
int rs = GenerateRnd(15) + 1;
|
|
|
|
if (!savail)
|
|
return false;
|
|
|
|
xx = 0;
|
|
yy = 0;
|
|
while (rs > 0) {
|
|
if (itemhold[xx][yy])
|
|
rs--;
|
|
if (rs <= 0)
|
|
continue;
|
|
xx++;
|
|
if (xx != 3)
|
|
continue;
|
|
xx = 0;
|
|
yy++;
|
|
if (yy == 3)
|
|
yy = 0;
|
|
}
|
|
|
|
xx += position.x - 1;
|
|
yy += position.y - 1;
|
|
Items[inum].position = { xx, yy };
|
|
dItem[xx][yy] = static_cast<int8_t>(inum + 1);
|
|
|
|
return true;
|
|
}
|
|
|
|
void CalcItemValue(Item &item)
|
|
{
|
|
int v = item._iVMult1 + item._iVMult2;
|
|
if (v > 0) {
|
|
v *= item._ivalue;
|
|
}
|
|
if (v < 0) {
|
|
v = item._ivalue / v;
|
|
}
|
|
v = item._iVAdd1 + item._iVAdd2 + v;
|
|
item._iIvalue = std::max(v, 1);
|
|
}
|
|
|
|
void GetBookSpell(Item &item, int lvl)
|
|
{
|
|
if (lvl == 0)
|
|
lvl = 1;
|
|
|
|
int rv = GenerateRnd(static_cast<int32_t>(SpellsData.size())) + 1;
|
|
|
|
if (gbIsSpawn && lvl > 5)
|
|
lvl = 5;
|
|
|
|
int s = static_cast<int8_t>(SpellID::Firebolt);
|
|
SpellID bs = SpellID::Firebolt;
|
|
while (rv > 0) {
|
|
const int sLevel = GetSpellBookLevel(static_cast<SpellID>(s));
|
|
if (sLevel != -1 && lvl >= sLevel) {
|
|
rv--;
|
|
bs = static_cast<SpellID>(s);
|
|
}
|
|
s++;
|
|
if (!gbIsMultiplayer) {
|
|
if (s == static_cast<int8_t>(SpellID::Resurrect))
|
|
s = static_cast<int8_t>(SpellID::Telekinesis);
|
|
}
|
|
if (!gbIsMultiplayer) {
|
|
if (s == static_cast<int8_t>(SpellID::HealOther))
|
|
s = static_cast<int8_t>(SpellID::BloodStar);
|
|
}
|
|
if (static_cast<size_t>(s) == SpellsData.size())
|
|
s = 1;
|
|
}
|
|
const std::string_view spellName = GetSpellData(bs).sNameText;
|
|
const size_t iNameLen = std::string_view(item._iName).size();
|
|
const size_t iINameLen = std::string_view(item._iIName).size();
|
|
CopyUtf8(item._iName + iNameLen, spellName, ItemNameLength - iNameLen);
|
|
CopyUtf8(item._iIName + iINameLen, spellName, ItemNameLength - iINameLen);
|
|
item._iSpell = bs;
|
|
const SpellData &spellData = GetSpellData(bs);
|
|
item._iMinMag = spellData.minInt;
|
|
item._ivalue += static_cast<int32_t>(spellData.bookCost());
|
|
item._iIvalue += static_cast<int32_t>(spellData.bookCost());
|
|
switch (spellData.type()) {
|
|
case MagicType::Fire:
|
|
item._iCurs = ICURS_BOOK_RED;
|
|
break;
|
|
case MagicType::Lightning:
|
|
item._iCurs = ICURS_BOOK_BLUE;
|
|
break;
|
|
case MagicType::Magic:
|
|
item._iCurs = ICURS_BOOK_GREY;
|
|
break;
|
|
}
|
|
}
|
|
|
|
int RndPL(int param1, int param2)
|
|
{
|
|
return param1 + GenerateRnd(param2 - param1 + 1);
|
|
}
|
|
|
|
int CalculateToHitBonus(int level)
|
|
{
|
|
switch (level) {
|
|
case -50:
|
|
return -RndPL(6, 10);
|
|
case -25:
|
|
return -RndPL(1, 5);
|
|
case 20:
|
|
return RndPL(1, 5);
|
|
case 36:
|
|
return RndPL(6, 10);
|
|
case 51:
|
|
return RndPL(11, 15);
|
|
case 66:
|
|
return RndPL(16, 20);
|
|
case 81:
|
|
return RndPL(21, 30);
|
|
case 96:
|
|
return RndPL(31, 40);
|
|
case 111:
|
|
return RndPL(41, 50);
|
|
case 126:
|
|
return RndPL(51, 75);
|
|
case 151:
|
|
return RndPL(76, 100);
|
|
default:
|
|
app_fatal("Unknown to hit bonus");
|
|
}
|
|
}
|
|
|
|
int SaveItemPower(const Player &player, Item &item, ItemPower &power)
|
|
{
|
|
int r = RndPL(power.param1, power.param2);
|
|
|
|
switch (power.type) {
|
|
case IPL_TOHIT:
|
|
item._iPLToHit += static_cast<int16_t>(r);
|
|
break;
|
|
case IPL_TOHIT_CURSE:
|
|
item._iPLToHit -= static_cast<int16_t>(r);
|
|
break;
|
|
case IPL_DAMP:
|
|
item._iPLDam += static_cast<int16_t>(r);
|
|
break;
|
|
case IPL_DAMP_CURSE:
|
|
item._iPLDam -= static_cast<int16_t>(r);
|
|
break;
|
|
case IPL_DOPPELGANGER:
|
|
item._iDamAcFlags |= ItemSpecialEffectHf::Doppelganger;
|
|
[[fallthrough]];
|
|
case IPL_TOHIT_DAMP:
|
|
r = RndPL(power.param1, power.param2);
|
|
item._iPLDam += static_cast<int16_t>(r);
|
|
item._iPLToHit += CalculateToHitBonus(power.param1);
|
|
break;
|
|
case IPL_TOHIT_DAMP_CURSE:
|
|
item._iPLDam -= static_cast<int16_t>(r);
|
|
item._iPLToHit += CalculateToHitBonus(-power.param1);
|
|
break;
|
|
case IPL_ACP:
|
|
item._iPLAC += r;
|
|
break;
|
|
case IPL_ACP_CURSE:
|
|
item._iPLAC -= r;
|
|
break;
|
|
case IPL_SETAC:
|
|
item._iAC = r;
|
|
break;
|
|
case IPL_AC_CURSE:
|
|
item._iAC -= r;
|
|
break;
|
|
case IPL_FIRERES:
|
|
item._iPLFR += r;
|
|
break;
|
|
case IPL_LIGHTRES:
|
|
item._iPLLR += r;
|
|
break;
|
|
case IPL_MAGICRES:
|
|
item._iPLMR += r;
|
|
break;
|
|
case IPL_ALLRES:
|
|
item._iPLFR = std::max(item._iPLFR + r, 0);
|
|
item._iPLLR = std::max(item._iPLLR + r, 0);
|
|
item._iPLMR = std::max(item._iPLMR + r, 0);
|
|
break;
|
|
case IPL_SPLLVLADD:
|
|
item._iSplLvlAdd = r;
|
|
break;
|
|
case IPL_CHARGES:
|
|
item._iCharges *= power.param1;
|
|
item._iMaxCharges = item._iCharges;
|
|
break;
|
|
case IPL_SPELL:
|
|
item._iSpell = static_cast<SpellID>(power.param1);
|
|
item._iCharges = power.param2;
|
|
item._iMaxCharges = power.param2;
|
|
break;
|
|
case IPL_FIREDAM:
|
|
item._iFlags |= ItemSpecialEffect::FireDamage;
|
|
item._iFlags &= ~ItemSpecialEffect::LightningDamage;
|
|
item._iFMinDam = power.param1;
|
|
item._iFMaxDam = power.param2;
|
|
item._iLMinDam = 0;
|
|
item._iLMaxDam = 0;
|
|
break;
|
|
case IPL_LIGHTDAM:
|
|
item._iFlags |= ItemSpecialEffect::LightningDamage;
|
|
item._iFlags &= ~ItemSpecialEffect::FireDamage;
|
|
item._iLMinDam = power.param1;
|
|
item._iLMaxDam = power.param2;
|
|
item._iFMinDam = 0;
|
|
item._iFMaxDam = 0;
|
|
break;
|
|
case IPL_STR:
|
|
item._iPLStr += r;
|
|
break;
|
|
case IPL_STR_CURSE:
|
|
item._iPLStr -= r;
|
|
break;
|
|
case IPL_MAG:
|
|
item._iPLMag += r;
|
|
break;
|
|
case IPL_MAG_CURSE:
|
|
item._iPLMag -= r;
|
|
break;
|
|
case IPL_DEX:
|
|
item._iPLDex += r;
|
|
break;
|
|
case IPL_DEX_CURSE:
|
|
item._iPLDex -= r;
|
|
break;
|
|
case IPL_VIT:
|
|
item._iPLVit += r;
|
|
break;
|
|
case IPL_VIT_CURSE:
|
|
item._iPLVit -= r;
|
|
break;
|
|
case IPL_ATTRIBS:
|
|
item._iPLStr += r;
|
|
item._iPLMag += r;
|
|
item._iPLDex += r;
|
|
item._iPLVit += r;
|
|
break;
|
|
case IPL_ATTRIBS_CURSE:
|
|
item._iPLStr -= r;
|
|
item._iPLMag -= r;
|
|
item._iPLDex -= r;
|
|
item._iPLVit -= r;
|
|
break;
|
|
case IPL_GETHIT_CURSE:
|
|
item._iPLGetHit += r;
|
|
break;
|
|
case IPL_GETHIT:
|
|
item._iPLGetHit -= r;
|
|
break;
|
|
case IPL_LIFE:
|
|
item._iPLHP += r << 6;
|
|
break;
|
|
case IPL_LIFE_CURSE:
|
|
item._iPLHP -= r << 6;
|
|
break;
|
|
case IPL_MANA:
|
|
item._iPLMana += r << 6;
|
|
RedrawComponent(PanelDrawComponent::Mana);
|
|
break;
|
|
case IPL_MANA_CURSE:
|
|
item._iPLMana -= r << 6;
|
|
RedrawComponent(PanelDrawComponent::Mana);
|
|
break;
|
|
case IPL_DUR: {
|
|
const int bonus = r * item._iMaxDur / 100;
|
|
item._iMaxDur += bonus;
|
|
item._iDurability += bonus;
|
|
} break;
|
|
case IPL_CRYSTALLINE:
|
|
item._iPLDam += 140 + r * 2;
|
|
[[fallthrough]];
|
|
case IPL_DUR_CURSE:
|
|
item._iMaxDur -= r * item._iMaxDur / 100;
|
|
item._iMaxDur = std::max<uint8_t>(item._iMaxDur, 1);
|
|
item._iDurability = item._iMaxDur;
|
|
break;
|
|
case IPL_INDESTRUCTIBLE:
|
|
item._iDurability = DUR_INDESTRUCTIBLE;
|
|
item._iMaxDur = DUR_INDESTRUCTIBLE;
|
|
break;
|
|
case IPL_LIGHT:
|
|
item._iPLLight += power.param1;
|
|
break;
|
|
case IPL_LIGHT_CURSE:
|
|
item._iPLLight -= power.param1;
|
|
break;
|
|
case IPL_MULT_ARROWS:
|
|
item._iFlags |= ItemSpecialEffect::MultipleArrows;
|
|
break;
|
|
case IPL_FIRE_ARROWS:
|
|
item._iFlags |= (ItemSpecialEffect::FireArrows | ItemSpecialEffect::FireDamage);
|
|
item._iFlags &= ~ItemSpecialEffect::LightningArrows;
|
|
item._iFMinDam = power.param1;
|
|
item._iFMaxDam = power.param2;
|
|
item._iLMinDam = 0;
|
|
item._iLMaxDam = 0;
|
|
break;
|
|
case IPL_LIGHT_ARROWS:
|
|
item._iFlags |= (ItemSpecialEffect::LightningArrows | ItemSpecialEffect::LightningDamage);
|
|
item._iFlags &= ~ItemSpecialEffect::FireArrows;
|
|
item._iLMinDam = power.param1;
|
|
item._iLMaxDam = power.param2;
|
|
item._iFMinDam = 0;
|
|
item._iFMaxDam = 0;
|
|
break;
|
|
case IPL_FIREBALL:
|
|
item._iFlags |= (ItemSpecialEffect::LightningArrows | ItemSpecialEffect::FireArrows);
|
|
item._iFMinDam = power.param1;
|
|
item._iFMaxDam = power.param2;
|
|
item._iLMinDam = 0;
|
|
item._iLMaxDam = 0;
|
|
break;
|
|
case IPL_THORNS:
|
|
item._iFlags |= ItemSpecialEffect::Thorns;
|
|
break;
|
|
case IPL_NOMANA:
|
|
item._iFlags |= ItemSpecialEffect::NoMana;
|
|
RedrawComponent(PanelDrawComponent::Mana);
|
|
break;
|
|
case IPL_ABSHALFTRAP:
|
|
item._iFlags |= ItemSpecialEffect::HalfTrapDamage;
|
|
break;
|
|
case IPL_KNOCKBACK:
|
|
item._iFlags |= ItemSpecialEffect::Knockback;
|
|
break;
|
|
case IPL_3XDAMVDEM:
|
|
item._iFlags |= ItemSpecialEffect::TripleDemonDamage;
|
|
break;
|
|
case IPL_ALLRESZERO:
|
|
item._iFlags |= ItemSpecialEffect::ZeroResistance;
|
|
break;
|
|
case IPL_STEALMANA:
|
|
if (power.param1 == 3)
|
|
item._iFlags |= ItemSpecialEffect::StealMana3;
|
|
if (power.param1 == 5)
|
|
item._iFlags |= ItemSpecialEffect::StealMana5;
|
|
RedrawComponent(PanelDrawComponent::Mana);
|
|
break;
|
|
case IPL_STEALLIFE:
|
|
if (power.param1 == 3)
|
|
item._iFlags |= ItemSpecialEffect::StealLife3;
|
|
if (power.param1 == 5)
|
|
item._iFlags |= ItemSpecialEffect::StealLife5;
|
|
RedrawComponent(PanelDrawComponent::Health);
|
|
break;
|
|
case IPL_TARGAC:
|
|
if (gbIsHellfire)
|
|
item._iPLEnAc = power.param1;
|
|
else
|
|
item._iPLEnAc += r;
|
|
break;
|
|
case IPL_FASTATTACK:
|
|
if (power.param1 == 1)
|
|
item._iFlags |= ItemSpecialEffect::QuickAttack;
|
|
if (power.param1 == 2)
|
|
item._iFlags |= ItemSpecialEffect::FastAttack;
|
|
if (power.param1 == 3)
|
|
item._iFlags |= ItemSpecialEffect::FasterAttack;
|
|
if (power.param1 == 4)
|
|
item._iFlags |= ItemSpecialEffect::FastestAttack;
|
|
break;
|
|
case IPL_FASTRECOVER:
|
|
if (power.param1 == 1)
|
|
item._iFlags |= ItemSpecialEffect::FastHitRecovery;
|
|
if (power.param1 == 2)
|
|
item._iFlags |= ItemSpecialEffect::FasterHitRecovery;
|
|
if (power.param1 == 3)
|
|
item._iFlags |= ItemSpecialEffect::FastestHitRecovery;
|
|
break;
|
|
case IPL_FASTBLOCK:
|
|
item._iFlags |= ItemSpecialEffect::FastBlock;
|
|
break;
|
|
case IPL_DAMMOD:
|
|
item._iPLDamMod += r;
|
|
break;
|
|
case IPL_RNDARROWVEL:
|
|
item._iFlags |= ItemSpecialEffect::RandomArrowVelocity;
|
|
break;
|
|
case IPL_SETDAM:
|
|
item._iMinDam = power.param1;
|
|
item._iMaxDam = power.param2;
|
|
break;
|
|
case IPL_SETDUR:
|
|
item._iDurability = power.param1;
|
|
item._iMaxDur = power.param1;
|
|
break;
|
|
case IPL_ONEHAND:
|
|
item._iLoc = ILOC_ONEHAND;
|
|
break;
|
|
case IPL_DRAINLIFE:
|
|
item._iFlags |= ItemSpecialEffect::DrainLife;
|
|
break;
|
|
case IPL_RNDSTEALLIFE:
|
|
item._iFlags |= ItemSpecialEffect::RandomStealLife;
|
|
break;
|
|
case IPL_NOMINSTR:
|
|
item._iMinStr = 0;
|
|
break;
|
|
case IPL_ADDACLIFE:
|
|
item._iFlags |= (ItemSpecialEffect::LightningArrows | ItemSpecialEffect::FireArrows);
|
|
item._iFMinDam = power.param1;
|
|
item._iFMaxDam = power.param2;
|
|
item._iLMinDam = 1;
|
|
item._iLMaxDam = 0;
|
|
break;
|
|
case IPL_ADDMANAAC:
|
|
item._iFlags |= (ItemSpecialEffect::LightningDamage | ItemSpecialEffect::FireDamage);
|
|
item._iFMinDam = power.param1;
|
|
item._iFMaxDam = power.param2;
|
|
item._iLMinDam = 2;
|
|
item._iLMaxDam = 0;
|
|
break;
|
|
case IPL_FIRERES_CURSE:
|
|
item._iPLFR -= r;
|
|
break;
|
|
case IPL_LIGHTRES_CURSE:
|
|
item._iPLLR -= r;
|
|
break;
|
|
case IPL_MAGICRES_CURSE:
|
|
item._iPLMR -= r;
|
|
break;
|
|
case IPL_DEVASTATION:
|
|
item._iDamAcFlags |= ItemSpecialEffectHf::Devastation;
|
|
break;
|
|
case IPL_DECAY:
|
|
item._iDamAcFlags |= ItemSpecialEffectHf::Decay;
|
|
item._iPLDam += r;
|
|
break;
|
|
case IPL_PERIL:
|
|
item._iDamAcFlags |= ItemSpecialEffectHf::Peril;
|
|
break;
|
|
case IPL_JESTERS:
|
|
item._iDamAcFlags |= ItemSpecialEffectHf::Jesters;
|
|
break;
|
|
case IPL_ACDEMON:
|
|
item._iDamAcFlags |= ItemSpecialEffectHf::ACAgainstDemons;
|
|
break;
|
|
case IPL_ACUNDEAD:
|
|
item._iDamAcFlags |= ItemSpecialEffectHf::ACAgainstUndead;
|
|
break;
|
|
case IPL_MANATOLIFE: {
|
|
const int portion = ((player._pMaxManaBase >> 6) * 50 / 100) << 6;
|
|
item._iPLMana -= portion;
|
|
item._iPLHP += portion;
|
|
} break;
|
|
case IPL_LIFETOMANA: {
|
|
const int portion = ((player._pMaxHPBase >> 6) * 40 / 100) << 6;
|
|
item._iPLHP -= portion;
|
|
item._iPLMana += portion;
|
|
} break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return r;
|
|
}
|
|
|
|
bool StringInPanel(const char *str)
|
|
{
|
|
return GetLineWidth(str, GameFont12, 2) < 254;
|
|
}
|
|
|
|
int PLVal(int pv, int p1, int p2, int minv, int maxv)
|
|
{
|
|
if (p1 == p2)
|
|
return minv;
|
|
if (minv == maxv)
|
|
return minv;
|
|
return minv + ((maxv - minv) * (100 * (pv - p1) / (p2 - p1)) / 100);
|
|
}
|
|
|
|
void SaveItemAffix(const Player &player, Item &item, const PLStruct &affix)
|
|
{
|
|
auto power = affix.power;
|
|
int value = SaveItemPower(player, item, power);
|
|
|
|
value = PLVal(value, power.param1, power.param2, affix.minVal, affix.maxVal);
|
|
if (item._iVAdd1 != 0 || item._iVMult1 != 0) {
|
|
item._iVAdd2 = value;
|
|
item._iVMult2 = affix.multVal;
|
|
} else {
|
|
item._iVAdd1 = value;
|
|
item._iVMult1 = affix.multVal;
|
|
}
|
|
}
|
|
|
|
std::optional<const PLStruct *> SelectAffix(
|
|
const std::vector<PLStruct> &affixList,
|
|
AffixItemType type,
|
|
int minlvl, int maxlvl,
|
|
bool onlygood,
|
|
goodorevil goe,
|
|
bool excludeChargesForStaffs)
|
|
{
|
|
StaticVector<const PLStruct *, 256> eligibleAffixes;
|
|
|
|
for (const PLStruct &affix : affixList) {
|
|
if (!HasAnyOf(type, affix.PLIType))
|
|
continue;
|
|
if (affix.PLMinLvl < minlvl || affix.PLMinLvl > maxlvl)
|
|
continue;
|
|
if (onlygood && !affix.PLOk)
|
|
continue;
|
|
if ((goe == GOE_GOOD && affix.PLGOE == GOE_EVIL) || (goe == GOE_EVIL && affix.PLGOE == GOE_GOOD))
|
|
continue;
|
|
if (excludeChargesForStaffs && type == AffixItemType::Staff && affix.power.type == IPL_CHARGES)
|
|
continue;
|
|
|
|
for (int i = 0; i < affix.PLChance; ++i) {
|
|
eligibleAffixes.push_back(&affix);
|
|
}
|
|
}
|
|
|
|
if (eligibleAffixes.empty())
|
|
return std::nullopt;
|
|
|
|
return eligibleAffixes[GenerateRnd(static_cast<int>(eligibleAffixes.size()))];
|
|
}
|
|
|
|
std::optional<const PLStruct *> GetStaffPrefix(int maxlvl, bool onlygood)
|
|
{
|
|
if (!FlipCoin(10) && !onlygood) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
return SelectAffix(ItemPrefixes, AffixItemType::Staff, 0, maxlvl, onlygood, GOE_ANY, false);
|
|
}
|
|
|
|
std::string GenerateStaffName(const ItemData &baseItemData, SpellID spellId, bool translate)
|
|
{
|
|
std::string_view baseName = translate ? _(baseItemData.iName) : baseItemData.iName;
|
|
std::string_view spellName = translate ? pgettext("spell", GetSpellData(spellId).sNameText) : GetSpellData(spellId).sNameText;
|
|
const std::string_view normalFmt = translate ? pgettext("spell", /* TRANSLATORS: Constructs item names. Format: {Item} of {Spell}. Example: War Staff of Firewall */ "{0} of {1}") : "{0} of {1}";
|
|
std::string name = fmt::format(fmt::runtime(normalFmt), baseName, spellName);
|
|
if (!StringInPanel(name.c_str())) {
|
|
std::string_view shortName = translate ? _(baseItemData.iSName) : baseItemData.iSName;
|
|
name = fmt::format(fmt::runtime(normalFmt), shortName, spellName);
|
|
}
|
|
return name;
|
|
}
|
|
|
|
std::string GenerateStaffNameMagical(const ItemData &baseItemData, SpellID spellId, const PLStruct &power, bool translate, std::optional<bool> forceNameLengthCheck)
|
|
{
|
|
std::string_view baseName = translate ? _(baseItemData.iName) : baseItemData.iName;
|
|
const std::string_view magicFmt = translate ? pgettext("spell", /* TRANSLATORS: Constructs item names. Format: {Prefix} {Item} of {Spell}. Example: King's War Staff of Firewall */ "{0} {1} of {2}") : "{0} {1} of {2}";
|
|
std::string_view spellName = translate ? pgettext("spell", GetSpellData(spellId).sNameText) : GetSpellData(spellId).sNameText;
|
|
std::string_view prefixName = translate ? _(power.PLName) : power.PLName;
|
|
|
|
std::string identifiedName = fmt::format(fmt::runtime(magicFmt), prefixName, baseName, spellName);
|
|
if (forceNameLengthCheck ? *forceNameLengthCheck : !StringInPanel(identifiedName.c_str())) {
|
|
std::string_view shortName = translate ? _(baseItemData.iSName) : baseItemData.iSName;
|
|
identifiedName = fmt::format(fmt::runtime(magicFmt), prefixName, shortName, spellName);
|
|
}
|
|
return identifiedName;
|
|
}
|
|
|
|
void GetStaffPower(const Player &player, Item &item, int maxlvl, bool onlygood)
|
|
{
|
|
std::optional<const PLStruct *> prefix = GetStaffPrefix(maxlvl, onlygood);
|
|
if (prefix.has_value()) {
|
|
item._iMagical = ITEM_QUALITY_MAGIC;
|
|
SaveItemAffix(player, item, **prefix);
|
|
item._iPrePower = (*prefix)->power.type;
|
|
}
|
|
|
|
const ItemData &baseItemData = AllItemsList[item.IDidx];
|
|
const std::string staffName = GenerateStaffName(baseItemData, item._iSpell, false);
|
|
|
|
CopyUtf8(item._iName, staffName, ItemNameLength);
|
|
if (prefix.has_value()) {
|
|
const std::string staffNameMagical = GenerateStaffNameMagical(baseItemData, item._iSpell, **prefix, false, std::nullopt);
|
|
CopyUtf8(item._iIName, staffNameMagical, ItemNameLength);
|
|
} else {
|
|
CopyUtf8(item._iIName, item._iName, ItemNameLength);
|
|
}
|
|
|
|
CalcItemValue(item);
|
|
}
|
|
|
|
std::string GenerateMagicItemName(const std::string_view &baseNamel, const PLStruct *pPrefix, const PLStruct *pSufix, bool translate)
|
|
{
|
|
if (pPrefix != nullptr && pSufix != nullptr) {
|
|
const std::string_view fmt = translate ? _(/* TRANSLATORS: Constructs item names. Format: {Prefix} {Item} of {Suffix}. Example: King's Long Sword of the Whale */ "{0} {1} of {2}") : "{0} {1} of {2}";
|
|
return fmt::format(fmt::runtime(fmt), translate ? _(pPrefix->PLName) : pPrefix->PLName, baseNamel, translate ? _(pSufix->PLName) : pSufix->PLName);
|
|
}
|
|
if (pPrefix != nullptr) {
|
|
const std::string_view fmt = translate ? _(/* TRANSLATORS: Constructs item names. Format: {Prefix} {Item}. Example: King's Long Sword */ "{0} {1}") : "{0} {1}";
|
|
return fmt::format(fmt::runtime(fmt), translate ? _(pPrefix->PLName) : pPrefix->PLName, baseNamel);
|
|
}
|
|
if (pSufix != nullptr) {
|
|
const std::string_view fmt = translate ? _(/* TRANSLATORS: Constructs item names. Format: {Item} of {Suffix}. Example: Long Sword of the Whale */ "{0} of {1}") : "{0} of {1}";
|
|
return fmt::format(fmt::runtime(fmt), baseNamel, translate ? _(pSufix->PLName) : pSufix->PLName);
|
|
}
|
|
|
|
return std::string(baseNamel);
|
|
}
|
|
|
|
void GetItemPowerPrefixAndSuffix(
|
|
int minlvl, int maxlvl,
|
|
AffixItemType flgs,
|
|
bool onlygood,
|
|
tl::function_ref<void(const PLStruct &prefix)> prefixFound,
|
|
tl::function_ref<void(const PLStruct &suffix)> suffixFound)
|
|
{
|
|
bool allocatePrefix = FlipCoin(4);
|
|
bool allocateSuffix = !FlipCoin(3);
|
|
if (!allocatePrefix && !allocateSuffix) {
|
|
// At least try and give each item a prefix or suffix
|
|
if (FlipCoin())
|
|
allocatePrefix = true;
|
|
else
|
|
allocateSuffix = true;
|
|
}
|
|
goodorevil goe = GOE_ANY;
|
|
if (!onlygood && !FlipCoin(3))
|
|
onlygood = true;
|
|
|
|
if (allocatePrefix) {
|
|
std::optional<const PLStruct *> prefix = SelectAffix(ItemPrefixes, flgs, minlvl, maxlvl, onlygood, goe, true);
|
|
if (prefix.has_value()) {
|
|
goe = (*prefix)->PLGOE;
|
|
prefixFound(**prefix);
|
|
}
|
|
}
|
|
|
|
if (allocateSuffix) {
|
|
std::optional<const PLStruct *> suffix = SelectAffix(ItemSuffixes, flgs, minlvl, maxlvl, onlygood, goe, true);
|
|
if (suffix.has_value()) {
|
|
suffixFound(**suffix);
|
|
}
|
|
}
|
|
}
|
|
|
|
void GetItemPower(const Player &player, Item &item, int minlvl, int maxlvl, AffixItemType flgs, bool onlygood)
|
|
{
|
|
const PLStruct *pPrefix = nullptr;
|
|
const PLStruct *pSufix = nullptr;
|
|
GetItemPowerPrefixAndSuffix(
|
|
minlvl, maxlvl, flgs, onlygood,
|
|
[&item, &player, &pPrefix](const PLStruct &prefix) {
|
|
item._iMagical = ITEM_QUALITY_MAGIC;
|
|
SaveItemAffix(player, item, prefix);
|
|
item._iPrePower = prefix.power.type;
|
|
pPrefix = &prefix;
|
|
},
|
|
[&item, &player, &pSufix](const PLStruct &suffix) {
|
|
item._iMagical = ITEM_QUALITY_MAGIC;
|
|
SaveItemAffix(player, item, suffix);
|
|
item._iSufPower = suffix.power.type;
|
|
pSufix = &suffix;
|
|
});
|
|
|
|
CopyUtf8(item._iIName, GenerateMagicItemName(item._iName, pPrefix, pSufix, false), ItemNameLength);
|
|
if (!StringInPanel(item._iIName)) {
|
|
CopyUtf8(item._iIName, GenerateMagicItemName(AllItemsList[item.IDidx].iSName, pPrefix, pSufix, false), ItemNameLength);
|
|
}
|
|
if (pPrefix != nullptr || pSufix != nullptr)
|
|
CalcItemValue(item);
|
|
}
|
|
|
|
void GetStaffSpell(const Player &player, Item &item, int lvl, bool onlygood)
|
|
{
|
|
if (!gbIsHellfire && FlipCoin(4)) {
|
|
GetItemPower(player, item, lvl / 2, lvl, AffixItemType::Staff, onlygood);
|
|
return;
|
|
}
|
|
|
|
int l = lvl / 2;
|
|
if (l == 0)
|
|
l = 1;
|
|
int rv = GenerateRnd(static_cast<int32_t>(SpellsData.size())) + 1;
|
|
|
|
if (gbIsSpawn && lvl > 10)
|
|
lvl = 10;
|
|
|
|
int s = static_cast<int8_t>(SpellID::Firebolt);
|
|
SpellID bs = SpellID::Null;
|
|
while (rv > 0) {
|
|
const int sLevel = GetSpellStaffLevel(static_cast<SpellID>(s));
|
|
if (sLevel != -1 && l >= sLevel) {
|
|
rv--;
|
|
bs = static_cast<SpellID>(s);
|
|
}
|
|
s++;
|
|
if (!gbIsMultiplayer && s == static_cast<int8_t>(SpellID::Resurrect))
|
|
s = static_cast<int8_t>(SpellID::Telekinesis);
|
|
if (!gbIsMultiplayer && s == static_cast<int8_t>(SpellID::HealOther))
|
|
s = static_cast<int8_t>(SpellID::BloodStar);
|
|
if (static_cast<size_t>(s) == SpellsData.size())
|
|
s = static_cast<int8_t>(SpellID::Firebolt);
|
|
}
|
|
|
|
const int minc = GetSpellData(bs).sStaffMin;
|
|
const int maxc = GetSpellData(bs).sStaffMax - minc + 1;
|
|
item._iSpell = bs;
|
|
item._iCharges = minc + GenerateRnd(maxc);
|
|
item._iMaxCharges = item._iCharges;
|
|
|
|
item._iMinMag = GetSpellData(bs).minInt;
|
|
const int v = item._iCharges * GetSpellData(bs).staffCost() / 5;
|
|
item._ivalue += v;
|
|
item._iIvalue += v;
|
|
GetStaffPower(player, item, lvl, onlygood);
|
|
}
|
|
|
|
void GetOilType(Item &item, int maxLvl)
|
|
{
|
|
int cnt = 2;
|
|
int8_t rnd[32] = { 5, 6 };
|
|
|
|
if (!gbIsMultiplayer) {
|
|
if (maxLvl == 0)
|
|
maxLvl = 1;
|
|
|
|
cnt = 0;
|
|
for (size_t j = 0; j < sizeof(OilLevels) / sizeof(OilLevels[0]); j++) {
|
|
if (OilLevels[j] <= maxLvl) {
|
|
rnd[cnt] = static_cast<int8_t>(j);
|
|
cnt++;
|
|
}
|
|
}
|
|
}
|
|
|
|
const int8_t t = rnd[GenerateRnd(cnt)];
|
|
|
|
CopyUtf8(item._iName, OilNames[t], ItemNameLength);
|
|
CopyUtf8(item._iIName, OilNames[t], ItemNameLength);
|
|
item._iMiscId = OilMagic[t];
|
|
item._ivalue = OilValues[t];
|
|
item._iIvalue = OilValues[t];
|
|
}
|
|
|
|
void GetItemBonus(const Player &player, Item &item, int minlvl, int maxlvl, bool onlygood, bool allowspells)
|
|
{
|
|
minlvl = std::min(minlvl, 25);
|
|
|
|
switch (item._itype) {
|
|
case ItemType::Sword:
|
|
case ItemType::Axe:
|
|
case ItemType::Mace:
|
|
GetItemPower(player, item, minlvl, maxlvl, AffixItemType::Weapon, onlygood);
|
|
break;
|
|
case ItemType::Bow:
|
|
GetItemPower(player, item, minlvl, maxlvl, AffixItemType::Bow, onlygood);
|
|
break;
|
|
case ItemType::Shield:
|
|
GetItemPower(player, item, minlvl, maxlvl, AffixItemType::Shield, onlygood);
|
|
break;
|
|
case ItemType::LightArmor:
|
|
case ItemType::Helm:
|
|
case ItemType::MediumArmor:
|
|
case ItemType::HeavyArmor:
|
|
GetItemPower(player, item, minlvl, maxlvl, AffixItemType::Armor, onlygood);
|
|
break;
|
|
case ItemType::Staff:
|
|
if (allowspells)
|
|
GetStaffSpell(player, item, maxlvl, onlygood);
|
|
else
|
|
GetItemPower(player, item, minlvl, maxlvl, AffixItemType::Staff, onlygood);
|
|
break;
|
|
case ItemType::Ring:
|
|
case ItemType::Amulet:
|
|
GetItemPower(player, item, minlvl, maxlvl, AffixItemType::Misc, onlygood);
|
|
break;
|
|
case ItemType::None:
|
|
case ItemType::Misc:
|
|
case ItemType::Gold:
|
|
break;
|
|
}
|
|
}
|
|
|
|
struct WeightedItemIndex {
|
|
_item_indexes index;
|
|
unsigned cumulativeWeight;
|
|
};
|
|
|
|
_item_indexes GetItemIndexForDroppableItem(bool considerDropRate, tl::function_ref<bool(const ItemData &item)> isItemOkay)
|
|
{
|
|
static std::vector<WeightedItemIndex> ril;
|
|
ril.clear();
|
|
|
|
unsigned cumulativeWeight = 0;
|
|
for (size_t i = 0; i < AllItemsList.size(); i++) {
|
|
if (!IsItemAvailable(static_cast<int>(i)))
|
|
continue;
|
|
const ItemData &item = AllItemsList[i];
|
|
if (item.dropRate == 0)
|
|
continue;
|
|
if (IsAnyOf(item.iSpell, SpellID::Resurrect, SpellID::HealOther) && !gbIsMultiplayer)
|
|
continue;
|
|
if (!isItemOkay(item))
|
|
continue;
|
|
cumulativeWeight += considerDropRate ? item.dropRate : 1;
|
|
ril.push_back({ static_cast<_item_indexes>(i), cumulativeWeight });
|
|
}
|
|
const auto targetWeight = static_cast<unsigned>(RandomIntLessThan(static_cast<int>(cumulativeWeight)));
|
|
return std::upper_bound(ril.begin(), ril.end(), targetWeight, [](unsigned target, const WeightedItemIndex &value) { return target < value.cumulativeWeight; })->index;
|
|
}
|
|
|
|
_item_indexes RndUItem(Monster *monster)
|
|
{
|
|
int itemMaxLevel = ItemsGetCurrlevel() * 2;
|
|
if (monster != nullptr)
|
|
itemMaxLevel = monster->level(sgGameInitInfo.nDifficulty);
|
|
return GetItemIndexForDroppableItem(false, [&itemMaxLevel](const ItemData &item) {
|
|
if (item.itype == ItemType::Misc && item.iMiscId == IMISC_BOOK)
|
|
return true;
|
|
if (itemMaxLevel < item.iMinMLvl)
|
|
return false;
|
|
if (IsAnyOf(item.itype, ItemType::Gold, ItemType::Misc))
|
|
return false;
|
|
return true;
|
|
});
|
|
}
|
|
|
|
_item_indexes RndAllItems()
|
|
{
|
|
if (GenerateRnd(100) > 25)
|
|
return IDI_GOLD;
|
|
|
|
int itemMaxLevel = ItemsGetCurrlevel() * 2;
|
|
return GetItemIndexForDroppableItem(false, [&itemMaxLevel](const ItemData &item) {
|
|
return itemMaxLevel >= item.iMinMLvl;
|
|
});
|
|
}
|
|
|
|
_item_indexes RndTypeItems(ItemType itemType, int imid, int lvl)
|
|
{
|
|
int itemMaxLevel = lvl * 2;
|
|
return GetItemIndexForDroppableItem(false, [&itemMaxLevel, &itemType, &imid](const ItemData &item) {
|
|
if (itemMaxLevel < item.iMinMLvl)
|
|
return false;
|
|
if (item.itype != itemType)
|
|
return false;
|
|
if (imid != -1 && item.iMiscId != imid)
|
|
return false;
|
|
return true;
|
|
});
|
|
}
|
|
|
|
std::vector<uint8_t> GetValidUniques(int lvl, unique_base_item baseItemId)
|
|
{
|
|
std::vector<uint8_t> validUniques;
|
|
int index = 0;
|
|
for (const UniqueItem &itemData : UniqueItems) {
|
|
if (itemData.UIItemId == baseItemId && lvl >= itemData.UIMinLvl) {
|
|
validUniques.push_back(index);
|
|
}
|
|
index++;
|
|
}
|
|
return validUniques;
|
|
}
|
|
|
|
_unique_items CheckUnique(Item &item, int lvl, int uper, int uidOffset = 0)
|
|
{
|
|
if (GenerateRnd(100) > uper)
|
|
return UITEM_INVALID;
|
|
|
|
auto validUniques = GetValidUniques(lvl, AllItemsList[item.IDidx].iItemId);
|
|
|
|
if (validUniques.empty())
|
|
return UITEM_INVALID;
|
|
|
|
DiscardRandomValues(1);
|
|
|
|
// Check if uidOffset is out of bounds
|
|
if (static_cast<size_t>(uidOffset) >= validUniques.size()) {
|
|
return UITEM_INVALID;
|
|
}
|
|
|
|
const uint8_t selectedUniqueIndex = validUniques[validUniques.size() - 1 - uidOffset];
|
|
|
|
return static_cast<_unique_items>(selectedUniqueIndex);
|
|
}
|
|
|
|
void GetUniqueItem(const Player &player, Item &item, _unique_items uid)
|
|
{
|
|
const auto &uniqueItemData = UniqueItems[uid];
|
|
|
|
for (auto power : uniqueItemData.powers) {
|
|
if (power.type == IPL_INVALID)
|
|
break;
|
|
SaveItemPower(player, item, power);
|
|
}
|
|
|
|
CopyUtf8(item._iIName, uniqueItemData.UIName, ItemNameLength);
|
|
if (uniqueItemData.UICurs != ICURS_DEFAULT)
|
|
item._iCurs = uniqueItemData.UICurs;
|
|
item._iIvalue = uniqueItemData.UIValue;
|
|
|
|
if (item._iMiscId == IMISC_UNIQUE)
|
|
item._iSeed = uid;
|
|
|
|
item._iUid = uid;
|
|
item._iMagical = ITEM_QUALITY_UNIQUE;
|
|
item._iCreateInfo |= CF_UNIQUE;
|
|
}
|
|
|
|
void ItemRndDur(Item &item)
|
|
{
|
|
if (item._iDurability > 0 && item._iDurability != DUR_INDESTRUCTIBLE)
|
|
item._iDurability = GenerateRnd(item._iMaxDur / 2) + (item._iMaxDur / 4) + 1;
|
|
}
|
|
|
|
int GetItemBLevel(int lvl, item_misc_id miscId, bool onlygood, bool uper15)
|
|
{
|
|
int iblvl = -1;
|
|
if (GenerateRnd(100) <= 10
|
|
|| GenerateRnd(100) <= lvl
|
|
|| onlygood
|
|
|| IsAnyOf(miscId, IMISC_STAFF, IMISC_RING, IMISC_AMULET)) {
|
|
iblvl = lvl;
|
|
}
|
|
if (uper15)
|
|
iblvl = lvl + 4;
|
|
return iblvl;
|
|
}
|
|
|
|
void SetupBaseItem(Point position, _item_indexes idx, bool onlygood, bool sendmsg, bool delta, bool spawn = false)
|
|
{
|
|
if (ActiveItemCount >= MAXITEMS)
|
|
return;
|
|
|
|
const int ii = AllocateItem();
|
|
auto &item = Items[ii];
|
|
GetSuperItemSpace(position, ii);
|
|
const int curlv = ItemsGetCurrlevel();
|
|
|
|
SetupAllItems(*MyPlayer, item, idx, AdvanceRndSeed(), 2 * curlv, 1, onlygood, delta);
|
|
TryRandomUniqueItem(item, idx, 2 * curlv, 1, onlygood, delta);
|
|
SetupItem(item);
|
|
|
|
if (sendmsg)
|
|
NetSendCmdPItem(false, CMD_DROPITEM, item.position, item);
|
|
if (delta)
|
|
DeltaAddItem(ii);
|
|
if (spawn)
|
|
NetSendCmdPItem(false, CMD_SPAWNITEM, item.position, item);
|
|
}
|
|
|
|
void SetupAllUseful(Item &item, int iseed, int lvl)
|
|
{
|
|
item._iSeed = iseed;
|
|
SetRndSeed(iseed);
|
|
|
|
_item_indexes idx;
|
|
|
|
if (gbIsHellfire) {
|
|
switch (GenerateRnd(7)) {
|
|
case 0:
|
|
idx = IDI_PORTAL;
|
|
if (lvl <= 1)
|
|
idx = IDI_HEAL;
|
|
break;
|
|
case 1:
|
|
case 2:
|
|
idx = IDI_HEAL;
|
|
break;
|
|
case 3:
|
|
idx = IDI_PORTAL;
|
|
if (lvl <= 1)
|
|
idx = IDI_MANA;
|
|
break;
|
|
case 4:
|
|
case 5:
|
|
idx = IDI_MANA;
|
|
break;
|
|
default:
|
|
idx = IDI_OIL;
|
|
break;
|
|
}
|
|
} else {
|
|
idx = PickRandomlyAmong({ IDI_MANA, IDI_HEAL });
|
|
|
|
if (lvl > 1 && FlipCoin(3))
|
|
idx = IDI_PORTAL;
|
|
}
|
|
|
|
GetItemAttrs(item, idx, lvl);
|
|
item._iCreateInfo = lvl | CF_USEFUL;
|
|
SetupItem(item);
|
|
}
|
|
|
|
uint8_t Char2int(uint8_t input)
|
|
{
|
|
if (input >= '0' && input <= '9')
|
|
return input - '0';
|
|
if (input >= 'A' && input <= 'F')
|
|
return input - 'A' + 10;
|
|
return 0;
|
|
}
|
|
|
|
void Hex2bin(const char *src, int bytes, uint8_t *target)
|
|
{
|
|
for (int i = 0; i < bytes; i++, src += 2) {
|
|
target[i] = (Char2int(src[0]) << 4) | Char2int(src[1]);
|
|
}
|
|
}
|
|
|
|
void SpawnRock()
|
|
{
|
|
if (ActiveItemCount >= MAXITEMS)
|
|
return;
|
|
|
|
const Object *stand = nullptr;
|
|
for (int i = 0; i < ActiveObjectCount; i++) {
|
|
const Object &object = Objects[ActiveObjects[i]];
|
|
if (object._otype == OBJ_STAND) {
|
|
stand = &object;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (stand == nullptr)
|
|
return;
|
|
|
|
const int ii = AllocateItem();
|
|
Item &item = Items[ii];
|
|
|
|
item.position = stand->position;
|
|
dItem[item.position.x][item.position.y] = ii + 1;
|
|
const int curlv = ItemsGetCurrlevel();
|
|
GetItemAttrs(item, IDI_ROCK, curlv);
|
|
SetupItem(item);
|
|
item.selectionRegion = SelectionRegion::Middle;
|
|
item._iPostDraw = true;
|
|
item.AnimInfo.currentFrame = 10;
|
|
item._iAnimFlag = true;
|
|
item._iCreateInfo |= CF_PREGEN;
|
|
|
|
DeltaAddItem(ii);
|
|
}
|
|
|
|
void ItemDoppel()
|
|
{
|
|
if (!gbIsMultiplayer)
|
|
return;
|
|
|
|
static int idoppely = 16;
|
|
|
|
for (int idoppelx = 16; idoppelx < 96; idoppelx++) {
|
|
if (dItem[idoppelx][idoppely] != 0) {
|
|
Item *i = &Items[dItem[idoppelx][idoppely] - 1];
|
|
if (i->position.x != idoppelx || i->position.y != idoppely)
|
|
dItem[idoppelx][idoppely] = 0;
|
|
}
|
|
}
|
|
|
|
idoppely++;
|
|
if (idoppely == 96)
|
|
idoppely = 16;
|
|
}
|
|
|
|
void AddItemInfoBoxString(const std::string_view str)
|
|
{
|
|
const bool floatingInfoBoxEnabled = *GetOptions().Gameplay.floatingInfoBox;
|
|
AddInfoBoxString(str, floatingInfoBoxEnabled);
|
|
}
|
|
|
|
void AddItemInfoBoxString(std::string &&str)
|
|
{
|
|
const bool floatingInfoBoxEnabled = *GetOptions().Gameplay.floatingInfoBox;
|
|
AddInfoBoxString(std::move(str), floatingInfoBoxEnabled);
|
|
}
|
|
|
|
void PrintItemOil(char iDidx)
|
|
{
|
|
switch (iDidx) {
|
|
case IMISC_OILACC:
|
|
AddItemInfoBoxString(_("increases a weapon's"));
|
|
AddItemInfoBoxString(_("chance to hit"));
|
|
break;
|
|
case IMISC_OILMAST:
|
|
AddItemInfoBoxString(_("greatly increases a"));
|
|
AddItemInfoBoxString(_("weapon's chance to hit"));
|
|
break;
|
|
case IMISC_OILSHARP:
|
|
AddItemInfoBoxString(_("increases a weapon's"));
|
|
AddItemInfoBoxString(_("damage potential"));
|
|
break;
|
|
case IMISC_OILDEATH:
|
|
AddItemInfoBoxString(_("greatly increases a weapon's"));
|
|
AddItemInfoBoxString(_("damage potential - not bows"));
|
|
break;
|
|
case IMISC_OILSKILL:
|
|
AddItemInfoBoxString(_("reduces attributes needed"));
|
|
AddItemInfoBoxString(_("to use armor or weapons"));
|
|
break;
|
|
case IMISC_OILBSMTH:
|
|
AddItemInfoBoxString(/*xgettext:no-c-format*/ _("restores 20% of an"));
|
|
AddItemInfoBoxString(_("item's durability"));
|
|
break;
|
|
case IMISC_OILFORT:
|
|
AddItemInfoBoxString(_("increases an item's"));
|
|
AddItemInfoBoxString(_("current and max durability"));
|
|
break;
|
|
case IMISC_OILPERM:
|
|
AddItemInfoBoxString(_("makes an item indestructible"));
|
|
break;
|
|
case IMISC_OILHARD:
|
|
AddItemInfoBoxString(_("increases the armor class"));
|
|
AddItemInfoBoxString(_("of armor and shields"));
|
|
break;
|
|
case IMISC_OILIMP:
|
|
AddItemInfoBoxString(_("greatly increases the armor"));
|
|
AddItemInfoBoxString(_("class of armor and shields"));
|
|
break;
|
|
case IMISC_RUNEF:
|
|
AddItemInfoBoxString(_("sets fire trap"));
|
|
break;
|
|
case IMISC_RUNEL:
|
|
case IMISC_GR_RUNEL:
|
|
AddItemInfoBoxString(_("sets lightning trap"));
|
|
break;
|
|
case IMISC_GR_RUNEF:
|
|
AddItemInfoBoxString(_("sets fire trap"));
|
|
break;
|
|
case IMISC_RUNES:
|
|
AddItemInfoBoxString(_("sets petrification trap"));
|
|
break;
|
|
case IMISC_FULLHEAL:
|
|
AddItemInfoBoxString(_("restore all life"));
|
|
break;
|
|
case IMISC_HEAL:
|
|
AddItemInfoBoxString(_("restore some life"));
|
|
break;
|
|
case IMISC_MANA:
|
|
AddItemInfoBoxString(_("restore some mana"));
|
|
break;
|
|
case IMISC_FULLMANA:
|
|
AddItemInfoBoxString(_("restore all mana"));
|
|
break;
|
|
case IMISC_ELIXSTR:
|
|
AddItemInfoBoxString(_("increase strength"));
|
|
break;
|
|
case IMISC_ELIXMAG:
|
|
AddItemInfoBoxString(_("increase magic"));
|
|
break;
|
|
case IMISC_ELIXDEX:
|
|
AddItemInfoBoxString(_("increase dexterity"));
|
|
break;
|
|
case IMISC_ELIXVIT:
|
|
AddItemInfoBoxString(_("increase vitality"));
|
|
break;
|
|
case IMISC_REJUV:
|
|
AddItemInfoBoxString(_("restore some life and mana"));
|
|
break;
|
|
case IMISC_FULLREJUV:
|
|
AddItemInfoBoxString(_("restore all life and mana"));
|
|
break;
|
|
case IMISC_ARENAPOT:
|
|
AddItemInfoBoxString(_("restore all life and mana"));
|
|
AddItemInfoBoxString(_("(works only in arenas)"));
|
|
break;
|
|
}
|
|
}
|
|
|
|
Point DrawUniqueInfoWindow(const Surface &out)
|
|
{
|
|
const bool isInStash = IsStashOpen && GetLeftPanel().contains(MousePosition);
|
|
int panelX, panelY;
|
|
if (isInStash) {
|
|
ClxDraw(out, GetPanelPosition(UiPanels::Stash, { 24 + SidePanelSize.width, 327 }), (*pSTextBoxCels)[0]);
|
|
panelX = GetLeftPanel().position.x + SidePanelSize.width + 27;
|
|
panelY = GetLeftPanel().position.y + 28;
|
|
} else {
|
|
ClxDraw(out, GetPanelPosition(UiPanels::Inventory, { 24 - SidePanelSize.width, 327 }), (*pSTextBoxCels)[0]);
|
|
panelX = GetRightPanel().position.x - SidePanelSize.width + 27;
|
|
panelY = GetRightPanel().position.y + 28;
|
|
}
|
|
|
|
const Point rightInfoPos = GetRightPanel().position - Displacement { SidePanelSize.width, 0 };
|
|
const Point leftInfoPos = GetLeftPanel().position + Displacement { SidePanelSize.width, 0 };
|
|
|
|
const bool isInfoOverlapping = IsLeftPanelOpen() && IsRightPanelOpen() && GetLeftPanel().contains(rightInfoPos);
|
|
const int fadeLevel = isInfoOverlapping ? 3 : 1;
|
|
|
|
for (int i = 0; i < fadeLevel; ++i) {
|
|
DrawHalfTransparentRectTo(out, panelX, panelY, 265, 297);
|
|
}
|
|
|
|
return isInStash ? leftInfoPos : rightInfoPos;
|
|
}
|
|
|
|
void printItemMiscKBM(const Item &item, const bool isOil, const bool isCastOnTarget)
|
|
{
|
|
if (item._iMiscId == IMISC_MAPOFDOOM) {
|
|
AddItemInfoBoxString(_("Right-click to view"));
|
|
} else if (isOil) {
|
|
PrintItemOil(item._iMiscId);
|
|
AddItemInfoBoxString(_("Right-click to use"));
|
|
} else if (isCastOnTarget) {
|
|
AddItemInfoBoxString(_("Right-click to read, then\nleft-click to target"));
|
|
} else if (IsAnyOf(item._iMiscId, IMISC_BOOK, IMISC_NOTE, IMISC_SCROLL, IMISC_SCROLLT)) {
|
|
AddItemInfoBoxString(_("Right-click to read"));
|
|
}
|
|
}
|
|
|
|
void printItemMiscGenericGamepad(const Item &item, const bool isOil, bool isCastOnTarget)
|
|
{
|
|
if (item._iMiscId == IMISC_MAPOFDOOM) {
|
|
AddItemInfoBoxString(_("Activate to view"));
|
|
} else if (isOil) {
|
|
PrintItemOil(item._iMiscId);
|
|
if (!invflag) {
|
|
AddItemInfoBoxString(_("Open inventory to use"));
|
|
} else {
|
|
AddItemInfoBoxString(_("Activate to use"));
|
|
}
|
|
} else if (isCastOnTarget) {
|
|
AddItemInfoBoxString(_("Select from spell book, then\ncast spell to read"));
|
|
} else if (IsAnyOf(item._iMiscId, IMISC_BOOK, IMISC_NOTE, IMISC_SCROLL, IMISC_SCROLLT)) {
|
|
AddItemInfoBoxString(_("Activate to read"));
|
|
}
|
|
}
|
|
|
|
void printItemMiscGamepad(const Item &item, bool isOil, bool isCastOnTarget)
|
|
{
|
|
if (GamepadType == GamepadLayout::Generic) {
|
|
printItemMiscGenericGamepad(item, isOil, isCastOnTarget);
|
|
return;
|
|
}
|
|
const std::string_view activateButton = GetOptions().Padmapper.InputNameForAction("SecondaryAction");
|
|
const std::string_view castButton = GetOptions().Padmapper.InputNameForAction("SpellAction");
|
|
|
|
if (item._iMiscId == IMISC_MAPOFDOOM) {
|
|
AddItemInfoBoxString(fmt::format(fmt::runtime(_("{} to view")), activateButton));
|
|
} else if (isOil) {
|
|
PrintItemOil(item._iMiscId);
|
|
if (!invflag) {
|
|
AddItemInfoBoxString(_("Open inventory to use"));
|
|
} else {
|
|
AddItemInfoBoxString(fmt::format(fmt::runtime(_("{} to use")), activateButton));
|
|
}
|
|
} else if (isCastOnTarget) {
|
|
AddItemInfoBoxString(fmt::format(fmt::runtime(_("Select from spell book,\nthen {} to read")), castButton));
|
|
} else if (IsAnyOf(item._iMiscId, IMISC_BOOK, IMISC_NOTE, IMISC_SCROLL, IMISC_SCROLLT)) {
|
|
AddItemInfoBoxString(fmt::format(fmt::runtime(_("{} to read")), activateButton));
|
|
}
|
|
}
|
|
|
|
void PrintItemMisc(const Item &item)
|
|
{
|
|
if (item._iMiscId == IMISC_EAR) {
|
|
AddItemInfoBoxString(fmt::format(fmt::runtime(pgettext("player", "Level: {:d}")), item._ivalue));
|
|
return;
|
|
}
|
|
if (item._iMiscId == IMISC_AURIC) {
|
|
AddItemInfoBoxString(_("Doubles gold capacity"));
|
|
return;
|
|
}
|
|
const bool isOil = (item._iMiscId >= IMISC_USEFIRST && item._iMiscId <= IMISC_USELAST)
|
|
|| (item._iMiscId > IMISC_OILFIRST && item._iMiscId < IMISC_OILLAST)
|
|
|| (item._iMiscId > IMISC_RUNEFIRST && item._iMiscId < IMISC_RUNELAST)
|
|
|| item._iMiscId == IMISC_ARENAPOT;
|
|
const bool mouseRequiresTarget = (item._iMiscId == IMISC_SCROLLT && item._iSpell != SpellID::Flash)
|
|
|| (item._iMiscId == IMISC_SCROLL && IsAnyOf(item._iSpell, SpellID::TownPortal, SpellID::Identify));
|
|
const bool gamepadRequiresTarget = item.isScroll() && TargetsMonster(item._iSpell);
|
|
|
|
switch (ControlMode) {
|
|
case ControlTypes::None:
|
|
break;
|
|
case ControlTypes::KeyboardAndMouse:
|
|
printItemMiscKBM(item, isOil, mouseRequiresTarget);
|
|
break;
|
|
case ControlTypes::VirtualGamepad:
|
|
printItemMiscGenericGamepad(item, isOil, gamepadRequiresTarget);
|
|
break;
|
|
case ControlTypes::Gamepad:
|
|
printItemMiscGamepad(item, isOil, gamepadRequiresTarget);
|
|
break;
|
|
}
|
|
}
|
|
|
|
void PrintItemInfo(const Item &item)
|
|
{
|
|
PrintItemMisc(item);
|
|
uint8_t str = item._iMinStr;
|
|
uint8_t dex = item._iMinDex;
|
|
uint8_t mag = item._iMinMag;
|
|
if (str != 0 || mag != 0 || dex != 0) {
|
|
std::string text = std::string(_("Required:"));
|
|
if (str != 0)
|
|
text.append(fmt::format(fmt::runtime(_(" {:d} Str")), str));
|
|
if (mag != 0)
|
|
text.append(fmt::format(fmt::runtime(_(" {:d} Mag")), mag));
|
|
if (dex != 0)
|
|
text.append(fmt::format(fmt::runtime(_(" {:d} Dex")), dex));
|
|
AddItemInfoBoxString(text);
|
|
}
|
|
}
|
|
|
|
bool SmithItemOk(const Player & /*player*/, const ItemData &item)
|
|
{
|
|
if (item.itype == ItemType::Misc)
|
|
return false;
|
|
if (item.itype == ItemType::Gold)
|
|
return false;
|
|
if (item.itype == ItemType::Staff && (!gbIsHellfire || IsValidSpell(item.iSpell)))
|
|
return false;
|
|
if (item.itype == ItemType::Ring)
|
|
return false;
|
|
if (item.itype == ItemType::Amulet)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
template <bool (*Ok)(const Player &, const ItemData &), bool ConsiderDropRate = false>
|
|
_item_indexes RndVendorItem(const Player &player, int minlvl, int maxlvl)
|
|
{
|
|
return GetItemIndexForDroppableItem(ConsiderDropRate, [&player, &minlvl, &maxlvl](const ItemData &item) {
|
|
if (!Ok(player, item))
|
|
return false;
|
|
if (item.iMinMLvl < minlvl || item.iMinMLvl > maxlvl)
|
|
return false;
|
|
return true;
|
|
});
|
|
}
|
|
|
|
_item_indexes RndSmithItem(const Player &player, int lvl)
|
|
{
|
|
return RndVendorItem<SmithItemOk, true>(player, 0, lvl);
|
|
}
|
|
|
|
void SortVendor(std::span<Item> itemList, size_t PinnedItemCount)
|
|
{
|
|
auto cmp = [](const Item &a, const Item &b) {
|
|
return a.IDidx < b.IDidx;
|
|
};
|
|
|
|
std::sort(itemList.begin() + PinnedItemCount, itemList.end(), cmp);
|
|
}
|
|
|
|
bool PremiumItemOk(const Player & /*player*/, const ItemData &item)
|
|
{
|
|
if (item.itype == ItemType::Misc)
|
|
return false;
|
|
if (item.itype == ItemType::Gold)
|
|
return false;
|
|
if (!gbIsHellfire && item.itype == ItemType::Staff)
|
|
return false;
|
|
|
|
if (gbIsMultiplayer) {
|
|
if (item.iMiscId == IMISC_OILOF)
|
|
return false;
|
|
if (item.itype == ItemType::Ring)
|
|
return false;
|
|
if (item.itype == ItemType::Amulet)
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
_item_indexes RndPremiumItem(const Player &player, int minlvl, int maxlvl)
|
|
{
|
|
return RndVendorItem<PremiumItemOk>(player, minlvl, maxlvl);
|
|
}
|
|
|
|
void SpawnOnePremium(Item &premiumItem, int plvl, const Player &player)
|
|
{
|
|
int strength = std::max(player.GetMaximumAttributeValue(CharacterAttribute::Strength), player._pStrength);
|
|
int dexterity = std::max(player.GetMaximumAttributeValue(CharacterAttribute::Dexterity), player._pDexterity);
|
|
int magic = std::max(player.GetMaximumAttributeValue(CharacterAttribute::Magic), player._pMagic);
|
|
strength += strength / 5;
|
|
dexterity += dexterity / 5;
|
|
magic += magic / 5;
|
|
|
|
plvl = std::clamp(plvl, 1, 30);
|
|
|
|
const int maxCount = 150;
|
|
const bool unlimited = !gbIsHellfire; // TODO: This could lead to an infinite loop if a suitable item can never be generated
|
|
for (int count = 0; unlimited || count < maxCount; count++) {
|
|
premiumItem = {};
|
|
premiumItem._iSeed = AdvanceRndSeed();
|
|
SetRndSeed(premiumItem._iSeed);
|
|
const _item_indexes itemType = RndPremiumItem(player, plvl / 4, plvl);
|
|
GetItemAttrs(premiumItem, itemType, plvl);
|
|
GetItemBonus(player, premiumItem, plvl / 2, plvl, true, !gbIsHellfire);
|
|
|
|
if (!gbIsHellfire) {
|
|
if (premiumItem._iIvalue <= MaxVendorValue) {
|
|
break;
|
|
}
|
|
} else {
|
|
int itemValue = 0;
|
|
switch (premiumItem._itype) {
|
|
case ItemType::LightArmor:
|
|
case ItemType::MediumArmor:
|
|
case ItemType::HeavyArmor: {
|
|
const auto *const mostValuablePlayerArmor = player.GetMostValuableItem(
|
|
[](const Item &item) {
|
|
return IsAnyOf(item._itype, ItemType::LightArmor, ItemType::MediumArmor, ItemType::HeavyArmor);
|
|
});
|
|
|
|
itemValue = mostValuablePlayerArmor == nullptr ? 0 : mostValuablePlayerArmor->_iIvalue;
|
|
break;
|
|
}
|
|
case ItemType::Shield:
|
|
case ItemType::Axe:
|
|
case ItemType::Bow:
|
|
case ItemType::Mace:
|
|
case ItemType::Sword:
|
|
case ItemType::Helm:
|
|
case ItemType::Staff:
|
|
case ItemType::Ring:
|
|
case ItemType::Amulet: {
|
|
const auto *const mostValuablePlayerItem = player.GetMostValuableItem(
|
|
[filterType = premiumItem._itype](const Item &item) { return item._itype == filterType; });
|
|
|
|
itemValue = mostValuablePlayerItem == nullptr ? 0 : mostValuablePlayerItem->_iIvalue;
|
|
break;
|
|
}
|
|
default:
|
|
itemValue = 0;
|
|
break;
|
|
}
|
|
itemValue = itemValue * 4 / 5; // avoids forced int > float > int conversion
|
|
if (premiumItem._iIvalue <= MaxVendorValueHf
|
|
&& premiumItem._iMinStr <= strength
|
|
&& premiumItem._iMinMag <= magic
|
|
&& premiumItem._iMinDex <= dexterity
|
|
&& premiumItem._iIvalue >= itemValue) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
premiumItem._iCreateInfo = plvl | CF_SMITHPREMIUM;
|
|
premiumItem._iIdentified = true;
|
|
premiumItem._iStatFlag = player.CanUseItem(premiumItem);
|
|
}
|
|
|
|
bool WitchItemOk(const Player & /*player*/, const ItemData &item)
|
|
{
|
|
if (IsNoneOf(item.itype, ItemType::Misc, ItemType::Staff))
|
|
return false;
|
|
if (item.iMiscId == IMISC_MANA)
|
|
return false;
|
|
if (item.iMiscId == IMISC_FULLMANA)
|
|
return false;
|
|
if (item.iSpell == SpellID::TownPortal)
|
|
return false;
|
|
if (item.iMiscId == IMISC_FULLHEAL)
|
|
return false;
|
|
if (item.iMiscId == IMISC_HEAL)
|
|
return false;
|
|
if (item.iMiscId > IMISC_OILFIRST && item.iMiscId < IMISC_OILLAST)
|
|
return false;
|
|
if (item.iSpell == SpellID::Resurrect && !gbIsMultiplayer)
|
|
return false;
|
|
if (item.iSpell == SpellID::HealOther && !gbIsMultiplayer)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
_item_indexes RndWitchItem(const Player &player, int lvl)
|
|
{
|
|
return RndVendorItem<WitchItemOk>(player, 0, lvl);
|
|
}
|
|
|
|
_item_indexes RndBoyItem(const Player &player, int lvl)
|
|
{
|
|
return RndVendorItem<PremiumItemOk>(player, 0, lvl);
|
|
}
|
|
|
|
bool HealerItemOk(const Player &player, const ItemData &item)
|
|
{
|
|
if (item.itype != ItemType::Misc)
|
|
return false;
|
|
|
|
if (item.iMiscId == IMISC_SCROLL)
|
|
return item.iSpell == SpellID::Healing;
|
|
if (item.iMiscId == IMISC_SCROLLT)
|
|
return item.iSpell == SpellID::HealOther && gbIsMultiplayer;
|
|
|
|
if (!gbIsMultiplayer) {
|
|
if (item.iMiscId == IMISC_ELIXSTR)
|
|
return !gbIsHellfire || player._pBaseStr < player.GetMaximumAttributeValue(CharacterAttribute::Strength);
|
|
if (item.iMiscId == IMISC_ELIXMAG)
|
|
return !gbIsHellfire || player._pBaseMag < player.GetMaximumAttributeValue(CharacterAttribute::Magic);
|
|
if (item.iMiscId == IMISC_ELIXDEX)
|
|
return !gbIsHellfire || player._pBaseDex < player.GetMaximumAttributeValue(CharacterAttribute::Dexterity);
|
|
if (item.iMiscId == IMISC_ELIXVIT)
|
|
return !gbIsHellfire || player._pBaseVit < player.GetMaximumAttributeValue(CharacterAttribute::Vitality);
|
|
}
|
|
|
|
if (item.iMiscId == IMISC_REJUV)
|
|
return true;
|
|
if (item.iMiscId == IMISC_FULLREJUV)
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
_item_indexes RndHealerItem(const Player &player, int lvl)
|
|
{
|
|
return RndVendorItem<HealerItemOk>(player, 0, lvl);
|
|
}
|
|
|
|
void RecreateSmithItem(const Player &player, Item &item, int lvl, int iseed)
|
|
{
|
|
SetRndSeed(iseed);
|
|
const _item_indexes itype = RndSmithItem(player, lvl);
|
|
GetItemAttrs(item, itype, lvl);
|
|
|
|
item._iSeed = iseed;
|
|
item._iCreateInfo = lvl | CF_SMITH;
|
|
item._iIdentified = true;
|
|
}
|
|
|
|
void RecreatePremiumItem(const Player &player, Item &item, int plvl, int iseed)
|
|
{
|
|
SetRndSeed(iseed);
|
|
const _item_indexes itype = RndPremiumItem(player, plvl / 4, plvl);
|
|
GetItemAttrs(item, itype, plvl);
|
|
GetItemBonus(player, item, plvl / 2, plvl, true, !gbIsHellfire);
|
|
|
|
item._iSeed = iseed;
|
|
item._iCreateInfo = plvl | CF_SMITHPREMIUM;
|
|
item._iIdentified = true;
|
|
}
|
|
|
|
void RecreateBoyItem(const Player &player, Item &item, int lvl, int iseed)
|
|
{
|
|
SetRndSeed(iseed);
|
|
const _item_indexes itype = RndBoyItem(player, lvl);
|
|
GetItemAttrs(item, itype, lvl);
|
|
GetItemBonus(player, item, lvl, 2 * lvl, true, true);
|
|
|
|
item._iSeed = iseed;
|
|
item._iCreateInfo = lvl | CF_BOY;
|
|
item._iIdentified = true;
|
|
}
|
|
|
|
void RecreateWitchItem(const Player &player, Item &item, _item_indexes idx, int lvl, int iseed)
|
|
{
|
|
if (IsAnyOf(idx, IDI_MANA, IDI_FULLMANA, IDI_PORTAL)) {
|
|
GetItemAttrs(item, idx, lvl);
|
|
} else if (gbIsHellfire && idx >= 114 && idx <= 117) {
|
|
SetRndSeed(iseed);
|
|
DiscardRandomValues(1);
|
|
GetItemAttrs(item, idx, lvl);
|
|
} else {
|
|
SetRndSeed(iseed);
|
|
const _item_indexes itype = RndWitchItem(player, lvl);
|
|
GetItemAttrs(item, itype, lvl);
|
|
int iblvl = -1;
|
|
if (GenerateRnd(100) <= 5)
|
|
iblvl = 2 * lvl;
|
|
if (iblvl == -1 && item._iMiscId == IMISC_STAFF)
|
|
iblvl = 2 * lvl;
|
|
if (iblvl != -1)
|
|
GetItemBonus(player, item, iblvl / 2, iblvl, true, true);
|
|
}
|
|
|
|
item._iSeed = iseed;
|
|
item._iCreateInfo = lvl | CF_WITCH;
|
|
item._iIdentified = true;
|
|
}
|
|
|
|
void RecreateHealerItem(const Player &player, Item &item, _item_indexes idx, int lvl, int iseed)
|
|
{
|
|
if (IsAnyOf(idx, IDI_HEAL, IDI_FULLHEAL, IDI_RESURRECT)) {
|
|
GetItemAttrs(item, idx, lvl);
|
|
} else {
|
|
SetRndSeed(iseed);
|
|
const _item_indexes itype = RndHealerItem(player, lvl);
|
|
GetItemAttrs(item, itype, lvl);
|
|
}
|
|
|
|
item._iSeed = iseed;
|
|
item._iCreateInfo = lvl | CF_HEALER;
|
|
item._iIdentified = true;
|
|
}
|
|
|
|
void RecreateTownItem(const Player &player, Item &item, _item_indexes idx, uint16_t icreateinfo, int iseed)
|
|
{
|
|
if ((icreateinfo & CF_SMITH) != 0)
|
|
RecreateSmithItem(player, item, icreateinfo & CF_LEVEL, iseed);
|
|
else if ((icreateinfo & CF_SMITHPREMIUM) != 0)
|
|
RecreatePremiumItem(player, item, icreateinfo & CF_LEVEL, iseed);
|
|
else if ((icreateinfo & CF_BOY) != 0)
|
|
RecreateBoyItem(player, item, icreateinfo & CF_LEVEL, iseed);
|
|
else if ((icreateinfo & CF_WITCH) != 0)
|
|
RecreateWitchItem(player, item, idx, icreateinfo & CF_LEVEL, iseed);
|
|
else if ((icreateinfo & CF_HEALER) != 0)
|
|
RecreateHealerItem(player, item, idx, icreateinfo & CF_LEVEL, iseed);
|
|
}
|
|
|
|
void CreateMagicItem(Point position, int lvl, ItemType itemType, int imid, int icurs, bool sendmsg, bool delta, bool spawn = false)
|
|
{
|
|
if (ActiveItemCount >= MAXITEMS)
|
|
return;
|
|
|
|
const int ii = AllocateItem();
|
|
auto &item = Items[ii];
|
|
_item_indexes idx = RndTypeItems(itemType, imid, lvl);
|
|
|
|
while (true) {
|
|
item = {};
|
|
SetupAllItems(*MyPlayer, item, idx, AdvanceRndSeed(), 2 * lvl, 1, true, delta);
|
|
TryRandomUniqueItem(item, idx, 2 * lvl, 1, true, delta);
|
|
SetupItem(item);
|
|
if (item._iCurs == icurs)
|
|
break;
|
|
|
|
idx = RndTypeItems(itemType, imid, lvl);
|
|
}
|
|
GetSuperItemSpace(position, ii);
|
|
|
|
if (sendmsg)
|
|
NetSendCmdPItem(false, CMD_DROPITEM, item.position, item);
|
|
if (delta)
|
|
DeltaAddItem(ii);
|
|
if (spawn)
|
|
NetSendCmdPItem(false, CMD_SPAWNITEM, item.position, item);
|
|
}
|
|
|
|
void NextItemRecord(int i)
|
|
{
|
|
gnNumGetRecords--;
|
|
|
|
if (gnNumGetRecords == 0) {
|
|
return;
|
|
}
|
|
|
|
itemrecord[i].dwTimestamp = itemrecord[gnNumGetRecords].dwTimestamp;
|
|
itemrecord[i].nSeed = itemrecord[gnNumGetRecords].nSeed;
|
|
itemrecord[i].wCI = itemrecord[gnNumGetRecords].wCI;
|
|
itemrecord[i].nIndex = itemrecord[gnNumGetRecords].nIndex;
|
|
}
|
|
|
|
StringOrView GetTranslatedItemName(const Item &item)
|
|
{
|
|
const auto &baseItemData = AllItemsList[static_cast<size_t>(item.IDidx)];
|
|
|
|
if (item._iCreateInfo == 0) {
|
|
return _(baseItemData.iName);
|
|
}
|
|
if (item._iMiscId == IMISC_BOOK) {
|
|
const std::string_view spellName = pgettext("spell", GetSpellData(item._iSpell).sNameText);
|
|
return fmt::format(fmt::runtime(_(/* TRANSLATORS: {:s} will be a spell name */ "Book of {:s}")), spellName);
|
|
}
|
|
if (item._iMiscId == IMISC_EAR) {
|
|
return fmt::format(fmt::runtime(_(/* TRANSLATORS: {:s} will be a Character Name */ "Ear of {:s}")), item._iIName);
|
|
}
|
|
if (item._iMiscId > IMISC_OILFIRST && item._iMiscId < IMISC_OILLAST) {
|
|
for (size_t i = 0; i < 10; i++) {
|
|
if (OilMagic[i] != item._iMiscId)
|
|
continue;
|
|
return _(OilNames[i]);
|
|
}
|
|
app_fatal("unknown oil");
|
|
} else if (item._itype == ItemType::Staff && item._iSpell != SpellID::Null && item._iMagical != ITEM_QUALITY_UNIQUE) {
|
|
return GenerateStaffName(baseItemData, item._iSpell, true);
|
|
} else {
|
|
return _(baseItemData.iName);
|
|
}
|
|
}
|
|
|
|
std::string GetTranslatedItemNameMagical(const Item &item, bool hellfireItem, bool translate, std::optional<bool> forceNameLengthCheck)
|
|
{
|
|
std::string identifiedName;
|
|
const auto &baseItemData = AllItemsList[static_cast<size_t>(item.IDidx)];
|
|
|
|
const int lvl = item._iCreateInfo & CF_LEVEL;
|
|
const bool onlygood = (item._iCreateInfo & (CF_ONLYGOOD | CF_SMITHPREMIUM | CF_BOY | CF_WITCH)) != 0;
|
|
|
|
const uint32_t currentSeed = GetLCGEngineState();
|
|
SetRndSeed(item._iSeed);
|
|
|
|
int minlvl;
|
|
int maxlvl;
|
|
if ((item._iCreateInfo & CF_SMITHPREMIUM) != 0) {
|
|
DiscardRandomValues(2); // RndVendorItem and GetItemAttrs
|
|
minlvl = lvl / 2;
|
|
maxlvl = lvl;
|
|
} else if ((item._iCreateInfo & CF_BOY) != 0) {
|
|
DiscardRandomValues(2); // RndVendorItem and GetItemAttrs
|
|
minlvl = lvl;
|
|
maxlvl = lvl * 2;
|
|
} else if ((item._iCreateInfo & CF_WITCH) != 0) {
|
|
DiscardRandomValues(2); // RndVendorItem and GetItemAttrs
|
|
int iblvl = -1;
|
|
if (GenerateRnd(100) <= 5)
|
|
iblvl = 2 * lvl;
|
|
if (iblvl == -1 && item._iMiscId == IMISC_STAFF)
|
|
iblvl = 2 * lvl;
|
|
minlvl = iblvl / 2;
|
|
maxlvl = iblvl;
|
|
} else {
|
|
DiscardRandomValues(1); // GetItemAttrs
|
|
const int iblvl = GetItemBLevel(lvl, item._iMiscId, onlygood, (item._iCreateInfo & CF_UPER15) != 0);
|
|
minlvl = iblvl / 2;
|
|
maxlvl = iblvl;
|
|
DiscardRandomValues(1); // CheckUnique
|
|
}
|
|
|
|
minlvl = std::min(minlvl, 25);
|
|
|
|
AffixItemType affixItemType = AffixItemType::None;
|
|
|
|
switch (item._itype) {
|
|
case ItemType::Sword:
|
|
case ItemType::Axe:
|
|
case ItemType::Mace:
|
|
affixItemType = AffixItemType::Weapon;
|
|
break;
|
|
case ItemType::Bow:
|
|
affixItemType = AffixItemType::Bow;
|
|
break;
|
|
case ItemType::Shield:
|
|
affixItemType = AffixItemType::Shield;
|
|
break;
|
|
case ItemType::LightArmor:
|
|
case ItemType::Helm:
|
|
case ItemType::MediumArmor:
|
|
case ItemType::HeavyArmor:
|
|
affixItemType = AffixItemType::Armor;
|
|
break;
|
|
case ItemType::Staff: {
|
|
const bool allowspells = !hellfireItem || ((item._iCreateInfo & CF_SMITHPREMIUM) == 0);
|
|
|
|
if (!allowspells)
|
|
affixItemType = AffixItemType::Staff;
|
|
else if (!hellfireItem && FlipCoin(4)) {
|
|
affixItemType = AffixItemType::Staff;
|
|
minlvl = maxlvl / 2;
|
|
} else {
|
|
DiscardRandomValues(2); // Spell and Charges
|
|
|
|
std::optional<const PLStruct *> prefix = GetStaffPrefix(maxlvl, onlygood);
|
|
if (!prefix.has_value() || item._iSpell == SpellID::Null) {
|
|
if (forceNameLengthCheck) {
|
|
// We generate names to check if it's a diablo or hellfire item. This checks fails => invalid item => don't generate a item name
|
|
identifiedName.clear();
|
|
} else {
|
|
// This can happen, if the item is hacked or a bug in the logic exists
|
|
LogWarn("GetTranslatedItemNameMagical failed for item '{}' with prefix '{}' and spellid '{}'", item._iIName, prefix.has_value() ? (*prefix)->PLName : "NULL", static_cast<std::underlying_type_t<SpellID>>(item._iSpell));
|
|
identifiedName = item._iIName;
|
|
}
|
|
} else {
|
|
identifiedName = GenerateStaffNameMagical(baseItemData, item._iSpell, **prefix, translate, forceNameLengthCheck);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case ItemType::Ring:
|
|
case ItemType::Amulet:
|
|
affixItemType = AffixItemType::Misc;
|
|
break;
|
|
case ItemType::None:
|
|
case ItemType::Misc:
|
|
case ItemType::Gold:
|
|
break;
|
|
}
|
|
|
|
if (affixItemType != AffixItemType::None) {
|
|
const PLStruct *pPrefix = nullptr;
|
|
const PLStruct *pSufix = nullptr;
|
|
GetItemPowerPrefixAndSuffix(
|
|
minlvl, maxlvl, affixItemType, onlygood,
|
|
[&pPrefix](const PLStruct &prefix) {
|
|
pPrefix = &prefix;
|
|
// GenerateRnd(prefix.power.param2 - prefix.power.param2 + 1)
|
|
DiscardRandomValues(1);
|
|
switch (pPrefix->power.type) {
|
|
case IPL_DOPPELGANGER:
|
|
case IPL_TOHIT_DAMP:
|
|
DiscardRandomValues(2);
|
|
break;
|
|
case IPL_TOHIT_DAMP_CURSE:
|
|
DiscardRandomValues(1);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
},
|
|
[&pSufix](const PLStruct &suffix) {
|
|
pSufix = &suffix;
|
|
});
|
|
|
|
identifiedName = GenerateMagicItemName(_(baseItemData.iName), pPrefix, pSufix, translate);
|
|
if (forceNameLengthCheck ? *forceNameLengthCheck : !StringInPanel(identifiedName.c_str())) {
|
|
identifiedName = GenerateMagicItemName(_(baseItemData.iSName), pPrefix, pSufix, translate);
|
|
}
|
|
}
|
|
|
|
SetRndSeed(currentSeed);
|
|
return identifiedName;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
bool IsItemAvailable(int i)
|
|
{
|
|
if (i < 0 || i >= static_cast<int>(AllItemsList.size()))
|
|
return false;
|
|
|
|
if (gbIsSpawn) {
|
|
if (i >= 62 && i <= 70)
|
|
return false; // Medium and heavy armors
|
|
if (IsAnyOf(i, 105, 107, 108, 110, 111, 113))
|
|
return false; // Unavailable scrolls
|
|
}
|
|
|
|
if (gbIsHellfire)
|
|
return true;
|
|
|
|
return (
|
|
i != IDI_MAPOFDOOM // Cathedral Map
|
|
&& i != IDI_LGTFORGE // Bovine Plate
|
|
&& (i < IDI_OIL || i > IDI_GREYSUIT) // Hellfire exclusive items
|
|
&& (i < 83 || i > 86) // Oils
|
|
&& i != 92 // Scroll of Search
|
|
&& (i < 161 || i > 165) // Runes
|
|
&& i != IDI_SORCERER // Short Staff of Mana
|
|
)
|
|
|| (
|
|
// Bard items are technically Hellfire-exclusive
|
|
// but are just normal items with adjusted stats.
|
|
*GetOptions().Gameplay.testBard && IsAnyOf(i, IDI_BARDSWORD, IDI_BARDDAGGER));
|
|
}
|
|
|
|
uint8_t GetOutlineColor(const Item &item, bool checkReq)
|
|
{
|
|
if (checkReq && !item._iStatFlag)
|
|
return ICOL_RED;
|
|
if (item._itype == ItemType::Gold)
|
|
return ICOL_YELLOW;
|
|
if (item._iMagical == ITEM_QUALITY_MAGIC)
|
|
return ICOL_BLUE;
|
|
if (item._iMagical == ITEM_QUALITY_UNIQUE)
|
|
return ICOL_YELLOW;
|
|
|
|
return ICOL_WHITE;
|
|
}
|
|
|
|
void ClearUniqueItemFlags()
|
|
{
|
|
memset(UniqueItemFlags, 0, sizeof(UniqueItemFlags));
|
|
}
|
|
|
|
void InitItemGFX()
|
|
{
|
|
char arglist[64];
|
|
|
|
const int itemTypes = gbIsHellfire ? ITEMTYPES : 35;
|
|
for (int i = 0; i < itemTypes; i++) {
|
|
*BufCopy(arglist, "items\\", ItemDropNames[i]) = '\0';
|
|
itemanims[i] = LoadCel(arglist, ItemAnimWidth);
|
|
}
|
|
}
|
|
|
|
void InitItems()
|
|
{
|
|
ActiveItemCount = 0;
|
|
memset(dItem, 0, sizeof(dItem));
|
|
|
|
for (auto &item : Items) {
|
|
item.clear();
|
|
item.position = { 0, 0 };
|
|
item._iAnimFlag = false;
|
|
item.selectionRegion = SelectionRegion::None;
|
|
item._iIdentified = false;
|
|
item._iPostDraw = false;
|
|
}
|
|
|
|
for (uint8_t i = 0; i < MAXITEMS; i++) {
|
|
ActiveItems[i] = i;
|
|
}
|
|
|
|
if (!setlevel) {
|
|
DiscardRandomValues(1);
|
|
if (Quests[Q_ROCK].IsAvailable())
|
|
SpawnRock();
|
|
if (Quests[Q_ANVIL].IsAvailable())
|
|
SpawnQuestItem(IDI_ANVIL, SetPiece.position.megaToWorld() + Displacement { 11, 11 }, 0, SelectionRegion::Bottom, false);
|
|
if (sgGameInitInfo.bCowQuest != 0 && currlevel == 20)
|
|
SpawnQuestItem(IDI_BROWNSUIT, { 25, 25 }, 3, SelectionRegion::Bottom, false);
|
|
if (sgGameInitInfo.bCowQuest != 0 && currlevel == 19)
|
|
SpawnQuestItem(IDI_GREYSUIT, { 25, 25 }, 3, SelectionRegion::Bottom, false);
|
|
// In multiplayer items spawn during level generation to avoid desyncs
|
|
if (gbIsMultiplayer) {
|
|
if (Quests[Q_MUSHROOM].IsAvailable())
|
|
SpawnQuestItem(IDI_FUNGALTM, { 0, 0 }, 5, SelectionRegion::Bottom, false);
|
|
if (currlevel == Quests[Q_VEIL]._qlevel + 1 && Quests[Q_VEIL]._qactive != QUEST_NOTAVAIL)
|
|
SpawnQuestItem(IDI_GLDNELIX, { 0, 0 }, 5, SelectionRegion::Bottom, false);
|
|
}
|
|
if (currlevel > 0 && currlevel < 16)
|
|
AddInitItems();
|
|
if (currlevel >= 21 && currlevel <= 23)
|
|
SpawnNote();
|
|
}
|
|
|
|
ShowUniqueItemInfoBox = false;
|
|
|
|
initItemGetRecords();
|
|
}
|
|
|
|
int GetBonusAC(const Item &item)
|
|
{
|
|
if (item._iPLAC != 0) {
|
|
int tempAc = item._iAC;
|
|
tempAc *= item._iPLAC;
|
|
tempAc /= 100;
|
|
if (tempAc == 0)
|
|
tempAc = math::Sign(item._iPLAC);
|
|
return tempAc;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
void CalcPlrDamage(Player &player, int minDamage, int maxDamage)
|
|
{
|
|
const uint8_t playerLevel = player.getCharacterLevel();
|
|
|
|
if (minDamage == 0 && maxDamage == 0) {
|
|
minDamage = 1;
|
|
maxDamage = 1;
|
|
|
|
if (player.isHoldingItem(ItemType::Shield)) {
|
|
maxDamage = 3;
|
|
}
|
|
|
|
if (player._pClass == HeroClass::Monk) {
|
|
minDamage = std::max(minDamage, playerLevel / 2);
|
|
maxDamage = std::max<int>(maxDamage, playerLevel);
|
|
}
|
|
}
|
|
|
|
player._pIMinDam = minDamage;
|
|
player._pIMaxDam = maxDamage;
|
|
}
|
|
|
|
void CalcPlrPrimaryStats(Player &player, int strength, int &magic, int dexterity, int &vitality)
|
|
{
|
|
const uint8_t playerLevel = player.getCharacterLevel();
|
|
|
|
if (HasAnyOf(player._pSpellFlags, SpellFlag::RageActive)) {
|
|
strength += 2 * playerLevel;
|
|
dexterity += playerLevel + playerLevel / 2;
|
|
vitality += 2 * playerLevel;
|
|
}
|
|
if (HasAnyOf(player._pSpellFlags, SpellFlag::RageCooldown)) {
|
|
strength -= 2 * playerLevel;
|
|
dexterity -= playerLevel + playerLevel / 2;
|
|
vitality -= 2 * playerLevel;
|
|
}
|
|
|
|
player._pStrength = std::clamp(strength + player._pBaseStr, 0, 750);
|
|
player._pMagic = std::clamp(magic + player._pBaseMag, 0, 750);
|
|
player._pDexterity = std::clamp(dexterity + player._pBaseDex, 0, 750);
|
|
player._pVitality = std::clamp(vitality + player._pBaseVit, 0, 750);
|
|
}
|
|
|
|
void CalcPlrLightRadius(Player &player, int lrad)
|
|
{
|
|
lrad = std::clamp(lrad, 2, 15);
|
|
|
|
if (player._pLightRad != lrad) {
|
|
if (player.isOnActiveLevel()) {
|
|
ChangeLightRadius(player.lightId, lrad);
|
|
ChangeVisionRadius(player.getId(), lrad);
|
|
}
|
|
player._pLightRad = lrad;
|
|
}
|
|
}
|
|
|
|
void CalcPlrDamageMod(Player &player)
|
|
{
|
|
const uint8_t playerLevel = player.getCharacterLevel();
|
|
const Item &leftHandItem = player.InvBody[INVLOC_HAND_LEFT];
|
|
const Item &rightHandItem = player.InvBody[INVLOC_HAND_RIGHT];
|
|
const int strMod = playerLevel * player._pStrength;
|
|
const int strDexMod = playerLevel * (player._pStrength + player._pDexterity);
|
|
|
|
switch (player._pClass) {
|
|
case HeroClass::Rogue:
|
|
player._pDamageMod = strDexMod / 200;
|
|
break;
|
|
case HeroClass::Monk:
|
|
if (player.isHoldingItem(ItemType::Staff) || (leftHandItem.isEmpty() && rightHandItem.isEmpty())) {
|
|
player._pDamageMod = strDexMod / 150;
|
|
} else {
|
|
player._pDamageMod = strDexMod / 300;
|
|
}
|
|
break;
|
|
case HeroClass::Bard:
|
|
if (player.isHoldingItem(ItemType::Sword)) {
|
|
player._pDamageMod = strDexMod / 150;
|
|
} else if (player.isHoldingItem(ItemType::Bow)) {
|
|
player._pDamageMod = strDexMod / 250;
|
|
} else {
|
|
player._pDamageMod = strMod / 100;
|
|
}
|
|
break;
|
|
case HeroClass::Barbarian:
|
|
if (player.isHoldingItem(ItemType::Axe) || player.isHoldingItem(ItemType::Mace)) {
|
|
player._pDamageMod = strMod / 75;
|
|
} else if (player.isHoldingItem(ItemType::Bow)) {
|
|
player._pDamageMod = strMod / 300;
|
|
} else {
|
|
player._pDamageMod = strMod / 100;
|
|
}
|
|
if (player.isHoldingItem(ItemType::Shield)) {
|
|
if (leftHandItem._itype == ItemType::Shield)
|
|
player._pIAC -= leftHandItem._iAC / 2;
|
|
else if (rightHandItem._itype == ItemType::Shield)
|
|
player._pIAC -= rightHandItem._iAC / 2;
|
|
} else if (!player.isHoldingItem(ItemType::Staff) && !player.isHoldingItem(ItemType::Bow)) {
|
|
player._pDamageMod += playerLevel * player._pVitality / 100;
|
|
}
|
|
break;
|
|
default:
|
|
player._pDamageMod = strMod / 100;
|
|
break;
|
|
}
|
|
|
|
const ClassAttributes &classAttributes = GetClassAttributes(player._pClass);
|
|
if (HasAnyOf(classAttributes.classFlags, PlayerClassFlag::IronSkin)) {
|
|
player._pIAC += playerLevel / 4;
|
|
}
|
|
}
|
|
|
|
void CalcPlrResistances(Player &player, ItemSpecialEffect iflgs, int fire, int lightning, int magic)
|
|
{
|
|
const uint8_t playerLevel = player.getCharacterLevel();
|
|
|
|
const ClassAttributes &classAttributes = GetClassAttributes(player._pClass);
|
|
|
|
if (HasAnyOf(classAttributes.classFlags, PlayerClassFlag::NaturalResistance)) {
|
|
magic += playerLevel;
|
|
fire += playerLevel;
|
|
lightning += playerLevel;
|
|
}
|
|
|
|
if (HasAnyOf(player._pSpellFlags, SpellFlag::RageCooldown)) {
|
|
magic -= playerLevel;
|
|
fire -= playerLevel;
|
|
lightning -= playerLevel;
|
|
}
|
|
|
|
if (HasAnyOf(iflgs, ItemSpecialEffect::ZeroResistance)) {
|
|
// reset resistances to zero if the respective special effect is active
|
|
magic = 0;
|
|
fire = 0;
|
|
lightning = 0;
|
|
}
|
|
|
|
player._pMagResist = std::clamp(magic, 0, MaxResistance);
|
|
player._pFireResist = std::clamp(fire, 0, MaxResistance);
|
|
player._pLghtResist = std::clamp(lightning, 0, MaxResistance);
|
|
}
|
|
|
|
void CalcPlrLifeMana(Player &player, int vitality, int magic, int life, int mana)
|
|
{
|
|
const ClassAttributes &playerClassAttributes = player.getClassAttributes();
|
|
vitality = (vitality * playerClassAttributes.itmLife) >> 6;
|
|
life += (vitality << 6);
|
|
|
|
magic = (magic * playerClassAttributes.itmMana) >> 6;
|
|
mana += (magic << 6);
|
|
|
|
player._pMaxHP = std::clamp(life + player._pMaxHPBase, 1 << 6, 2000 << 6);
|
|
player._pHitPoints = std::min(life + player._pHPBase, player._pMaxHP);
|
|
|
|
if (&player == MyPlayer && player.hasNoLife()) {
|
|
SetPlayerHitPoints(player, 0);
|
|
}
|
|
|
|
player._pMaxMana = std::clamp(mana + player._pMaxManaBase, 0, 2000 << 6);
|
|
player._pMana = std::min(mana + player._pManaBase, player._pMaxMana);
|
|
}
|
|
|
|
void CalcPlrBlockFlag(Player &player)
|
|
{
|
|
const auto &leftHandItem = player.InvBody[INVLOC_HAND_LEFT];
|
|
const auto &rightHandItem = player.InvBody[INVLOC_HAND_RIGHT];
|
|
|
|
player._pBlockFlag = false;
|
|
|
|
if (player._pClass == HeroClass::Monk) {
|
|
if (player.isHoldingItem(ItemType::Staff)) {
|
|
player._pBlockFlag = true;
|
|
player._pIFlags |= ItemSpecialEffect::FastBlock;
|
|
} else if ((leftHandItem.isEmpty() && rightHandItem.isEmpty()) || (leftHandItem._iClass == ICLASS_WEAPON && leftHandItem._iLoc != ILOC_TWOHAND && rightHandItem.isEmpty()) || (rightHandItem._iClass == ICLASS_WEAPON && rightHandItem._iLoc != ILOC_TWOHAND && leftHandItem.isEmpty())) {
|
|
player._pBlockFlag = true;
|
|
}
|
|
}
|
|
|
|
player._pBlockFlag = player._pBlockFlag || player.isHoldingItem(ItemType::Shield);
|
|
}
|
|
|
|
PlayerWeaponGraphic GetPlrAnimWeaponId(const Player &player)
|
|
{
|
|
const Item &leftHandItem = player.InvBody[INVLOC_HAND_LEFT];
|
|
const Item &rightHandItem = player.InvBody[INVLOC_HAND_RIGHT];
|
|
const bool holdsShield = player.isHoldingItem(ItemType::Shield);
|
|
const bool leftHandUsable = player.CanUseItem(leftHandItem);
|
|
const bool rightHandUsable = player.CanUseItem(rightHandItem);
|
|
ItemType weaponItemType = ItemType::None;
|
|
|
|
if (!leftHandItem.isEmpty() && leftHandItem._iClass == ICLASS_WEAPON && leftHandUsable) {
|
|
weaponItemType = leftHandItem._itype;
|
|
}
|
|
|
|
if (!rightHandItem.isEmpty() && rightHandItem._iClass == ICLASS_WEAPON && rightHandUsable) {
|
|
weaponItemType = rightHandItem._itype;
|
|
}
|
|
|
|
switch (weaponItemType) {
|
|
case ItemType::Sword:
|
|
return holdsShield ? PlayerWeaponGraphic::SwordShield : PlayerWeaponGraphic::Sword;
|
|
case ItemType::Axe:
|
|
return PlayerWeaponGraphic::Axe;
|
|
case ItemType::Bow:
|
|
return PlayerWeaponGraphic::Bow;
|
|
case ItemType::Mace:
|
|
return holdsShield ? PlayerWeaponGraphic::MaceShield : PlayerWeaponGraphic::Mace;
|
|
case ItemType::Staff:
|
|
return PlayerWeaponGraphic::Staff;
|
|
default:
|
|
return holdsShield ? PlayerWeaponGraphic::UnarmedShield : PlayerWeaponGraphic::Unarmed;
|
|
}
|
|
}
|
|
|
|
PlayerArmorGraphic GetPlrAnimArmorId(Player &player)
|
|
{
|
|
const Item &chestItem = player.InvBody[INVLOC_CHEST];
|
|
const bool chestUsable = player.CanUseItem(chestItem);
|
|
const uint8_t playerLevel = player.getCharacterLevel();
|
|
|
|
if (chestUsable) {
|
|
switch (chestItem._itype) {
|
|
case ItemType::HeavyArmor:
|
|
if (player._pClass == HeroClass::Monk) {
|
|
if (chestItem._iMagical == ITEM_QUALITY_UNIQUE)
|
|
player._pIAC += playerLevel / 2;
|
|
}
|
|
return PlayerArmorGraphic::Heavy;
|
|
case ItemType::MediumArmor:
|
|
if (player._pClass == HeroClass::Monk) {
|
|
if (chestItem._iMagical == ITEM_QUALITY_UNIQUE)
|
|
player._pIAC += playerLevel * 2;
|
|
else
|
|
player._pIAC += playerLevel / 2;
|
|
}
|
|
return PlayerArmorGraphic::Medium;
|
|
default:
|
|
if (player._pClass == HeroClass::Monk)
|
|
player._pIAC += playerLevel * 2;
|
|
return PlayerArmorGraphic::Light;
|
|
}
|
|
}
|
|
|
|
return PlayerArmorGraphic::Light;
|
|
}
|
|
|
|
void CalcPlrGraphics(Player &player, PlayerWeaponGraphic animWeaponId, PlayerArmorGraphic animArmorId, bool loadgfx)
|
|
{
|
|
const uint8_t gfxNum = static_cast<uint8_t>(animWeaponId) | static_cast<uint8_t>(animArmorId);
|
|
if (player._pgfxnum != gfxNum && loadgfx) {
|
|
player._pgfxnum = gfxNum;
|
|
ResetPlayerGFX(player);
|
|
SetPlrAnims(player);
|
|
player.previewCelSprite = std::nullopt;
|
|
player_graphic graphic = player.getGraphic();
|
|
int8_t numberOfFrames;
|
|
int8_t ticksPerFrame;
|
|
player.getAnimationFramesAndTicksPerFrame(graphic, numberOfFrames, ticksPerFrame);
|
|
LoadPlrGFX(player, graphic);
|
|
OptionalClxSpriteList sprites;
|
|
if (!HeadlessMode) {
|
|
auto &animData = player.AnimationData[static_cast<size_t>(graphic)];
|
|
if (animData.sprites.has_value()) {
|
|
sprites = animData.spritesForDirection(player._pdir);
|
|
} else {
|
|
// In multiplayer games, a remote player can unequip their shield while that player is blocking an attack on the host.
|
|
// This results in a nonexistent animation state on the host where the remote player must block with no shield equipped.
|
|
// ie, (graphic == player_graphic::Block && !player._pBlockFlag) requests warrior animation "whnbl"
|
|
// Attempting to load the nonexistent animation crashes the host.
|
|
// This could also happen when unequipping a weapon during an attack, etc.
|
|
// To avoid the crash, we can set the remote player into a standing animation before updating the items on their sprite.
|
|
graphic = player_graphic::Stand;
|
|
NewPlrAnim(player, graphic, player._pdir);
|
|
player.getAnimationFramesAndTicksPerFrame(graphic, numberOfFrames, ticksPerFrame);
|
|
sprites = player.AnimationData[static_cast<size_t>(graphic)].spritesForDirection(player._pdir);
|
|
}
|
|
}
|
|
player.AnimInfo.changeAnimationData(sprites, numberOfFrames, ticksPerFrame);
|
|
} else {
|
|
player._pgfxnum = gfxNum;
|
|
}
|
|
}
|
|
|
|
void CalcPlrAuricBonus(Player &player)
|
|
|
|
{
|
|
if (&player == MyPlayer) {
|
|
if (player.InvBody[INVLOC_AMULET].isEmpty() || player.InvBody[INVLOC_AMULET].IDidx != IDI_AURIC) {
|
|
const int half = MaxGold;
|
|
MaxGold = GOLD_MAX_LIMIT;
|
|
|
|
if (half != MaxGold)
|
|
StripTopGold(player);
|
|
} else {
|
|
MaxGold = GOLD_MAX_LIMIT * 2;
|
|
}
|
|
}
|
|
}
|
|
|
|
void CalcPlrItemVals(Player &player, bool loadgfx)
|
|
{
|
|
int minDamage = 0;
|
|
int maxDamage = 0;
|
|
int ac = 0;
|
|
|
|
int dam = 0;
|
|
int toHit = 0;
|
|
int bonusAc = 0;
|
|
|
|
ItemSpecialEffect flags = ItemSpecialEffect::None;
|
|
ItemSpecialEffectHf damAcFlags = ItemSpecialEffectHf::None;
|
|
|
|
int strength = 0;
|
|
int magic = 0;
|
|
int dexterity = 0;
|
|
int vitality = 0;
|
|
|
|
uint64_t spells = 0;
|
|
|
|
int fireRes = 0;
|
|
int lightRes = 0;
|
|
int magicRes = 0;
|
|
|
|
int damMod = 0;
|
|
int getHit = 0;
|
|
|
|
int lightRadius = 10;
|
|
|
|
int life = 0;
|
|
int mana = 0;
|
|
|
|
int8_t splLvlAdd = 0;
|
|
int targetAc = 0;
|
|
|
|
int minFireDam = 0;
|
|
int maxFireDam = 0;
|
|
int minLightDam = 0;
|
|
int maxLightDam = 0;
|
|
|
|
for (const Item &item : player.InvBody) {
|
|
if (!item.isEmpty() && item._iStatFlag) {
|
|
|
|
minDamage += item._iMinDam;
|
|
maxDamage += item._iMaxDam;
|
|
ac += item._iAC;
|
|
|
|
if (IsValidSpell(item._iSpell) && item._iCharges != 0) {
|
|
spells |= GetSpellBitmask(item._iSpell);
|
|
}
|
|
|
|
if (item._iMagical == ITEM_QUALITY_NORMAL || item._iIdentified) {
|
|
dam += item._iPLDam;
|
|
toHit += item._iPLToHit;
|
|
bonusAc += GetBonusAC(item);
|
|
flags |= item._iFlags;
|
|
damAcFlags |= item._iDamAcFlags;
|
|
strength += item._iPLStr;
|
|
magic += item._iPLMag;
|
|
dexterity += item._iPLDex;
|
|
vitality += item._iPLVit;
|
|
fireRes += item._iPLFR;
|
|
lightRes += item._iPLLR;
|
|
magicRes += item._iPLMR;
|
|
damMod += item._iPLDamMod;
|
|
getHit += item._iPLGetHit;
|
|
lightRadius += item._iPLLight;
|
|
life += item._iPLHP;
|
|
mana += item._iPLMana;
|
|
splLvlAdd += item._iSplLvlAdd;
|
|
targetAc += item._iPLEnAc;
|
|
minFireDam += item._iFMinDam;
|
|
maxFireDam += item._iFMaxDam;
|
|
minLightDam += item._iLMinDam;
|
|
maxLightDam += item._iLMaxDam;
|
|
}
|
|
}
|
|
}
|
|
|
|
CalcPlrDamage(player, minDamage, maxDamage);
|
|
CalcPlrPrimaryStats(player, strength, magic, dexterity, vitality);
|
|
player._pIAC = ac;
|
|
player._pIBonusDam = dam;
|
|
player._pIBonusToHit = toHit;
|
|
player._pIBonusAC = bonusAc;
|
|
player._pIFlags = flags;
|
|
player.pDamAcFlags = damAcFlags;
|
|
player._pIBonusDamMod = damMod;
|
|
player._pIGetHit = getHit;
|
|
CalcPlrLightRadius(player, lightRadius);
|
|
CalcPlrDamageMod(player);
|
|
player._pISpells = spells;
|
|
EnsureValidReadiedSpell(player);
|
|
player._pISplLvlAdd = splLvlAdd;
|
|
player._pIEnAc = targetAc;
|
|
CalcPlrResistances(player, flags, fireRes, lightRes, magicRes);
|
|
CalcPlrLifeMana(player, vitality, magic, life, mana);
|
|
player._pIFMinDam = minFireDam;
|
|
player._pIFMaxDam = maxFireDam;
|
|
player._pILMinDam = minLightDam;
|
|
player._pILMaxDam = maxLightDam;
|
|
|
|
CalcPlrBlockFlag(player);
|
|
|
|
CalcPlrGraphics(player, GetPlrAnimWeaponId(player), GetPlrAnimArmorId(player), loadgfx);
|
|
|
|
CalcPlrAuricBonus(player);
|
|
RedrawComponent(PanelDrawComponent::Mana);
|
|
RedrawComponent(PanelDrawComponent::Health);
|
|
}
|
|
|
|
void CalcPlrInv(Player &player, bool loadgfx)
|
|
{
|
|
// Determine the players current stats, this updates the statFlag on all equipped items that became unusable after
|
|
// a change in equipment.
|
|
CalcSelfItems(player);
|
|
|
|
// Determine the current item bonuses gained from usable equipped items
|
|
if (&player != MyPlayer && !player.isOnActiveLevel()) {
|
|
// Ensure we don't load graphics for players that aren't on our level
|
|
loadgfx = false;
|
|
}
|
|
CalcPlrItemVals(player, loadgfx);
|
|
|
|
if (&player == MyPlayer) {
|
|
// Now that stat gains from equipped items have been calculated, mark unusable scrolls etc
|
|
for (Item &item : InventoryAndBeltPlayerItemsRange { player }) {
|
|
item.updateRequiredStatsCacheForPlayer(player);
|
|
}
|
|
player.CalcScrolls();
|
|
if (IsStashOpen) {
|
|
// If stash is open, ensure the items are displayed correctly
|
|
Stash.RefreshItemStatFlags();
|
|
}
|
|
if (!player.HoldItem.isEmpty())
|
|
player.HoldItem.updateRequiredStatsCacheForPlayer(player);
|
|
}
|
|
}
|
|
|
|
void InitializeItem(Item &item, _item_indexes itemData)
|
|
{
|
|
auto &pAllItem = AllItemsList[static_cast<size_t>(itemData)];
|
|
|
|
// zero-initialize struct
|
|
item = {};
|
|
|
|
item._itype = pAllItem.itype;
|
|
item._iCurs = pAllItem.iCurs;
|
|
CopyUtf8(item._iName, pAllItem.iName, ItemNameLength);
|
|
CopyUtf8(item._iIName, pAllItem.iName, ItemNameLength);
|
|
item._iLoc = pAllItem.iLoc;
|
|
item._iClass = pAllItem.iClass;
|
|
item._iMinDam = pAllItem.iMinDam;
|
|
item._iMaxDam = pAllItem.iMaxDam;
|
|
item._iAC = pAllItem.iMinAC;
|
|
item._iMiscId = pAllItem.iMiscId;
|
|
item._iSpell = pAllItem.iSpell;
|
|
|
|
if (pAllItem.iMiscId == IMISC_STAFF) {
|
|
item._iCharges = gbIsHellfire ? 18 : 40;
|
|
}
|
|
|
|
item._iMaxCharges = item._iCharges;
|
|
item._iDurability = pAllItem.iDurability;
|
|
item._iMaxDur = pAllItem.iDurability;
|
|
item._iMinStr = pAllItem.iMinStr;
|
|
item._iMinMag = pAllItem.iMinMag;
|
|
item._iMinDex = pAllItem.iMinDex;
|
|
item._ivalue = pAllItem.iValue;
|
|
item._iIvalue = pAllItem.iValue;
|
|
item._iPrePower = IPL_INVALID;
|
|
item._iSufPower = IPL_INVALID;
|
|
item._iMagical = ITEM_QUALITY_NORMAL;
|
|
item.IDidx = itemData;
|
|
if (gbIsHellfire)
|
|
item.dwBuff |= CF_HELLFIRE;
|
|
}
|
|
|
|
void GenerateNewSeed(Item &item)
|
|
{
|
|
item._iSeed = AdvanceRndSeed();
|
|
}
|
|
|
|
int GetGoldCursor(int value)
|
|
{
|
|
if (value >= GOLD_MEDIUM_LIMIT)
|
|
return ICURS_GOLD_LARGE;
|
|
|
|
if (value <= GOLD_SMALL_LIMIT)
|
|
return ICURS_GOLD_SMALL;
|
|
|
|
return ICURS_GOLD_MEDIUM;
|
|
}
|
|
|
|
void SetPlrHandGoldCurs(Item &gold)
|
|
{
|
|
gold._iCurs = GetGoldCursor(gold._ivalue);
|
|
}
|
|
|
|
namespace {
|
|
void CreateStartingItem(Player &player, _item_indexes itemData)
|
|
{
|
|
Item item;
|
|
InitializeItem(item, itemData);
|
|
GenerateNewSeed(item);
|
|
item.updateRequiredStatsCacheForPlayer(player);
|
|
AutoEquip(player, item) || AutoPlaceItemInBelt(player, item, true) || AutoPlaceItemInInventory(player, item);
|
|
}
|
|
} // namespace
|
|
|
|
void CreatePlrItems(Player &player)
|
|
{
|
|
for (auto &item : player.InvBody) {
|
|
item.clear();
|
|
}
|
|
|
|
// converting this to a for loop creates a `rep stosd` instruction,
|
|
// so this probably actually was a memset
|
|
memset(&player.InvGrid, 0, sizeof(player.InvGrid));
|
|
|
|
for (auto &item : player.InvList) {
|
|
item.clear();
|
|
}
|
|
|
|
player._pNumInv = 0;
|
|
|
|
for (auto &item : player.SpdList) {
|
|
item.clear();
|
|
}
|
|
|
|
const PlayerStartingLoadoutData &loadout = GetPlayerStartingLoadoutForClass(player._pClass);
|
|
|
|
if (loadout.spell != SpellID::Null && loadout.spellLevel > 0) {
|
|
player._pMemSpells = GetSpellBitmask(loadout.spell);
|
|
player._pRSplType = SpellType::Spell;
|
|
player._pRSpell = loadout.spell;
|
|
player._pSplLvl[static_cast<unsigned>(loadout.spell)] = loadout.spellLevel;
|
|
} else {
|
|
player._pMemSpells = 0;
|
|
}
|
|
|
|
if (loadout.skill != SpellID::Null) {
|
|
player._pAblSpells = GetSpellBitmask(loadout.skill);
|
|
if (player._pRSplType == SpellType::Invalid) {
|
|
player._pRSplType = SpellType::Skill;
|
|
player._pRSpell = loadout.skill;
|
|
}
|
|
}
|
|
|
|
InitCursor();
|
|
for (const _item_indexes itemData : loadout.items) {
|
|
if (itemData != _item_indexes::IDI_NONE)
|
|
CreateStartingItem(player, itemData);
|
|
}
|
|
FreeCursor();
|
|
|
|
if (loadout.gold > 0) {
|
|
Item &goldItem = player.InvList[player._pNumInv];
|
|
MakeGoldStack(goldItem, loadout.gold);
|
|
|
|
player._pNumInv++;
|
|
player.InvGrid[30] = player._pNumInv;
|
|
|
|
player._pGold = goldItem._ivalue;
|
|
}
|
|
|
|
CalcPlrItemVals(player, false);
|
|
}
|
|
|
|
bool ItemSpaceOk(Point position)
|
|
{
|
|
if (!InDungeonBounds(position)) {
|
|
return false;
|
|
}
|
|
|
|
if (IsTileSolid(position)) {
|
|
return false;
|
|
}
|
|
|
|
if (dItem[position.x][position.y] != 0) {
|
|
return false;
|
|
}
|
|
|
|
if (dMonster[position.x][position.y] != 0) {
|
|
return false;
|
|
}
|
|
|
|
if (dPlayer[position.x][position.y] != 0) {
|
|
return false;
|
|
}
|
|
|
|
if (IsItemBlockingObjectAtPosition(position)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
int AllocateItem()
|
|
{
|
|
assert(ActiveItemCount < MAXITEMS);
|
|
|
|
const int inum = ActiveItems[ActiveItemCount];
|
|
ActiveItemCount++;
|
|
|
|
Items[inum] = {};
|
|
|
|
return inum;
|
|
}
|
|
|
|
uint8_t PlaceItemInWorld(Item &&item, WorldTilePosition position)
|
|
{
|
|
assert(ActiveItemCount < MAXITEMS);
|
|
|
|
const uint8_t ii = ActiveItems[ActiveItemCount];
|
|
ActiveItemCount++;
|
|
|
|
dItem[position.x][position.y] = static_cast<int8_t>(ii + 1);
|
|
auto &newItem = Items[ii];
|
|
newItem = std::move(item);
|
|
newItem.position = position;
|
|
RespawnItem(newItem, true);
|
|
|
|
if (CornerStone.isAvailable() && position == CornerStone.position) {
|
|
CornerStone.item = newItem;
|
|
InitQTextMsg(TEXT_CORNSTN);
|
|
Quests[Q_CORNSTN]._qactive = QUEST_DONE;
|
|
}
|
|
|
|
return ii;
|
|
}
|
|
|
|
Point GetSuperItemLoc(Point position)
|
|
{
|
|
const std::optional<Point> itemPosition = FindClosestValidPosition(ItemSpaceOk, position, 1, 50);
|
|
|
|
return itemPosition.value_or(Point { 0, 0 }); // TODO handle no space for dropping items
|
|
}
|
|
|
|
void GetItemAttrs(Item &item, _item_indexes itemData, int lvl)
|
|
{
|
|
auto &baseItemData = AllItemsList[static_cast<size_t>(itemData)];
|
|
item._itype = baseItemData.itype;
|
|
item._iCurs = baseItemData.iCurs;
|
|
CopyUtf8(item._iName, baseItemData.iName, ItemNameLength);
|
|
CopyUtf8(item._iIName, baseItemData.iName, ItemNameLength);
|
|
item._iLoc = baseItemData.iLoc;
|
|
item._iClass = baseItemData.iClass;
|
|
item._iMinDam = baseItemData.iMinDam;
|
|
item._iMaxDam = baseItemData.iMaxDam;
|
|
item._iAC = baseItemData.iMinAC + GenerateRnd(baseItemData.iMaxAC - baseItemData.iMinAC + 1);
|
|
item._iFlags = baseItemData.iFlags;
|
|
item._iMiscId = baseItemData.iMiscId;
|
|
item._iSpell = baseItemData.iSpell;
|
|
item._iMagical = ITEM_QUALITY_NORMAL;
|
|
item._ivalue = baseItemData.iValue;
|
|
item._iIvalue = baseItemData.iValue;
|
|
item._iDurability = baseItemData.iDurability;
|
|
item._iMaxDur = baseItemData.iDurability;
|
|
item._iMinStr = baseItemData.iMinStr;
|
|
item._iMinMag = baseItemData.iMinMag;
|
|
item._iMinDex = baseItemData.iMinDex;
|
|
item.IDidx = itemData;
|
|
if (gbIsHellfire)
|
|
item.dwBuff |= CF_HELLFIRE;
|
|
item._iPrePower = IPL_INVALID;
|
|
item._iSufPower = IPL_INVALID;
|
|
|
|
if (item._iMiscId == IMISC_BOOK)
|
|
GetBookSpell(item, lvl);
|
|
|
|
if (gbIsHellfire && item._iMiscId == IMISC_OILOF)
|
|
GetOilType(item, lvl);
|
|
|
|
if (item._itype != ItemType::Gold)
|
|
return;
|
|
|
|
int rndv;
|
|
const int itemlevel = ItemsGetCurrlevel();
|
|
switch (sgGameInitInfo.nDifficulty) {
|
|
case DIFF_NORMAL:
|
|
rndv = 5 * itemlevel + GenerateRnd(10 * itemlevel);
|
|
break;
|
|
case DIFF_NIGHTMARE:
|
|
rndv = 5 * (itemlevel + 16) + GenerateRnd(10 * (itemlevel + 16));
|
|
break;
|
|
case DIFF_HELL:
|
|
rndv = 5 * (itemlevel + 32) + GenerateRnd(10 * (itemlevel + 32));
|
|
break;
|
|
}
|
|
if (leveltype == DTYPE_HELL)
|
|
rndv += rndv / 8;
|
|
|
|
item._ivalue = std::min(rndv, GOLD_MAX_LIMIT);
|
|
SetPlrHandGoldCurs(item);
|
|
}
|
|
|
|
void SetupItem(Item &item)
|
|
{
|
|
item.setNewAnimation(MyPlayer != nullptr && MyPlayer->pLvlLoad == 0);
|
|
item._iIdentified = false;
|
|
}
|
|
|
|
Item *SpawnUnique(_unique_items uid, Point position, std::optional<int> level /*= std::nullopt*/, bool sendmsg /*= true*/, bool exactPosition /*= false*/)
|
|
{
|
|
if (ActiveItemCount >= MAXITEMS)
|
|
return nullptr;
|
|
|
|
const int ii = AllocateItem();
|
|
auto &item = Items[ii];
|
|
if (exactPosition && CanPut(position)) {
|
|
item.position = position;
|
|
dItem[position.x][position.y] = ii + 1;
|
|
} else {
|
|
GetSuperItemSpace(position, ii);
|
|
}
|
|
int curlv = ItemsGetCurrlevel();
|
|
|
|
std::underlying_type_t<_item_indexes> idx = 0;
|
|
while (AllItemsList[idx].iItemId != UniqueItems[uid].UIItemId)
|
|
idx++;
|
|
|
|
if (sgGameInitInfo.nDifficulty == DIFF_NORMAL) {
|
|
GetItemAttrs(item, static_cast<_item_indexes>(idx), curlv);
|
|
GetUniqueItem(*MyPlayer, item, uid);
|
|
SetupItem(item);
|
|
} else {
|
|
if (level)
|
|
curlv = *level;
|
|
const ItemData &uniqueItemData = AllItemsList[idx];
|
|
const _item_indexes dropIdx = GetItemIndexForDroppableItem(false, [&uniqueItemData](const ItemData &item) {
|
|
return item.itype == uniqueItemData.itype;
|
|
});
|
|
SetupAllItems(*MyPlayer, item, dropIdx, AdvanceRndSeed(), curlv * 2, 15, true, false);
|
|
TryRandomUniqueItem(item, dropIdx, curlv * 2, 15, true, false);
|
|
SetupItem(item);
|
|
}
|
|
|
|
if (sendmsg)
|
|
NetSendCmdPItem(false, CMD_SPAWNITEM, item.position, item);
|
|
|
|
return &item;
|
|
}
|
|
|
|
void GetSuperItemSpace(Point position, int8_t inum)
|
|
{
|
|
Point positionToCheck = position;
|
|
if (GetItemSpace(positionToCheck, inum))
|
|
return;
|
|
for (int k = 2; k < 50; k++) {
|
|
for (int j = -k; j <= k; j++) {
|
|
for (int i = -k; i <= k; i++) {
|
|
const Displacement offset = { i, j };
|
|
positionToCheck = position + offset;
|
|
if (!ItemSpaceOk(positionToCheck))
|
|
continue;
|
|
Items[inum].position = positionToCheck;
|
|
dItem[positionToCheck.x][positionToCheck.y] = inum + 1;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
_item_indexes RndItemForMonsterLevel(int8_t monsterLevel)
|
|
{
|
|
if (GenerateRnd(100) > 40)
|
|
return IDI_NONE;
|
|
|
|
if (GenerateRnd(100) > 25)
|
|
return IDI_GOLD;
|
|
|
|
return GetItemIndexForDroppableItem(true, [&monsterLevel](const ItemData &item) {
|
|
return item.iMinMLvl <= monsterLevel;
|
|
});
|
|
}
|
|
|
|
void SetupAllItems(const Player &player, Item &item, _item_indexes idx, uint32_t iseed, int lvl, int uper, bool onlygood, bool pregen, int uidOffset /*= 0*/, bool forceNotUnique /*= false*/)
|
|
{
|
|
item._iSeed = iseed;
|
|
SetRndSeed(iseed);
|
|
GetItemAttrs(item, idx, lvl / 2);
|
|
item._iCreateInfo = lvl;
|
|
|
|
if (pregen)
|
|
item._iCreateInfo |= CF_PREGEN;
|
|
if (onlygood)
|
|
item._iCreateInfo |= CF_ONLYGOOD;
|
|
|
|
if (uper == 15)
|
|
item._iCreateInfo |= CF_UPER15;
|
|
else if (uper == 1)
|
|
item._iCreateInfo |= CF_UPER1;
|
|
|
|
if (item._iMiscId != IMISC_UNIQUE) {
|
|
const int iblvl = GetItemBLevel(lvl, item._iMiscId, onlygood, uper == 15);
|
|
if (iblvl != -1) {
|
|
_unique_items uid = UITEM_INVALID;
|
|
if (!forceNotUnique) {
|
|
uid = CheckUnique(item, iblvl, uper, uidOffset);
|
|
} else {
|
|
DiscardRandomValues(1);
|
|
}
|
|
if (uid == UITEM_INVALID) {
|
|
GetItemBonus(player, item, iblvl / 2, iblvl, onlygood, true);
|
|
} else {
|
|
GetUniqueItem(player, item, uid);
|
|
}
|
|
}
|
|
if (item._iMagical != ITEM_QUALITY_UNIQUE)
|
|
ItemRndDur(item);
|
|
} else {
|
|
if (item._iLoc != ILOC_UNEQUIPABLE) {
|
|
if (iseed > 109 || AllItemsList[static_cast<size_t>(idx)].iItemId != UniqueItems[iseed].UIItemId) {
|
|
item.clear();
|
|
return;
|
|
}
|
|
|
|
GetUniqueItem(player, item, (_unique_items)iseed); // uid is stored in iseed for uniques
|
|
}
|
|
}
|
|
}
|
|
|
|
void TryRandomUniqueItem(Item &item, _item_indexes idx, int8_t mLevel, int uper, bool onlygood, bool pregen)
|
|
{
|
|
// If the item is a non-quest unique, find a random valid uid and force generate items to get an item with that uid.
|
|
if ((item._iCreateInfo & CF_UNIQUE) == 0 || item._iMiscId == IMISC_UNIQUE)
|
|
return;
|
|
|
|
SetRndSeed(item._iSeed);
|
|
|
|
// Get item base level, which is used in CheckUnique to get the correct valid uniques for the base item.
|
|
DiscardRandomValues(1); // GetItemAttrs
|
|
const int blvl = GetItemBLevel(mLevel, item._iMiscId, onlygood, uper == 15);
|
|
|
|
// Gather all potential unique items. uid is the index into UniqueItems.
|
|
auto validUniques = GetValidUniques(blvl, AllItemsList[static_cast<size_t>(idx)].iItemId);
|
|
assert(!validUniques.empty());
|
|
std::vector<int> uids;
|
|
for (auto &possibleUid : validUniques) {
|
|
// Verify item hasn't been dropped yet. We set this to true in MP, since uniques previously dropping shouldn't prevent further identical uniques from dropping.
|
|
if (!UniqueItemFlags[possibleUid] || gbIsMultiplayer) {
|
|
uids.emplace_back(possibleUid);
|
|
}
|
|
}
|
|
|
|
// If we find at least one unique in uids that hasn't been obtained yet, we can proceed getting a random unique.
|
|
if (uids.empty()) {
|
|
// Set uper to 1 and make the level adjustment so we have better odds of not generating a unique item.
|
|
if (uper == 15)
|
|
mLevel += 4;
|
|
uper = 1;
|
|
|
|
const Point itemPos = item.position;
|
|
|
|
// Force generate a non-unique item.
|
|
DiabloGenerator itemGenerator(item._iSeed);
|
|
do {
|
|
item = {}; // Reset item data
|
|
item.position = itemPos;
|
|
SetupAllItems(*MyPlayer, item, idx, itemGenerator.advanceRndSeed(), mLevel, uper, onlygood, pregen);
|
|
} while (item._iMagical == ITEM_QUALITY_UNIQUE);
|
|
|
|
return;
|
|
}
|
|
|
|
const int32_t uidsIdx = std::max<int32_t>(0, GenerateRnd(static_cast<int32_t>(uids.size()))); // Index into uids, used to get a random uid from the uids vector.
|
|
const int uid = uids[uidsIdx]; // Actual unique id.
|
|
const UniqueItem &uniqueItem = UniqueItems[uid];
|
|
|
|
// If the selected unique was already generated, there is no need to fiddle with its parameters.
|
|
if (item._iUid == uid) {
|
|
if (!gbIsMultiplayer) {
|
|
UniqueItemFlags[uid] = true;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Find our own id to calculate the offset in validUniques and check if we can generate a reverse-compatible version of the item.
|
|
int uidOffset = -1;
|
|
bool canGenerateReverseCompatible = true;
|
|
for (size_t i = 0; i < validUniques.size(); i++) {
|
|
if (validUniques[i] == uid) {
|
|
// Vanilla always picks the last unique, so the offset is calculated from the back of the valid unique list.
|
|
uidOffset = static_cast<int>(validUniques.size() - i - 1);
|
|
} else if (uidOffset != -1 && UniqueItems[validUniques[i]].UIMinLvl <= uniqueItem.UIMinLvl) {
|
|
// Found an item with same or lower level as our desired unique after our unique.
|
|
// This means that we cannot possibly generate the item in reverse compatible mode and must rely on an offset.
|
|
canGenerateReverseCompatible = false;
|
|
}
|
|
}
|
|
assert(uidOffset != -1);
|
|
|
|
const Point itemPos = item.position;
|
|
if (canGenerateReverseCompatible) {
|
|
int targetLvl = 1; // Target level for reverse compatibility, since vanilla always takes the last applicable uid in the list.
|
|
|
|
// Set target level. Ideally we use uper 15 to have a 16% chance of generating a unique item.
|
|
if (uniqueItem.UIMinLvl - 4 > 0) { // Negative level will underflow. Lvl 0 items may have unintended consequences.
|
|
uper = 15;
|
|
targetLvl = uniqueItem.UIMinLvl - 4;
|
|
} else {
|
|
uper = 1;
|
|
targetLvl = uniqueItem.UIMinLvl;
|
|
}
|
|
|
|
// Force generate items until we find a uid match.
|
|
DiabloGenerator itemGenerator(item._iSeed);
|
|
do {
|
|
item = {}; // Reset item data
|
|
item.position = itemPos;
|
|
// Set onlygood = true, to always get the required item base level for the unique.
|
|
SetupAllItems(*MyPlayer, item, idx, itemGenerator.advanceRndSeed(), targetLvl, uper, true, pregen);
|
|
} while (item._iUid != uid);
|
|
} else {
|
|
// Recreate the item with new offset, this creates the desired unique item but is not reverse compatible.
|
|
const int seed = item._iSeed;
|
|
item = {}; // Reset item data
|
|
item.position = itemPos;
|
|
SetupAllItems(*MyPlayer, item, idx, seed, mLevel, uper, onlygood, pregen, uidOffset);
|
|
item.dwBuff |= (uidOffset << 1) & CF_UIDOFFSET;
|
|
assert(item._iUid == uid);
|
|
}
|
|
|
|
// Set item as obtained to prevent it from being dropped again in SP.
|
|
if (!gbIsMultiplayer) {
|
|
UniqueItemFlags[uid] = true;
|
|
}
|
|
}
|
|
|
|
void SpawnItem(Monster &monster, Point position, bool sendmsg, bool spawn /*= false*/)
|
|
{
|
|
_item_indexes idx;
|
|
bool onlygood = true;
|
|
|
|
const bool dropsSpecialTreasure = (monster.data().treasure & T_UNIQ) != 0;
|
|
const bool dropBrain = Quests[Q_MUSHROOM]._qactive == QUEST_ACTIVE && Quests[Q_MUSHROOM]._qvar1 == QS_MUSHGIVEN;
|
|
|
|
if (dropsSpecialTreasure && !UseMultiplayerQuests()) {
|
|
Item *uniqueItem = SpawnUnique(static_cast<_unique_items>(monster.data().treasure & T_MASK), position, std::nullopt, false);
|
|
if (uniqueItem != nullptr && sendmsg)
|
|
NetSendCmdPItem(false, CMD_DROPITEM, uniqueItem->position, *uniqueItem);
|
|
return;
|
|
}
|
|
if (monster.isUnique() || dropsSpecialTreasure) {
|
|
// Unique monster is killed => use better item base (for example no gold)
|
|
idx = RndUItem(&monster);
|
|
} else if (dropBrain && !gbIsMultiplayer) {
|
|
// Normal monster is killed => need to drop brain to progress the quest
|
|
Quests[Q_MUSHROOM]._qvar1 = QS_BRAINSPAWNED;
|
|
NetSendCmdQuest(true, Quests[Q_MUSHROOM]);
|
|
// brain replaces normal drop
|
|
idx = IDI_BRAIN;
|
|
} else {
|
|
if (dropBrain && gbIsMultiplayer && sendmsg) {
|
|
Quests[Q_MUSHROOM]._qvar1 = QS_BRAINSPAWNED;
|
|
NetSendCmdQuest(true, Quests[Q_MUSHROOM]);
|
|
// Drop the brain as extra item to ensure that all clients see the brain drop
|
|
// When executing SpawnItem is not reliable, because another client can already have the quest state updated before SpawnItem is executed
|
|
const Point posBrain = GetSuperItemLoc(position);
|
|
SpawnQuestItem(IDI_BRAIN, posBrain, 0, SelectionRegion::None, true);
|
|
}
|
|
// Normal monster
|
|
if ((monster.data().treasure & T_NODROP) != 0)
|
|
return;
|
|
onlygood = false;
|
|
idx = RndItemForMonsterLevel(static_cast<int8_t>(monster.level(sgGameInitInfo.nDifficulty)));
|
|
}
|
|
|
|
if (idx == IDI_NONE)
|
|
return;
|
|
|
|
if (ActiveItemCount >= MAXITEMS)
|
|
return;
|
|
|
|
const int ii = AllocateItem();
|
|
auto &item = Items[ii];
|
|
GetSuperItemSpace(position, ii);
|
|
const int uper = monster.isUnique() ? 15 : 1;
|
|
|
|
const int8_t mLevel = monster.data().level;
|
|
SetupAllItems(*MyPlayer, item, idx, AdvanceRndSeed(), mLevel, uper, onlygood, false);
|
|
TryRandomUniqueItem(item, idx, mLevel, uper, onlygood, false);
|
|
SetupItem(item);
|
|
|
|
if (sendmsg)
|
|
NetSendCmdPItem(false, CMD_DROPITEM, item.position, item);
|
|
if (spawn)
|
|
NetSendCmdPItem(false, CMD_SPAWNITEM, item.position, item);
|
|
}
|
|
|
|
void CreateRndItem(Point position, bool onlygood, bool sendmsg, bool delta)
|
|
{
|
|
const _item_indexes idx = onlygood ? RndUItem(nullptr) : RndAllItems();
|
|
|
|
SetupBaseItem(position, idx, onlygood, sendmsg, delta);
|
|
}
|
|
|
|
void CreateRndUseful(Point position, bool sendmsg)
|
|
{
|
|
if (ActiveItemCount >= MAXITEMS)
|
|
return;
|
|
|
|
const int ii = AllocateItem();
|
|
auto &item = Items[ii];
|
|
GetSuperItemSpace(position, ii);
|
|
const int curlv = ItemsGetCurrlevel();
|
|
|
|
SetupAllUseful(item, AdvanceRndSeed(), curlv);
|
|
if (sendmsg)
|
|
NetSendCmdPItem(false, CMD_DROPITEM, item.position, item);
|
|
}
|
|
|
|
void CreateTypeItem(Point position, bool onlygood, ItemType itemType, int imisc, bool sendmsg, bool delta, bool spawn)
|
|
{
|
|
_item_indexes idx;
|
|
|
|
const int curlv = ItemsGetCurrlevel();
|
|
if (itemType != ItemType::Gold)
|
|
idx = RndTypeItems(itemType, imisc, curlv);
|
|
else
|
|
idx = IDI_GOLD;
|
|
|
|
SetupBaseItem(position, idx, onlygood, sendmsg, delta, spawn);
|
|
}
|
|
|
|
void RecreateItem(const Player &player, Item &item, _item_indexes idx, uint16_t icreateinfo, uint32_t iseed, int ivalue, uint32_t dwBuff)
|
|
{
|
|
const bool tmpIsHellfire = gbIsHellfire;
|
|
item.dwBuff = dwBuff;
|
|
gbIsHellfire = (item.dwBuff & CF_HELLFIRE) != 0;
|
|
|
|
if (idx == IDI_GOLD) {
|
|
InitializeItem(item, IDI_GOLD);
|
|
item._iSeed = iseed;
|
|
item._iCreateInfo = icreateinfo;
|
|
item._ivalue = ivalue;
|
|
SetPlrHandGoldCurs(item);
|
|
gbIsHellfire = tmpIsHellfire;
|
|
return;
|
|
}
|
|
|
|
if (icreateinfo == 0) {
|
|
InitializeItem(item, idx);
|
|
item._iSeed = iseed;
|
|
gbIsHellfire = tmpIsHellfire;
|
|
return;
|
|
}
|
|
|
|
if ((icreateinfo & CF_UNIQUE) == 0) {
|
|
if ((icreateinfo & CF_TOWN) != 0) {
|
|
RecreateTownItem(player, item, idx, icreateinfo, iseed);
|
|
gbIsHellfire = tmpIsHellfire;
|
|
return;
|
|
}
|
|
|
|
if ((icreateinfo & CF_USEFUL) == CF_USEFUL) {
|
|
SetupAllUseful(item, iseed, icreateinfo & CF_LEVEL);
|
|
gbIsHellfire = tmpIsHellfire;
|
|
return;
|
|
}
|
|
}
|
|
|
|
const int level = icreateinfo & CF_LEVEL;
|
|
|
|
int uper = 0;
|
|
if ((icreateinfo & CF_UPER1) != 0)
|
|
uper = 1;
|
|
if ((icreateinfo & CF_UPER15) != 0)
|
|
uper = 15;
|
|
|
|
const bool onlygood = (icreateinfo & CF_ONLYGOOD) != 0;
|
|
const bool forceNotUnique = (icreateinfo & CF_UNIQUE) == 0;
|
|
const bool pregen = (icreateinfo & CF_PREGEN) != 0;
|
|
auto uidOffset = static_cast<int>((item.dwBuff & CF_UIDOFFSET) >> 1);
|
|
|
|
SetupAllItems(player, item, idx, iseed, level, uper, onlygood, pregen, uidOffset, forceNotUnique);
|
|
SetupItem(item);
|
|
gbIsHellfire = tmpIsHellfire;
|
|
}
|
|
|
|
void RecreateEar(Item &item, uint16_t ic, uint32_t iseed, uint8_t bCursval, std::string_view heroName)
|
|
{
|
|
InitializeItem(item, IDI_EAR);
|
|
|
|
const std::string itemName = fmt::format(fmt::runtime("Ear of {:s}"), heroName);
|
|
|
|
CopyUtf8(item._iName, itemName, ItemNameLength);
|
|
CopyUtf8(item._iIName, heroName, ItemNameLength);
|
|
|
|
item._iCurs = ((bCursval >> 6) & 3) + ICURS_EAR_SORCERER;
|
|
item._ivalue = bCursval & 0x3F;
|
|
item._iCreateInfo = ic;
|
|
item._iSeed = iseed;
|
|
}
|
|
|
|
void CornerstoneSave()
|
|
{
|
|
if (!CornerStone.activated)
|
|
return;
|
|
if (!CornerStone.item.isEmpty()) {
|
|
ItemPack id;
|
|
PackItem(id, CornerStone.item, (CornerStone.item.dwBuff & CF_HELLFIRE) != 0);
|
|
const auto *buffer = reinterpret_cast<uint8_t *>(&id);
|
|
for (size_t i = 0; i < sizeof(ItemPack); i++) {
|
|
BufCopy(&GetOptions().Hellfire.szItem[i * 2], AsHexPad2(buffer[i], /*uppercase=*/true));
|
|
}
|
|
GetOptions().Hellfire.szItem[sizeof(GetOptions().Hellfire.szItem) - 1] = '\0';
|
|
} else {
|
|
GetOptions().Hellfire.szItem[0] = '\0';
|
|
}
|
|
}
|
|
|
|
void CornerstoneLoad(Point position)
|
|
{
|
|
ItemPack pkSItem;
|
|
|
|
if (CornerStone.activated || position.x == 0 || position.y == 0) {
|
|
return;
|
|
}
|
|
|
|
CornerStone.item.clear();
|
|
CornerStone.activated = true;
|
|
if (dItem[position.x][position.y] != 0) {
|
|
const int ii = dItem[position.x][position.y] - 1;
|
|
for (int i = 0; i < ActiveItemCount; i++) {
|
|
if (ActiveItems[i] == ii) {
|
|
DeleteItem(i);
|
|
break;
|
|
}
|
|
}
|
|
dItem[position.x][position.y] = 0;
|
|
}
|
|
|
|
if (strlen(GetOptions().Hellfire.szItem) < sizeof(ItemPack) * 2)
|
|
return;
|
|
|
|
Hex2bin(GetOptions().Hellfire.szItem, sizeof(ItemPack), reinterpret_cast<uint8_t *>(&pkSItem));
|
|
|
|
const int ii = AllocateItem();
|
|
auto &item = Items[ii];
|
|
|
|
dItem[position.x][position.y] = static_cast<int8_t>(ii + 1);
|
|
|
|
UnPackItem(pkSItem, *MyPlayer, item, (pkSItem.dwBuff & CF_HELLFIRE) != 0);
|
|
item.position = position;
|
|
RespawnItem(item, false);
|
|
CornerStone.item = item;
|
|
}
|
|
|
|
void SpawnQuestItem(_item_indexes itemid, Point position, int randarea, SelectionRegion selectionRegion, bool sendmsg)
|
|
{
|
|
if (randarea > 0) {
|
|
int tries = 0;
|
|
while (true) {
|
|
tries++;
|
|
if (tries > 1000 && randarea > 1)
|
|
randarea--;
|
|
|
|
position.x = GenerateRnd(MAXDUNX);
|
|
position.y = GenerateRnd(MAXDUNY);
|
|
|
|
bool failed = false;
|
|
for (int i = 0; i < randarea && !failed; i++) {
|
|
for (int j = 0; j < randarea && !failed; j++) {
|
|
failed = !ItemSpaceOk(position + Displacement { i, j });
|
|
}
|
|
}
|
|
if (!failed)
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (ActiveItemCount >= MAXITEMS)
|
|
return;
|
|
|
|
const int ii = AllocateItem();
|
|
auto &item = Items[ii];
|
|
|
|
item.position = position;
|
|
|
|
dItem[position.x][position.y] = ii + 1;
|
|
|
|
const int curlv = ItemsGetCurrlevel();
|
|
GetItemAttrs(item, itemid, curlv);
|
|
|
|
SetupItem(item);
|
|
item._iSeed = AdvanceRndSeed();
|
|
SetRndSeed(item._iSeed);
|
|
item._iPostDraw = true;
|
|
if (selectionRegion != SelectionRegion::None) {
|
|
item.selectionRegion = selectionRegion;
|
|
item.AnimInfo.currentFrame = item.AnimInfo.numberOfFrames - 1;
|
|
item._iAnimFlag = false;
|
|
}
|
|
|
|
if (sendmsg)
|
|
NetSendCmdPItem(true, CMD_SPAWNITEM, item.position, item);
|
|
else {
|
|
item._iCreateInfo |= CF_PREGEN;
|
|
DeltaAddItem(ii);
|
|
}
|
|
}
|
|
|
|
void SpawnRewardItem(_item_indexes itemid, Point position, bool sendmsg)
|
|
{
|
|
if (ActiveItemCount >= MAXITEMS)
|
|
return;
|
|
|
|
const int ii = AllocateItem();
|
|
auto &item = Items[ii];
|
|
|
|
item.position = position;
|
|
dItem[position.x][position.y] = ii + 1;
|
|
const int curlv = ItemsGetCurrlevel();
|
|
GetItemAttrs(item, itemid, curlv);
|
|
item.setNewAnimation(true);
|
|
item.selectionRegion = SelectionRegion::Middle;
|
|
item._iPostDraw = true;
|
|
item._iIdentified = true;
|
|
GenerateNewSeed(item);
|
|
|
|
if (sendmsg) {
|
|
NetSendCmdPItem(true, CMD_SPAWNITEM, item.position, item);
|
|
}
|
|
}
|
|
|
|
void SpawnMapOfDoom(Point position, bool sendmsg)
|
|
{
|
|
SpawnRewardItem(IDI_MAPOFDOOM, position, sendmsg);
|
|
}
|
|
|
|
void SpawnRuneBomb(Point position, bool sendmsg)
|
|
{
|
|
SpawnRewardItem(IDI_RUNEBOMB, position, sendmsg);
|
|
}
|
|
|
|
void SpawnTheodore(Point position, bool sendmsg)
|
|
{
|
|
SpawnRewardItem(IDI_THEODORE, position, sendmsg);
|
|
}
|
|
|
|
void RespawnItem(Item &item, bool flipFlag)
|
|
{
|
|
const int it = ItemCAnimTbl[item._iCurs];
|
|
item.setNewAnimation(flipFlag);
|
|
item._iRequest = false; // Item isn't being picked up by a player
|
|
|
|
switch (item._iCurs) {
|
|
case ICURS_TAVERN_SIGN:
|
|
case ICURS_ANVIL_OF_FURY:
|
|
item.selectionRegion = SelectionRegion::Bottom;
|
|
break;
|
|
case ICURS_MAP_OF_THE_STARS:
|
|
case ICURS_RUNE_BOMB:
|
|
case ICURS_THEODORE:
|
|
case ICURS_AURIC_AMULET:
|
|
item.selectionRegion = SelectionRegion::Middle; // Item is selectable at elevated level
|
|
break;
|
|
case ICURS_MAGIC_ROCK:
|
|
Object *stand = FindObjectAtPosition(item.position);
|
|
if (stand != nullptr && stand->_otype == OBJ_STAND) {
|
|
item.selectionRegion = SelectionRegion::Middle; // Item is selectable at elevated level and renders at elevated level
|
|
item._iPostDraw = true; // Draw in front of stand
|
|
item.AnimInfo.currentFrame = 10; // Frame 10 is the start of the elevated frames in the cel
|
|
} else {
|
|
item.selectionRegion = SelectionRegion::Bottom; // Item is selectable at floor level and renders at floor level
|
|
}
|
|
PlaySfxLoc(ItemDropSnds[it], item.position); // Play the drop sound (this item is perpetually in a dropping state, but can always be picked up)
|
|
break;
|
|
}
|
|
}
|
|
|
|
void DeleteItem(int i)
|
|
{
|
|
if (ActiveItemCount > 0)
|
|
ActiveItemCount--;
|
|
|
|
assert(i >= 0 && i < MAXITEMS && ActiveItemCount < MAXITEMS);
|
|
|
|
if (pcursitem == ActiveItems[i]) // Unselect item if player has it highlighted
|
|
pcursitem = -1;
|
|
|
|
if (i < ActiveItemCount) {
|
|
// If the deleted item was not already at the end of the active list, swap the indexes around to make the next item allocation simpler.
|
|
std::swap(ActiveItems[i], ActiveItems[ActiveItemCount]);
|
|
}
|
|
}
|
|
|
|
void ProcessItems()
|
|
{
|
|
for (int i = 0; i < ActiveItemCount; i++) {
|
|
const int ii = ActiveItems[i];
|
|
auto &item = Items[ii];
|
|
if (!item._iAnimFlag)
|
|
continue;
|
|
item.AnimInfo.processAnimation();
|
|
if (item._iCurs == ICURS_MAGIC_ROCK) {
|
|
if (item.selectionRegion == SelectionRegion::Bottom && item.AnimInfo.currentFrame == 10) // Reached end of floor frames + 1, cycle back
|
|
item.AnimInfo.currentFrame = 0; // Beginning of floor frames
|
|
if (item.selectionRegion == SelectionRegion::Middle && item.AnimInfo.currentFrame == 19) // Reached end of elevated frames, cycle back
|
|
item.AnimInfo.currentFrame = 10; // Beginning of elevated frames
|
|
} else {
|
|
if (item.AnimInfo.currentFrame == (item.AnimInfo.numberOfFrames - 1) / 2)
|
|
PlaySfxLoc(ItemDropSnds[ItemCAnimTbl[item._iCurs]], item.position);
|
|
|
|
if (item.AnimInfo.isLastFrame()) {
|
|
item.AnimInfo.currentFrame = item.AnimInfo.numberOfFrames - 1;
|
|
item._iAnimFlag = false;
|
|
item.selectionRegion = SelectionRegion::Bottom;
|
|
}
|
|
}
|
|
}
|
|
ItemDoppel();
|
|
}
|
|
|
|
void FreeItemGFX()
|
|
{
|
|
for (auto &itemanim : itemanims) {
|
|
itemanim = std::nullopt;
|
|
}
|
|
}
|
|
|
|
void GetItemFrm(Item &item)
|
|
{
|
|
const int it = ItemCAnimTbl[item._iCurs];
|
|
if (itemanims[it])
|
|
item.AnimInfo.sprites.emplace(*itemanims[it]);
|
|
}
|
|
|
|
void GetItemStr(Item &item)
|
|
{
|
|
if (item._itype != ItemType::Gold) {
|
|
InfoString = item.getName();
|
|
InfoColor = item.getTextColor();
|
|
} else {
|
|
const int nGold = item._ivalue;
|
|
InfoString = fmt::format(fmt::runtime(ngettext("{:s} gold piece", "{:s} gold pieces", nGold)), FormatInteger(nGold));
|
|
}
|
|
}
|
|
|
|
void CheckIdentify(Player &player, int cii)
|
|
{
|
|
Item *pi;
|
|
|
|
if (cii >= NUM_INVLOC)
|
|
pi = &player.InvList[cii - NUM_INVLOC];
|
|
else
|
|
pi = &player.InvBody[cii];
|
|
|
|
pi->_iIdentified = true;
|
|
CalcPlrInv(player, true);
|
|
}
|
|
|
|
void DoRepair(Player &player, int cii)
|
|
{
|
|
Item *pi;
|
|
|
|
PlaySfxLoc(SfxID::SpellRepair, player.position.tile);
|
|
|
|
if (cii >= NUM_INVLOC) {
|
|
pi = &player.InvList[cii - NUM_INVLOC];
|
|
} else {
|
|
pi = &player.InvBody[cii];
|
|
}
|
|
|
|
RepairItem(*pi, player.getCharacterLevel());
|
|
CalcPlrInv(player, true);
|
|
}
|
|
|
|
void DoRecharge(Player &player, int cii)
|
|
{
|
|
Item *pi;
|
|
|
|
if (cii >= NUM_INVLOC) {
|
|
pi = &player.InvList[cii - NUM_INVLOC];
|
|
} else {
|
|
pi = &player.InvBody[cii];
|
|
}
|
|
|
|
RechargeItem(*pi, player);
|
|
CalcPlrInv(player, true);
|
|
}
|
|
|
|
bool DoOil(Player &player, int cii)
|
|
{
|
|
Item *pi;
|
|
if (cii >= NUM_INVLOC) {
|
|
pi = &player.InvList[cii - NUM_INVLOC];
|
|
} else {
|
|
pi = &player.InvBody[cii];
|
|
}
|
|
if (!ApplyOilToItem(*pi, player))
|
|
return false;
|
|
CalcPlrInv(player, true);
|
|
return true;
|
|
}
|
|
|
|
[[nodiscard]] StringOrView PrintItemPower(char plidx, const Item &item)
|
|
{
|
|
switch (plidx) {
|
|
case IPL_TOHIT:
|
|
case IPL_TOHIT_CURSE:
|
|
return fmt::format(fmt::runtime(_("chance to hit: {:+d}%")), item._iPLToHit);
|
|
case IPL_DAMP:
|
|
case IPL_DAMP_CURSE:
|
|
return fmt::format(fmt::runtime(_(/*xgettext:no-c-format*/ "{:+d}% damage")), item._iPLDam);
|
|
case IPL_TOHIT_DAMP:
|
|
case IPL_TOHIT_DAMP_CURSE:
|
|
return fmt::format(fmt::runtime(_("to hit: {:+d}%, {:+d}% damage")), item._iPLToHit, item._iPLDam);
|
|
case IPL_ACP:
|
|
case IPL_ACP_CURSE:
|
|
return fmt::format(fmt::runtime(_(/*xgettext:no-c-format*/ "{:+d}% armor")), item._iPLAC);
|
|
case IPL_SETAC:
|
|
case IPL_AC_CURSE:
|
|
return fmt::format(fmt::runtime(_("armor class: {:d}")), item._iAC);
|
|
case IPL_FIRERES:
|
|
case IPL_FIRERES_CURSE:
|
|
if (item._iPLFR < MaxResistance)
|
|
return fmt::format(fmt::runtime(_("Resist Fire: {:+d}%")), item._iPLFR);
|
|
else
|
|
return fmt::format(fmt::runtime(_("Resist Fire: {:+d}% MAX")), MaxResistance);
|
|
case IPL_LIGHTRES:
|
|
case IPL_LIGHTRES_CURSE:
|
|
if (item._iPLLR < MaxResistance)
|
|
return fmt::format(fmt::runtime(_("Resist Lightning: {:+d}%")), item._iPLLR);
|
|
else
|
|
return fmt::format(fmt::runtime(_("Resist Lightning: {:+d}% MAX")), MaxResistance);
|
|
case IPL_MAGICRES:
|
|
case IPL_MAGICRES_CURSE:
|
|
if (item._iPLMR < MaxResistance)
|
|
return fmt::format(fmt::runtime(_("Resist Magic: {:+d}%")), item._iPLMR);
|
|
else
|
|
return fmt::format(fmt::runtime(_("Resist Magic: {:+d}% MAX")), MaxResistance);
|
|
case IPL_ALLRES:
|
|
if (item._iPLFR < MaxResistance)
|
|
return fmt::format(fmt::runtime(_("Resist All: {:+d}%")), item._iPLFR);
|
|
else
|
|
return fmt::format(fmt::runtime(_("Resist All: {:+d}% MAX")), MaxResistance);
|
|
case IPL_SPLLVLADD:
|
|
if (item._iSplLvlAdd > 0)
|
|
return fmt::format(fmt::runtime(ngettext("spells are increased {:d} level", "spells are increased {:d} levels", item._iSplLvlAdd)), item._iSplLvlAdd);
|
|
else if (item._iSplLvlAdd < 0)
|
|
return fmt::format(fmt::runtime(ngettext("spells are decreased {:d} level", "spells are decreased {:d} levels", -item._iSplLvlAdd)), -item._iSplLvlAdd);
|
|
else
|
|
return _("spell levels unchanged (?)");
|
|
case IPL_CHARGES:
|
|
return _("Extra charges");
|
|
case IPL_SPELL:
|
|
return fmt::format(fmt::runtime(ngettext("{:d} {:s} charge", "{:d} {:s} charges", item._iMaxCharges)), item._iMaxCharges, pgettext("spell", GetSpellData(item._iSpell).sNameText));
|
|
case IPL_FIREDAM:
|
|
if (item._iFMinDam == item._iFMaxDam)
|
|
return fmt::format(fmt::runtime(_("Fire hit damage: {:d}")), item._iFMinDam);
|
|
else
|
|
return fmt::format(fmt::runtime(_("Fire hit damage: {:d}-{:d}")), item._iFMinDam, item._iFMaxDam);
|
|
case IPL_LIGHTDAM:
|
|
if (item._iLMinDam == item._iLMaxDam)
|
|
return fmt::format(fmt::runtime(_("Lightning hit damage: {:d}")), item._iLMinDam);
|
|
else
|
|
return fmt::format(fmt::runtime(_("Lightning hit damage: {:d}-{:d}")), item._iLMinDam, item._iLMaxDam);
|
|
case IPL_STR:
|
|
case IPL_STR_CURSE:
|
|
return fmt::format(fmt::runtime(_("{:+d} to strength")), item._iPLStr);
|
|
case IPL_MAG:
|
|
case IPL_MAG_CURSE:
|
|
return fmt::format(fmt::runtime(_("{:+d} to magic")), item._iPLMag);
|
|
case IPL_DEX:
|
|
case IPL_DEX_CURSE:
|
|
return fmt::format(fmt::runtime(_("{:+d} to dexterity")), item._iPLDex);
|
|
case IPL_VIT:
|
|
case IPL_VIT_CURSE:
|
|
return fmt::format(fmt::runtime(_("{:+d} to vitality")), item._iPLVit);
|
|
case IPL_ATTRIBS:
|
|
case IPL_ATTRIBS_CURSE:
|
|
return fmt::format(fmt::runtime(_("{:+d} to all attributes")), item._iPLStr);
|
|
case IPL_GETHIT_CURSE:
|
|
case IPL_GETHIT:
|
|
return fmt::format(fmt::runtime(_("{:+d} damage from enemies")), item._iPLGetHit);
|
|
case IPL_LIFE:
|
|
case IPL_LIFE_CURSE:
|
|
return fmt::format(fmt::runtime(_("Hit Points: {:+d}")), item._iPLHP >> 6);
|
|
case IPL_MANA:
|
|
case IPL_MANA_CURSE:
|
|
return fmt::format(fmt::runtime(_("Mana: {:+d}")), item._iPLMana >> 6);
|
|
case IPL_DUR:
|
|
return _("high durability");
|
|
case IPL_DUR_CURSE:
|
|
return _("decreased durability");
|
|
case IPL_INDESTRUCTIBLE:
|
|
return _("indestructible");
|
|
case IPL_LIGHT:
|
|
return fmt::format(fmt::runtime(_(/*xgettext:no-c-format*/ "+{:d}% light radius")), 10 * item._iPLLight);
|
|
case IPL_LIGHT_CURSE:
|
|
return fmt::format(fmt::runtime(_(/*xgettext:no-c-format*/ "-{:d}% light radius")), -10 * item._iPLLight);
|
|
case IPL_MULT_ARROWS:
|
|
return _("multiple arrows per shot");
|
|
case IPL_FIRE_ARROWS:
|
|
if (item._iFMinDam == item._iFMaxDam)
|
|
return fmt::format(fmt::runtime(_("fire arrows damage: {:d}")), item._iFMinDam);
|
|
else
|
|
return fmt::format(fmt::runtime(_("fire arrows damage: {:d}-{:d}")), item._iFMinDam, item._iFMaxDam);
|
|
case IPL_LIGHT_ARROWS:
|
|
if (item._iLMinDam == item._iLMaxDam)
|
|
return fmt::format(fmt::runtime(_("lightning arrows damage {:d}")), item._iLMinDam);
|
|
else
|
|
return fmt::format(fmt::runtime(_("lightning arrows damage {:d}-{:d}")), item._iLMinDam, item._iLMaxDam);
|
|
case IPL_FIREBALL:
|
|
if (item._iFMinDam == item._iFMaxDam)
|
|
return fmt::format(fmt::runtime(_("fireball damage: {:d}")), item._iFMinDam);
|
|
else
|
|
return fmt::format(fmt::runtime(_("fireball damage: {:d}-{:d}")), item._iFMinDam, item._iFMaxDam);
|
|
case IPL_THORNS:
|
|
return _("attacker takes 1-3 damage");
|
|
case IPL_NOMANA:
|
|
return _("user loses all mana");
|
|
case IPL_ABSHALFTRAP:
|
|
return _("absorbs half of trap damage");
|
|
case IPL_KNOCKBACK:
|
|
return _("knocks target back");
|
|
case IPL_3XDAMVDEM:
|
|
return _(/*xgettext:no-c-format*/ "+200% damage vs. demons");
|
|
case IPL_ALLRESZERO:
|
|
return _("All Resistance equals 0");
|
|
case IPL_STEALMANA:
|
|
if (HasAnyOf(item._iFlags, ItemSpecialEffect::StealMana3))
|
|
return _(/*xgettext:no-c-format*/ "hit steals 3% mana");
|
|
if (HasAnyOf(item._iFlags, ItemSpecialEffect::StealMana5))
|
|
return _(/*xgettext:no-c-format*/ "hit steals 5% mana");
|
|
return {};
|
|
case IPL_STEALLIFE:
|
|
if (HasAnyOf(item._iFlags, ItemSpecialEffect::StealLife3))
|
|
return _(/*xgettext:no-c-format*/ "hit steals 3% life");
|
|
if (HasAnyOf(item._iFlags, ItemSpecialEffect::StealLife5))
|
|
return _(/*xgettext:no-c-format*/ "hit steals 5% life");
|
|
return {};
|
|
case IPL_TARGAC:
|
|
return _("penetrates target's armor");
|
|
case IPL_FASTATTACK:
|
|
if (HasAnyOf(item._iFlags, ItemSpecialEffect::QuickAttack))
|
|
return _("quick attack");
|
|
if (HasAnyOf(item._iFlags, ItemSpecialEffect::FastAttack))
|
|
return _("fast attack");
|
|
if (HasAnyOf(item._iFlags, ItemSpecialEffect::FasterAttack))
|
|
return _("faster attack");
|
|
if (HasAnyOf(item._iFlags, ItemSpecialEffect::FastestAttack))
|
|
return _("fastest attack");
|
|
return _("Another ability (NW)");
|
|
case IPL_FASTRECOVER:
|
|
if (HasAnyOf(item._iFlags, ItemSpecialEffect::FastHitRecovery))
|
|
return _("fast hit recovery");
|
|
if (HasAnyOf(item._iFlags, ItemSpecialEffect::FasterHitRecovery))
|
|
return _("faster hit recovery");
|
|
if (HasAnyOf(item._iFlags, ItemSpecialEffect::FastestHitRecovery))
|
|
return _("fastest hit recovery");
|
|
return _("Another ability (NW)");
|
|
case IPL_FASTBLOCK:
|
|
return _("fast block");
|
|
case IPL_DAMMOD:
|
|
return fmt::format(fmt::runtime(ngettext("adds {:d} point to damage", "adds {:d} points to damage", item._iPLDamMod)), item._iPLDamMod);
|
|
case IPL_RNDARROWVEL:
|
|
return _("fires random speed arrows");
|
|
case IPL_SETDAM:
|
|
return _("unusual item damage");
|
|
case IPL_SETDUR:
|
|
return _("altered durability");
|
|
case IPL_ONEHAND:
|
|
return _("one handed sword");
|
|
case IPL_DRAINLIFE:
|
|
return _("constantly lose hit points");
|
|
case IPL_RNDSTEALLIFE:
|
|
return _("life stealing");
|
|
case IPL_NOMINSTR:
|
|
return _("no strength requirement");
|
|
case IPL_ADDACLIFE:
|
|
if (item._iFMinDam == item._iFMaxDam)
|
|
return fmt::format(fmt::runtime(_("lightning damage: {:d}")), item._iFMinDam);
|
|
else
|
|
return fmt::format(fmt::runtime(_("lightning damage: {:d}-{:d}")), item._iFMinDam, item._iFMaxDam);
|
|
case IPL_ADDMANAAC:
|
|
return _("charged bolts on hits");
|
|
case IPL_DEVASTATION:
|
|
return _("occasional triple damage");
|
|
case IPL_DECAY:
|
|
return fmt::format(fmt::runtime(_(/*xgettext:no-c-format*/ "decaying {:+d}% damage")), item._iPLDam);
|
|
case IPL_PERIL:
|
|
return _("2x dmg to monst, 1x to you");
|
|
case IPL_JESTERS:
|
|
return std::string(_(/*xgettext:no-c-format*/ "Random 0 - 600% damage"));
|
|
case IPL_CRYSTALLINE:
|
|
return fmt::format(fmt::runtime(_(/*xgettext:no-c-format*/ "low dur, {:+d}% damage")), item._iPLDam);
|
|
case IPL_DOPPELGANGER:
|
|
return fmt::format(fmt::runtime(_("to hit: {:+d}%, {:+d}% damage")), item._iPLToHit, item._iPLDam);
|
|
case IPL_ACDEMON:
|
|
return _("extra AC vs demons");
|
|
case IPL_ACUNDEAD:
|
|
return _("extra AC vs undead");
|
|
case IPL_MANATOLIFE:
|
|
return _("50% Mana moved to Health");
|
|
case IPL_LIFETOMANA:
|
|
return _("40% Health moved to Mana");
|
|
default:
|
|
return _("Another ability (NW)");
|
|
}
|
|
}
|
|
|
|
void DrawUniqueInfo(const Surface &out)
|
|
{
|
|
const Point position = DrawUniqueInfoWindow(out);
|
|
|
|
Rectangle rect { position + Displacement { 32, 56 }, { 257, 0 } };
|
|
const UniqueItem &uitem = UniqueItems[curruitem._iUid];
|
|
DrawString(out, _(uitem.UIName), rect, { .flags = UiFlags::AlignCenter });
|
|
|
|
const Rectangle dividerLineRect { position + Displacement { 26, 25 }, { 267, 3 } };
|
|
out.BlitFrom(out, MakeSdlRect(dividerLineRect), dividerLineRect.position + Displacement { 0, (5 * 12) + 13 });
|
|
|
|
rect.position.y += (10 - uitem.UINumPL) * 12;
|
|
assert(uitem.UINumPL <= sizeof(uitem.powers) / sizeof(*uitem.powers));
|
|
const TextRenderOptions textRenderOptions { .flags = UiFlags::ColorWhite | UiFlags::AlignCenter };
|
|
const GameFontTables fontSize = GetFontSizeFromUiFlags(textRenderOptions.flags);
|
|
for (const auto &power : uitem.powers) {
|
|
if (power.type == IPL_INVALID)
|
|
break;
|
|
rect.position.y += 2 * 12;
|
|
// Pre-wrap the string at spaces, otherwise DrawString would hard wrap in the middle of words.
|
|
const std::string wrapped = WordWrapString(PrintItemPower(power.type, curruitem), rect.size.width);
|
|
DrawString(out, wrapped, rect, textRenderOptions);
|
|
for (const std::string_view line : SplitByChar(wrapped, '\n')) {
|
|
if (line.data() + line.size() == wrapped.data() + wrapped.size()) break;
|
|
rect.position.y += GetLineHeight(line, fontSize);
|
|
}
|
|
}
|
|
}
|
|
|
|
void PrintItemDetails(const Item &item)
|
|
{
|
|
if (HeadlessMode)
|
|
return;
|
|
|
|
if (item._iClass == ICLASS_WEAPON) {
|
|
if (item._iMinDam == item._iMaxDam) {
|
|
if (item._iMaxDur == DUR_INDESTRUCTIBLE)
|
|
AddItemInfoBoxString(fmt::format(fmt::runtime(_("damage: {:d} Indestructible")), item._iMinDam));
|
|
else
|
|
AddItemInfoBoxString(fmt::format(fmt::runtime(_(/* TRANSLATORS: Dur: is durability */ "damage: {:d} Dur: {:d}/{:d}")), item._iMinDam, item._iDurability, item._iMaxDur));
|
|
} else {
|
|
if (item._iMaxDur == DUR_INDESTRUCTIBLE)
|
|
AddItemInfoBoxString(fmt::format(fmt::runtime(_("damage: {:d}-{:d} Indestructible")), item._iMinDam, item._iMaxDam));
|
|
else
|
|
AddItemInfoBoxString(fmt::format(fmt::runtime(_(/* TRANSLATORS: Dur: is durability */ "damage: {:d}-{:d} Dur: {:d}/{:d}")), item._iMinDam, item._iMaxDam, item._iDurability, item._iMaxDur));
|
|
}
|
|
}
|
|
if (item._iClass == ICLASS_ARMOR) {
|
|
if (item._iMaxDur == DUR_INDESTRUCTIBLE)
|
|
AddItemInfoBoxString(fmt::format(fmt::runtime(_("armor: {:d} Indestructible")), item._iAC));
|
|
else
|
|
AddItemInfoBoxString(fmt::format(fmt::runtime(_(/* TRANSLATORS: Dur: is durability */ "armor: {:d} Dur: {:d}/{:d}")), item._iAC, item._iDurability, item._iMaxDur));
|
|
}
|
|
if (item._iMiscId == IMISC_STAFF && item._iMaxCharges != 0) {
|
|
AddItemInfoBoxString(fmt::format(fmt::runtime(_("Charges: {:d}/{:d}")), item._iCharges, item._iMaxCharges));
|
|
}
|
|
if (item._iPrePower != -1) {
|
|
AddItemInfoBoxString(PrintItemPower(item._iPrePower, item));
|
|
}
|
|
if (item._iSufPower != -1) {
|
|
AddItemInfoBoxString(PrintItemPower(item._iSufPower, item));
|
|
}
|
|
if (item._iMagical == ITEM_QUALITY_UNIQUE) {
|
|
AddItemInfoBoxString(_("unique item"));
|
|
ShowUniqueItemInfoBox = true;
|
|
curruitem = item;
|
|
}
|
|
PrintItemInfo(item);
|
|
}
|
|
|
|
void PrintItemDur(const Item &item)
|
|
{
|
|
if (HeadlessMode)
|
|
return;
|
|
|
|
if (item._iClass == ICLASS_WEAPON) {
|
|
if (item._iMinDam == item._iMaxDam) {
|
|
if (item._iMaxDur == DUR_INDESTRUCTIBLE)
|
|
AddItemInfoBoxString(fmt::format(fmt::runtime(_("damage: {:d} Indestructible")), item._iMinDam));
|
|
else
|
|
AddItemInfoBoxString(fmt::format(fmt::runtime(_("damage: {:d} Dur: {:d}/{:d}")), item._iMinDam, item._iDurability, item._iMaxDur));
|
|
} else {
|
|
if (item._iMaxDur == DUR_INDESTRUCTIBLE)
|
|
AddItemInfoBoxString(fmt::format(fmt::runtime(_("damage: {:d}-{:d} Indestructible")), item._iMinDam, item._iMaxDam));
|
|
else
|
|
AddItemInfoBoxString(fmt::format(fmt::runtime(_("damage: {:d}-{:d} Dur: {:d}/{:d}")), item._iMinDam, item._iMaxDam, item._iDurability, item._iMaxDur));
|
|
}
|
|
if (item._iMiscId == IMISC_STAFF && item._iMaxCharges > 0) {
|
|
AddItemInfoBoxString(fmt::format(fmt::runtime(_("Charges: {:d}/{:d}")), item._iCharges, item._iMaxCharges));
|
|
}
|
|
if (item._iMagical != ITEM_QUALITY_NORMAL)
|
|
AddItemInfoBoxString(_("Not Identified"));
|
|
}
|
|
if (item._iClass == ICLASS_ARMOR) {
|
|
if (item._iMaxDur == DUR_INDESTRUCTIBLE)
|
|
AddItemInfoBoxString(fmt::format(fmt::runtime(_("armor: {:d} Indestructible")), item._iAC));
|
|
else
|
|
AddItemInfoBoxString(fmt::format(fmt::runtime(_("armor: {:d} Dur: {:d}/{:d}")), item._iAC, item._iDurability, item._iMaxDur));
|
|
if (item._iMagical != ITEM_QUALITY_NORMAL)
|
|
AddItemInfoBoxString(_("Not Identified"));
|
|
if (item._iMiscId == IMISC_STAFF && item._iMaxCharges > 0) {
|
|
AddItemInfoBoxString(fmt::format(fmt::runtime(_("Charges: {:d}/{:d}")), item._iCharges, item._iMaxCharges));
|
|
}
|
|
}
|
|
if (IsAnyOf(item._itype, ItemType::Ring, ItemType::Amulet))
|
|
AddItemInfoBoxString(_("Not Identified"));
|
|
PrintItemInfo(item);
|
|
}
|
|
|
|
void UseItem(Player &player, item_misc_id mid, SpellID spellID, int spellFrom)
|
|
{
|
|
std::optional<SpellID> prepareSpellID;
|
|
|
|
switch (mid) {
|
|
case IMISC_HEAL:
|
|
player.RestorePartialLife();
|
|
if (&player == MyPlayer) {
|
|
RedrawComponent(PanelDrawComponent::Health);
|
|
}
|
|
break;
|
|
case IMISC_FULLHEAL:
|
|
player.RestoreFullLife();
|
|
if (&player == MyPlayer) {
|
|
RedrawComponent(PanelDrawComponent::Health);
|
|
}
|
|
break;
|
|
case IMISC_MANA:
|
|
player.RestorePartialMana();
|
|
if (&player == MyPlayer) {
|
|
RedrawComponent(PanelDrawComponent::Mana);
|
|
}
|
|
break;
|
|
case IMISC_FULLMANA:
|
|
player.RestoreFullMana();
|
|
if (&player == MyPlayer) {
|
|
RedrawComponent(PanelDrawComponent::Mana);
|
|
}
|
|
break;
|
|
case IMISC_ELIXSTR:
|
|
ModifyPlrStr(player, 1);
|
|
break;
|
|
case IMISC_ELIXMAG:
|
|
ModifyPlrMag(player, 1);
|
|
if (gbIsHellfire) {
|
|
player.RestoreFullMana();
|
|
if (&player == MyPlayer) {
|
|
RedrawComponent(PanelDrawComponent::Mana);
|
|
}
|
|
}
|
|
break;
|
|
case IMISC_ELIXDEX:
|
|
ModifyPlrDex(player, 1);
|
|
break;
|
|
case IMISC_ELIXVIT:
|
|
ModifyPlrVit(player, 1);
|
|
if (gbIsHellfire) {
|
|
player.RestoreFullLife();
|
|
if (&player == MyPlayer) {
|
|
RedrawComponent(PanelDrawComponent::Health);
|
|
}
|
|
}
|
|
break;
|
|
case IMISC_REJUV: {
|
|
player.RestorePartialLife();
|
|
player.RestorePartialMana();
|
|
if (&player == MyPlayer) {
|
|
RedrawComponent(PanelDrawComponent::Health);
|
|
RedrawComponent(PanelDrawComponent::Mana);
|
|
}
|
|
} break;
|
|
case IMISC_FULLREJUV:
|
|
case IMISC_ARENAPOT:
|
|
player.RestoreFullLife();
|
|
player.RestoreFullMana();
|
|
if (&player == MyPlayer) {
|
|
RedrawComponent(PanelDrawComponent::Health);
|
|
RedrawComponent(PanelDrawComponent::Mana);
|
|
}
|
|
break;
|
|
case IMISC_SCROLL:
|
|
case IMISC_SCROLLT:
|
|
if (ControlMode == ControlTypes::KeyboardAndMouse && GetSpellData(spellID).isTargeted()) {
|
|
prepareSpellID = spellID;
|
|
} else {
|
|
// Find a valid target for the spell because tile coords
|
|
// will be validated when processing the network message
|
|
Point target = cursPosition;
|
|
if (!InDungeonBounds(target))
|
|
target = player.position.future + Displacement(player._pdir);
|
|
// Use CMD_SPELLXY because it's the same behavior as normal casting
|
|
assert(IsValidSpellFrom(spellFrom));
|
|
NetSendCmdLocParam3(true, CMD_SPELLXY, target, static_cast<int8_t>(spellID), static_cast<uint8_t>(SpellType::Scroll), static_cast<uint16_t>(spellFrom));
|
|
}
|
|
break;
|
|
case IMISC_BOOK: {
|
|
const uint8_t newSpellLevel = player._pSplLvl[static_cast<int8_t>(spellID)] + 1;
|
|
if (newSpellLevel <= MaxSpellLevel) {
|
|
player._pSplLvl[static_cast<int8_t>(spellID)] = newSpellLevel;
|
|
NetSendCmdParam2(true, CMD_CHANGE_SPELL_LEVEL, static_cast<uint16_t>(spellID), newSpellLevel);
|
|
}
|
|
if (HasNoneOf(player._pIFlags, ItemSpecialEffect::NoMana)) {
|
|
player._pMana += GetSpellData(spellID).sManaCost << 6;
|
|
player._pMana = std::min(player._pMana, player._pMaxMana);
|
|
player._pManaBase += GetSpellData(spellID).sManaCost << 6;
|
|
player._pManaBase = std::min(player._pManaBase, player._pMaxManaBase);
|
|
}
|
|
if (&player == MyPlayer) {
|
|
for (Item &item : InventoryPlayerItemsRange { player }) {
|
|
item.updateRequiredStatsCacheForPlayer(player);
|
|
}
|
|
if (IsStashOpen) {
|
|
Stash.RefreshItemStatFlags();
|
|
}
|
|
}
|
|
RedrawComponent(PanelDrawComponent::Mana);
|
|
} break;
|
|
case IMISC_MAPOFDOOM:
|
|
doom_init();
|
|
break;
|
|
case IMISC_OILACC:
|
|
case IMISC_OILMAST:
|
|
case IMISC_OILSHARP:
|
|
case IMISC_OILDEATH:
|
|
case IMISC_OILSKILL:
|
|
case IMISC_OILBSMTH:
|
|
case IMISC_OILFORT:
|
|
case IMISC_OILPERM:
|
|
case IMISC_OILHARD:
|
|
case IMISC_OILIMP:
|
|
player._pOilType = mid;
|
|
if (&player != MyPlayer) {
|
|
return;
|
|
}
|
|
if (SpellbookFlag) {
|
|
SpellbookFlag = false;
|
|
}
|
|
if (!invflag) {
|
|
invflag = true;
|
|
}
|
|
NewCursor(CURSOR_OIL);
|
|
break;
|
|
case IMISC_SPECELIX:
|
|
ModifyPlrStr(player, 3);
|
|
ModifyPlrMag(player, 3);
|
|
ModifyPlrDex(player, 3);
|
|
ModifyPlrVit(player, 3);
|
|
break;
|
|
case IMISC_RUNEF:
|
|
prepareSpellID = SpellID::RuneOfFire;
|
|
break;
|
|
case IMISC_RUNEL:
|
|
prepareSpellID = SpellID::RuneOfLight;
|
|
break;
|
|
case IMISC_GR_RUNEL:
|
|
prepareSpellID = SpellID::RuneOfNova;
|
|
break;
|
|
case IMISC_GR_RUNEF:
|
|
prepareSpellID = SpellID::RuneOfImmolation;
|
|
break;
|
|
case IMISC_RUNES:
|
|
prepareSpellID = SpellID::RuneOfStone;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (prepareSpellID) {
|
|
assert(IsValidSpellFrom(spellFrom));
|
|
player.inventorySpell = *prepareSpellID;
|
|
player.spellFrom = spellFrom;
|
|
if (&player == MyPlayer)
|
|
NewCursor(CURSOR_TELEPORT);
|
|
}
|
|
}
|
|
|
|
bool UseItemOpensHive(const Item &item, Point position)
|
|
{
|
|
if (item.IDidx != IDI_RUNEBOMB)
|
|
return false;
|
|
for (auto dir : PathDirs) {
|
|
const Point adjacentPosition = position + dir;
|
|
if (OpensHive(adjacentPosition))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool UseItemOpensGrave(const Item &item, Point position)
|
|
{
|
|
if (item.IDidx != IDI_MAPOFDOOM)
|
|
return false;
|
|
for (auto dir : PathDirs) {
|
|
const Point adjacentPosition = position + dir;
|
|
if (OpensGrave(adjacentPosition))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void SpawnSmith(int lvl)
|
|
{
|
|
constexpr int PinnedItemCount = 0;
|
|
|
|
int maxValue = MaxVendorValue;
|
|
int maxItems = NumSmithBasicItems;
|
|
if (gbIsHellfire) {
|
|
maxValue = MaxVendorValueHf;
|
|
maxItems = NumSmithBasicItemsHf;
|
|
}
|
|
|
|
const size_t iCnt = RandomIntBetween(10, maxItems);
|
|
SmithItems.clear();
|
|
|
|
while (SmithItems.size() < iCnt) {
|
|
Item newItem;
|
|
do {
|
|
newItem = {};
|
|
newItem._iSeed = AdvanceRndSeed();
|
|
SetRndSeed(newItem._iSeed);
|
|
const _item_indexes itemData = RndSmithItem(*MyPlayer, lvl);
|
|
GetItemAttrs(newItem, itemData, lvl);
|
|
} while (newItem._iIvalue > maxValue);
|
|
|
|
newItem._iCreateInfo = lvl | CF_SMITH;
|
|
newItem._iIdentified = true;
|
|
|
|
SmithItems.push_back(newItem);
|
|
}
|
|
|
|
SortVendor(SmithItems, PinnedItemCount);
|
|
}
|
|
|
|
void ReplacePremium(const Player &player, int idx)
|
|
{
|
|
int plvl = gbIsHellfire ? itemLevelAddHf[idx] : itemLevelAdd[idx];
|
|
plvl += player.getCharacterLevel();
|
|
SpawnOnePremium(PremiumItems[idx], plvl, player);
|
|
}
|
|
|
|
void SpawnPremium(const Player &player)
|
|
{
|
|
const int lvl = player.getCharacterLevel();
|
|
const size_t maxItems = gbIsHellfire ? NumSmithItemsHf : NumSmithItems;
|
|
|
|
while (PremiumItems.size() < maxItems) {
|
|
int plvl = PremiumItemLevel + (gbIsHellfire ? itemLevelAddHf[PremiumItems.size()] : itemLevelAdd[PremiumItems.size()]);
|
|
Item item = {};
|
|
SpawnOnePremium(item, plvl, player);
|
|
PremiumItems.push_back(item);
|
|
}
|
|
|
|
while (PremiumItemLevel < lvl) {
|
|
PremiumItemLevel++;
|
|
Item *ptr = PremiumItems.begin();
|
|
if (gbIsHellfire) {
|
|
std::move(ptr + 3, ptr + 13, ptr);
|
|
PremiumItems[11] = PremiumItems[13];
|
|
PremiumItems[13] = PremiumItems[14];
|
|
SpawnOnePremium(PremiumItems[10], PremiumItemLevel + itemLevelAddHf[10], player);
|
|
SpawnOnePremium(PremiumItems[12], PremiumItemLevel + itemLevelAddHf[12], player);
|
|
SpawnOnePremium(PremiumItems[14], PremiumItemLevel + itemLevelAddHf[14], player);
|
|
} else {
|
|
std::move(ptr + 2, ptr + 5, ptr);
|
|
PremiumItems[4] = PremiumItems[5];
|
|
SpawnOnePremium(PremiumItems[3], PremiumItemLevel + itemLevelAdd[3], player);
|
|
SpawnOnePremium(PremiumItems[5], PremiumItemLevel + itemLevelAdd[5], player);
|
|
}
|
|
}
|
|
}
|
|
|
|
void SpawnWitch(int lvl)
|
|
{
|
|
constexpr int PinnedItemCount = NumWitchPinnedItems;
|
|
constexpr std::array<_item_indexes, PinnedItemCount> PinnedItemTypes = { IDI_MANA, IDI_FULLMANA, IDI_PORTAL };
|
|
constexpr int MaxPinnedBookCount = 4;
|
|
constexpr std::array<_item_indexes, MaxPinnedBookCount> PinnedBookTypes = { IDI_BOOK1, IDI_BOOK2, IDI_BOOK3, IDI_BOOK4 };
|
|
|
|
int bookCount = 0;
|
|
const int pinnedBookCount = gbIsHellfire ? RandomIntLessThan(MaxPinnedBookCount) : 0;
|
|
const int itemCount = RandomIntBetween(10, gbIsHellfire ? NumWitchItemsHf : NumWitchItems);
|
|
const int maxValue = gbIsHellfire ? MaxVendorValueHf : MaxVendorValue;
|
|
WitchItems.clear();
|
|
|
|
for (int i = 0; i < itemCount; i++) {
|
|
Item item = {};
|
|
|
|
if (i < PinnedItemCount) {
|
|
item._iSeed = AdvanceRndSeed();
|
|
GetItemAttrs(item, PinnedItemTypes[i], 1);
|
|
item._iCreateInfo = lvl;
|
|
item._iStatFlag = true;
|
|
WitchItems.push_back(item);
|
|
continue;
|
|
}
|
|
|
|
if (gbIsHellfire) {
|
|
if (i < PinnedItemCount + MaxPinnedBookCount && bookCount < pinnedBookCount) {
|
|
const _item_indexes bookType = PinnedBookTypes[i - PinnedItemCount];
|
|
if (lvl >= AllItemsList[bookType].iMinMLvl) {
|
|
item._iSeed = AdvanceRndSeed();
|
|
SetRndSeed(item._iSeed);
|
|
DiscardRandomValues(1);
|
|
GetItemAttrs(item, bookType, lvl);
|
|
item._iCreateInfo = lvl | CF_WITCH;
|
|
item._iIdentified = true;
|
|
bookCount++;
|
|
WitchItems.push_back(item);
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
do {
|
|
item = {};
|
|
item._iSeed = AdvanceRndSeed();
|
|
SetRndSeed(item._iSeed);
|
|
const _item_indexes itemData = RndWitchItem(*MyPlayer, lvl);
|
|
GetItemAttrs(item, itemData, lvl);
|
|
int maxlvl = -1;
|
|
if (GenerateRnd(100) <= 5)
|
|
maxlvl = 2 * lvl;
|
|
if (maxlvl == -1 && item._iMiscId == IMISC_STAFF)
|
|
maxlvl = 2 * lvl;
|
|
if (maxlvl != -1)
|
|
GetItemBonus(*MyPlayer, item, maxlvl / 2, maxlvl, true, true);
|
|
} while (item._iIvalue > maxValue);
|
|
|
|
item._iCreateInfo = lvl | CF_WITCH;
|
|
item._iIdentified = true;
|
|
|
|
WitchItems.push_back(item);
|
|
}
|
|
|
|
SortVendor(WitchItems, PinnedItemCount);
|
|
}
|
|
|
|
void SpawnBoy(int lvl)
|
|
{
|
|
int ivalue = 0;
|
|
bool keepgoing = false;
|
|
int count = 0;
|
|
|
|
const Player &myPlayer = *MyPlayer;
|
|
|
|
const HeroClass pc = myPlayer._pClass;
|
|
int strength = std::max(myPlayer.GetMaximumAttributeValue(CharacterAttribute::Strength), myPlayer._pStrength);
|
|
int dexterity = std::max(myPlayer.GetMaximumAttributeValue(CharacterAttribute::Dexterity), myPlayer._pDexterity);
|
|
int magic = std::max(myPlayer.GetMaximumAttributeValue(CharacterAttribute::Magic), myPlayer._pMagic);
|
|
strength += strength / 5;
|
|
dexterity += dexterity / 5;
|
|
magic += magic / 5;
|
|
|
|
if (BoyItemLevel >= (lvl / 2) && !BoyItem.isEmpty())
|
|
return;
|
|
do {
|
|
keepgoing = false;
|
|
BoyItem = {};
|
|
BoyItem._iSeed = AdvanceRndSeed();
|
|
SetRndSeed(BoyItem._iSeed);
|
|
const _item_indexes itype = RndBoyItem(*MyPlayer, lvl);
|
|
GetItemAttrs(BoyItem, itype, lvl);
|
|
GetItemBonus(*MyPlayer, BoyItem, lvl, 2 * lvl, true, true);
|
|
|
|
if (!gbIsHellfire) {
|
|
if (BoyItem._iIvalue > MaxBoyValue) {
|
|
keepgoing = true; // prevent breaking the do/while loop too early by failing hellfire's condition in while
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
|
|
ivalue = 0;
|
|
|
|
const ItemType itemType = BoyItem._itype;
|
|
|
|
switch (itemType) {
|
|
case ItemType::LightArmor:
|
|
case ItemType::MediumArmor:
|
|
case ItemType::HeavyArmor: {
|
|
const auto *const mostValuablePlayerArmor = myPlayer.GetMostValuableItem(
|
|
[](const Item &item) {
|
|
return IsAnyOf(item._itype, ItemType::LightArmor, ItemType::MediumArmor, ItemType::HeavyArmor);
|
|
});
|
|
|
|
ivalue = mostValuablePlayerArmor == nullptr ? 0 : mostValuablePlayerArmor->_iIvalue;
|
|
break;
|
|
}
|
|
case ItemType::Shield:
|
|
case ItemType::Axe:
|
|
case ItemType::Bow:
|
|
case ItemType::Mace:
|
|
case ItemType::Sword:
|
|
case ItemType::Helm:
|
|
case ItemType::Staff:
|
|
case ItemType::Ring:
|
|
case ItemType::Amulet: {
|
|
const auto *const mostValuablePlayerItem = myPlayer.GetMostValuableItem(
|
|
[itemType](const Item &item) { return item._itype == itemType; });
|
|
|
|
ivalue = mostValuablePlayerItem == nullptr ? 0 : mostValuablePlayerItem->_iIvalue;
|
|
break;
|
|
}
|
|
default:
|
|
app_fatal("Invalid item spawn");
|
|
}
|
|
ivalue = ivalue * 4 / 5; // avoids forced int > float > int conversion
|
|
|
|
count++;
|
|
|
|
if (count < 200) {
|
|
switch (pc) {
|
|
case HeroClass::Warrior:
|
|
if (IsAnyOf(itemType, ItemType::Bow, ItemType::Staff))
|
|
ivalue = INT_MAX;
|
|
break;
|
|
case HeroClass::Rogue:
|
|
if (IsAnyOf(itemType, ItemType::Sword, ItemType::Staff, ItemType::Axe, ItemType::Mace, ItemType::Shield))
|
|
ivalue = INT_MAX;
|
|
break;
|
|
case HeroClass::Sorcerer:
|
|
if (IsAnyOf(itemType, ItemType::Staff, ItemType::Axe, ItemType::Bow, ItemType::Mace))
|
|
ivalue = INT_MAX;
|
|
break;
|
|
case HeroClass::Monk:
|
|
if (IsAnyOf(itemType, ItemType::Bow, ItemType::MediumArmor, ItemType::Shield, ItemType::Mace))
|
|
ivalue = INT_MAX;
|
|
break;
|
|
case HeroClass::Bard:
|
|
if (IsAnyOf(itemType, ItemType::Axe, ItemType::Mace, ItemType::Staff))
|
|
ivalue = INT_MAX;
|
|
break;
|
|
case HeroClass::Barbarian:
|
|
if (IsAnyOf(itemType, ItemType::Bow, ItemType::Staff))
|
|
ivalue = INT_MAX;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
} while (keepgoing
|
|
|| ((
|
|
BoyItem._iIvalue > MaxBoyValueHf
|
|
|| BoyItem._iMinStr > strength
|
|
|| BoyItem._iMinMag > magic
|
|
|| BoyItem._iMinDex > dexterity
|
|
|| BoyItem._iIvalue < ivalue)
|
|
&& count < 250));
|
|
BoyItem._iCreateInfo = lvl | CF_BOY;
|
|
BoyItem._iIdentified = true;
|
|
BoyItemLevel = lvl / 2;
|
|
}
|
|
|
|
void SpawnHealer(int lvl)
|
|
{
|
|
constexpr size_t PinnedItemCount = NumHealerPinnedItems;
|
|
constexpr std::array<_item_indexes, PinnedItemCount + 1> PinnedItemTypes = { IDI_HEAL, IDI_FULLHEAL, IDI_RESURRECT };
|
|
const auto itemCount = static_cast<size_t>(RandomIntBetween(10, gbIsHellfire ? NumHealerItemsHf : NumHealerItems));
|
|
HealerItems.clear();
|
|
|
|
for (size_t i = 0; i < itemCount; i++) {
|
|
Item item = {};
|
|
|
|
if (i < PinnedItemCount || (gbIsMultiplayer && i < NumHealerPinnedItemsMp)) {
|
|
item._iSeed = AdvanceRndSeed();
|
|
GetItemAttrs(item, PinnedItemTypes[i], 1);
|
|
item._iCreateInfo = lvl;
|
|
item._iStatFlag = true;
|
|
} else {
|
|
item._iSeed = AdvanceRndSeed();
|
|
SetRndSeed(item._iSeed);
|
|
const _item_indexes itype = RndHealerItem(*MyPlayer, lvl);
|
|
GetItemAttrs(item, itype, lvl);
|
|
item._iCreateInfo = lvl | CF_HEALER;
|
|
item._iIdentified = true;
|
|
}
|
|
|
|
HealerItems.push_back(item);
|
|
}
|
|
|
|
SortVendor(HealerItems, PinnedItemCount);
|
|
}
|
|
|
|
void MakeGoldStack(Item &goldItem, int value)
|
|
{
|
|
InitializeItem(goldItem, IDI_GOLD);
|
|
GenerateNewSeed(goldItem);
|
|
goldItem._iStatFlag = true;
|
|
goldItem._ivalue = value;
|
|
SetPlrHandGoldCurs(goldItem);
|
|
}
|
|
|
|
int ItemNoFlippy()
|
|
{
|
|
const int r = ActiveItems[ActiveItemCount - 1];
|
|
Items[r].AnimInfo.currentFrame = Items[r].AnimInfo.numberOfFrames - 1;
|
|
Items[r]._iAnimFlag = false;
|
|
Items[r].selectionRegion = SelectionRegion::Bottom;
|
|
|
|
return r;
|
|
}
|
|
|
|
void CreateSpellBook(Point position, SpellID ispell, bool sendmsg, bool delta)
|
|
{
|
|
int lvl = currlevel;
|
|
|
|
if (gbIsHellfire) {
|
|
lvl = GetSpellBookLevel(ispell) + 1;
|
|
if (lvl < 1) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
const _item_indexes idx = RndTypeItems(ItemType::Misc, IMISC_BOOK, lvl);
|
|
if (ActiveItemCount >= MAXITEMS)
|
|
return;
|
|
|
|
const int ii = AllocateItem();
|
|
auto &item = Items[ii];
|
|
|
|
while (true) {
|
|
item = {};
|
|
SetupAllItems(*MyPlayer, item, idx, AdvanceRndSeed(), 2 * lvl, 1, true, delta);
|
|
SetupItem(item);
|
|
if (item._iMiscId == IMISC_BOOK && item._iSpell == ispell)
|
|
break;
|
|
}
|
|
GetSuperItemSpace(position, ii);
|
|
|
|
if (sendmsg)
|
|
NetSendCmdPItem(false, CMD_DROPITEM, item.position, item);
|
|
if (delta)
|
|
DeltaAddItem(ii);
|
|
}
|
|
|
|
void CreateMagicArmor(Point position, ItemType itemType, int icurs, bool sendmsg, bool delta)
|
|
{
|
|
const int lvl = ItemsGetCurrlevel();
|
|
CreateMagicItem(position, lvl, itemType, IMISC_NONE, icurs, sendmsg, delta);
|
|
}
|
|
|
|
void CreateAmulet(Point position, int lvl, bool sendmsg, bool delta, bool spawn /*= false*/)
|
|
{
|
|
CreateMagicItem(position, lvl, ItemType::Amulet, IMISC_AMULET, ICURS_AMULET, sendmsg, delta, spawn);
|
|
}
|
|
|
|
void CreateMagicWeapon(Point position, ItemType itemType, int icurs, bool sendmsg, bool delta)
|
|
{
|
|
int imid = IMISC_NONE;
|
|
if (itemType == ItemType::Staff)
|
|
imid = IMISC_STAFF;
|
|
|
|
const int curlv = ItemsGetCurrlevel();
|
|
|
|
CreateMagicItem(position, curlv, itemType, imid, icurs, sendmsg, delta);
|
|
}
|
|
|
|
bool GetItemRecord(uint32_t nSeed, uint16_t wCI, int nIndex)
|
|
{
|
|
const uint32_t ticks = SDL_GetTicks();
|
|
|
|
for (int i = 0; i < gnNumGetRecords; i++) {
|
|
if (ticks - itemrecord[i].dwTimestamp > 6000) {
|
|
// BUGFIX: loot actions for multiple quest items with same seed (e.g. blood stone) performed within less than 6 seconds will be ignored.
|
|
NextItemRecord(i);
|
|
i--;
|
|
} else if (nSeed == itemrecord[i].nSeed && wCI == itemrecord[i].wCI && nIndex == itemrecord[i].nIndex) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void SetItemRecord(uint32_t nSeed, uint16_t wCI, int nIndex)
|
|
{
|
|
const uint32_t ticks = SDL_GetTicks();
|
|
|
|
if (gnNumGetRecords == MAXITEMS) {
|
|
return;
|
|
}
|
|
|
|
itemrecord[gnNumGetRecords].dwTimestamp = ticks;
|
|
itemrecord[gnNumGetRecords].nSeed = nSeed;
|
|
itemrecord[gnNumGetRecords].wCI = wCI;
|
|
itemrecord[gnNumGetRecords].nIndex = nIndex;
|
|
gnNumGetRecords++;
|
|
}
|
|
|
|
void PutItemRecord(uint32_t nSeed, uint16_t wCI, int nIndex)
|
|
{
|
|
const uint32_t ticks = SDL_GetTicks();
|
|
|
|
for (int i = 0; i < gnNumGetRecords; i++) {
|
|
if (ticks - itemrecord[i].dwTimestamp > 6000) {
|
|
NextItemRecord(i);
|
|
i--;
|
|
} else if (nSeed == itemrecord[i].nSeed && wCI == itemrecord[i].wCI && nIndex == itemrecord[i].nIndex) {
|
|
NextItemRecord(i);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool Item::isUsable() const
|
|
{
|
|
if (IDidx == IDI_SPECELIX && Quests[Q_MUSHROOM]._qactive != QUEST_DONE)
|
|
return false;
|
|
return AllItemsList[IDidx].iUsable;
|
|
}
|
|
|
|
void Item::setNewAnimation(bool showAnimation)
|
|
{
|
|
const int8_t it = ItemCAnimTbl[_iCurs];
|
|
const int8_t numberOfFrames = ItemAnimLs[it];
|
|
const OptionalClxSpriteList sprite = itemanims[it] ? OptionalClxSpriteList { *itemanims[static_cast<size_t>(it)] } : std::nullopt;
|
|
if (_iCurs != ICURS_MAGIC_ROCK)
|
|
AnimInfo.setNewAnimation(sprite, numberOfFrames, 1, AnimationDistributionFlags::ProcessAnimationPending, 0, numberOfFrames);
|
|
else
|
|
AnimInfo.setNewAnimation(sprite, numberOfFrames, 1);
|
|
_iPostDraw = false;
|
|
_iRequest = false;
|
|
if (showAnimation) {
|
|
_iAnimFlag = true;
|
|
selectionRegion = SelectionRegion::None;
|
|
} else {
|
|
AnimInfo.currentFrame = AnimInfo.numberOfFrames - 1;
|
|
_iAnimFlag = false;
|
|
selectionRegion = SelectionRegion::Bottom;
|
|
}
|
|
}
|
|
|
|
void Item::updateRequiredStatsCacheForPlayer(const Player &player)
|
|
{
|
|
if (_itype == ItemType::Misc && _iMiscId == IMISC_BOOK) {
|
|
_iMinMag = GetSpellData(_iSpell).minInt;
|
|
int8_t spellLevel = player._pSplLvl[static_cast<int8_t>(_iSpell)];
|
|
while (spellLevel != 0) {
|
|
_iMinMag += 20 * _iMinMag / 100;
|
|
spellLevel--;
|
|
if (_iMinMag + 20 * _iMinMag / 100 > 255) {
|
|
_iMinMag = 255;
|
|
spellLevel = 0;
|
|
}
|
|
}
|
|
}
|
|
_iStatFlag = player.CanUseItem(*this);
|
|
}
|
|
|
|
StringOrView Item::getName() const
|
|
{
|
|
if (isEmpty()) {
|
|
return std::string_view("");
|
|
}
|
|
if (!_iIdentified || _iCreateInfo == 0 || _iMagical == ITEM_QUALITY_NORMAL) {
|
|
return GetTranslatedItemName(*this);
|
|
}
|
|
if (_iMagical == ITEM_QUALITY_UNIQUE) {
|
|
return _(UniqueItems[_iUid].UIName);
|
|
}
|
|
return GetTranslatedItemNameMagical(*this, dwBuff & CF_HELLFIRE, true, std::nullopt);
|
|
}
|
|
|
|
bool CornerStoneStruct::isAvailable()
|
|
{
|
|
return currlevel == 21 && !gbIsMultiplayer;
|
|
}
|
|
|
|
void initItemGetRecords()
|
|
{
|
|
memset(itemrecord, 0, sizeof(itemrecord));
|
|
gnNumGetRecords = 0;
|
|
}
|
|
|
|
void RepairItem(Item &item, int lvl)
|
|
{
|
|
if (item._iDurability == item._iMaxDur) {
|
|
return;
|
|
}
|
|
|
|
if (item._iMaxDur <= 0) {
|
|
item.clear();
|
|
return;
|
|
}
|
|
|
|
int rep = 0;
|
|
do {
|
|
rep += lvl + GenerateRnd(lvl);
|
|
item._iMaxDur -= std::max(item._iMaxDur / (lvl + 9), 1);
|
|
if (item._iMaxDur == 0) {
|
|
item.clear();
|
|
return;
|
|
}
|
|
} while (rep + item._iDurability < item._iMaxDur);
|
|
|
|
item._iDurability = std::min<int>(item._iDurability + rep, item._iMaxDur);
|
|
}
|
|
|
|
void RechargeItem(Item &item, Player &player)
|
|
{
|
|
if (item._itype != ItemType::Staff || !IsValidSpell(item._iSpell))
|
|
return;
|
|
|
|
if (item._iCharges == item._iMaxCharges)
|
|
return;
|
|
|
|
const int rechargeStrength = RandomIntBetween(1, player.getCharacterLevel() / GetSpellStaffLevel(item._iSpell));
|
|
|
|
do {
|
|
item._iMaxCharges--;
|
|
if (item._iMaxCharges == 0) {
|
|
return;
|
|
}
|
|
item._iCharges += rechargeStrength;
|
|
} while (item._iCharges < item._iMaxCharges);
|
|
|
|
item._iCharges = std::min(item._iCharges, item._iMaxCharges);
|
|
|
|
if (&player != MyPlayer)
|
|
return;
|
|
if (&item == &player.InvBody[INVLOC_HAND_LEFT]) {
|
|
NetSendCmdChItem(true, INVLOC_HAND_LEFT);
|
|
return;
|
|
}
|
|
if (&item == &player.InvBody[INVLOC_HAND_RIGHT]) {
|
|
NetSendCmdChItem(true, INVLOC_HAND_RIGHT);
|
|
return;
|
|
}
|
|
for (int i = 0; i < player._pNumInv; i++) {
|
|
if (&item == &player.InvList[i]) {
|
|
NetSyncInvItem(player, i);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool ApplyOilToItem(Item &item, Player &player)
|
|
{
|
|
int r;
|
|
|
|
if (item._iClass == ICLASS_MISC) {
|
|
return false;
|
|
}
|
|
if (item._iClass == ICLASS_GOLD) {
|
|
return false;
|
|
}
|
|
if (item._iClass == ICLASS_QUEST) {
|
|
return false;
|
|
}
|
|
|
|
switch (player._pOilType) {
|
|
case IMISC_OILACC:
|
|
case IMISC_OILMAST:
|
|
case IMISC_OILSHARP:
|
|
if (item._iClass == ICLASS_ARMOR) {
|
|
return false;
|
|
}
|
|
break;
|
|
case IMISC_OILDEATH:
|
|
if (item._iClass == ICLASS_ARMOR) {
|
|
return false;
|
|
}
|
|
if (item._itype == ItemType::Bow) {
|
|
return false;
|
|
}
|
|
break;
|
|
case IMISC_OILHARD:
|
|
case IMISC_OILIMP:
|
|
if (item._iClass == ICLASS_WEAPON) {
|
|
return false;
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
switch (player._pOilType) {
|
|
case IMISC_OILACC:
|
|
if (item._iPLToHit < 50) {
|
|
item._iPLToHit += RandomIntBetween(1, 2);
|
|
}
|
|
break;
|
|
case IMISC_OILMAST:
|
|
if (item._iPLToHit < 100) {
|
|
item._iPLToHit += RandomIntBetween(3, 5);
|
|
}
|
|
break;
|
|
case IMISC_OILSHARP:
|
|
if (item._iMaxDam - item._iMinDam < 30 && item._iMaxDam < 255) {
|
|
item._iMaxDam = item._iMaxDam + 1;
|
|
}
|
|
break;
|
|
case IMISC_OILDEATH:
|
|
if (item._iMaxDam - item._iMinDam < 30 && item._iMaxDam < 254) {
|
|
item._iMinDam = item._iMinDam + 1;
|
|
item._iMaxDam = item._iMaxDam + 2;
|
|
}
|
|
break;
|
|
case IMISC_OILSKILL:
|
|
r = RandomIntBetween(5, 10);
|
|
item._iMinStr = std::max(0, item._iMinStr - r);
|
|
item._iMinMag = std::max(0, item._iMinMag - r);
|
|
item._iMinDex = std::max(0, item._iMinDex - r);
|
|
break;
|
|
case IMISC_OILBSMTH:
|
|
if (item._iMaxDur == DUR_INDESTRUCTIBLE)
|
|
return true;
|
|
if (item._iDurability < item._iMaxDur) {
|
|
item._iDurability = (item._iMaxDur + 4) / 5 + item._iDurability;
|
|
item._iDurability = std::min<int>(item._iDurability, item._iMaxDur);
|
|
} else {
|
|
if (item._iMaxDur >= 100) {
|
|
return true;
|
|
}
|
|
item._iMaxDur++;
|
|
item._iDurability = item._iMaxDur;
|
|
}
|
|
break;
|
|
case IMISC_OILFORT:
|
|
if (item._iMaxDur != DUR_INDESTRUCTIBLE && item._iMaxDur < 200) {
|
|
r = RandomIntBetween(10, 50);
|
|
item._iMaxDur += r;
|
|
item._iDurability += r;
|
|
}
|
|
break;
|
|
case IMISC_OILPERM:
|
|
item._iDurability = DUR_INDESTRUCTIBLE;
|
|
item._iMaxDur = DUR_INDESTRUCTIBLE;
|
|
break;
|
|
case IMISC_OILHARD:
|
|
if (item._iAC < 60) {
|
|
item._iAC += RandomIntBetween(1, 2);
|
|
}
|
|
break;
|
|
case IMISC_OILIMP:
|
|
if (item._iAC < 120) {
|
|
item._iAC += RandomIntBetween(3, 5);
|
|
}
|
|
break;
|
|
default:
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void UpdateHellfireFlag(Item &item, const char *identifiedItemName)
|
|
{
|
|
// DevilutionX support vanilla and hellfire items in one save file and for that introduced CF_HELLFIRE
|
|
// But vanilla hellfire items don't have CF_HELLFIRE set in Item::dwBuff
|
|
// This functions tries to set this flag for vanilla hellfire items based on the item name
|
|
// This ensures that Item::getName() returns the correct translated item name
|
|
if ((item.dwBuff & CF_HELLFIRE) != 0U)
|
|
return; // Item is already a hellfire item
|
|
if (item._iMagical != ITEM_QUALITY_MAGIC)
|
|
return; // Only magic item's name can differ between diablo and hellfire
|
|
if (gbIsMultiplayer)
|
|
return; // Vanilla hellfire multiplayer is not supported in devilutionX, so there can't be items with missing dwBuff from there
|
|
// We need to test both short and long name, because StringInPanel can return a different result (other font and some bugfixes)
|
|
const std::string diabloItemNameShort = GetTranslatedItemNameMagical(item, false, false, false);
|
|
if (diabloItemNameShort == identifiedItemName)
|
|
return; // Diablo item name is identical => not a hellfire specific item
|
|
const std::string diabloItemNameLong = GetTranslatedItemNameMagical(item, false, false, true);
|
|
if (diabloItemNameLong == identifiedItemName)
|
|
return; // Diablo item name is identical => not a hellfire specific item
|
|
const std::string hellfireItemNameShort = GetTranslatedItemNameMagical(item, true, false, false);
|
|
const std::string hellfireItemNameLong = GetTranslatedItemNameMagical(item, true, false, true);
|
|
if (hellfireItemNameShort == identifiedItemName || hellfireItemNameLong == identifiedItemName) {
|
|
// This item should be a vanilla hellfire item that has CF_HELLFIRE missing, cause only then the item name matches
|
|
item.dwBuff |= CF_HELLFIRE;
|
|
}
|
|
}
|
|
|
|
} // namespace devilution
|