Browse Source

Merge branch 'master' into dev-merged

pull/6547/head
Gleb Mazovetskiy 3 years ago
parent
commit
af869ac732
  1. 6
      .bettercodehub.yml
  2. 4
      .github/workflows/Windows_MSVC_x64.yml
  3. 17
      Packaging/nix/devilutionx.metainfo.xml
  4. 28
      Source/automap.cpp
  5. 151
      Source/controls/plrctrls.cpp
  6. 8
      Source/engine/render/scrollrt.cpp
  7. 51
      Source/error.cpp
  8. 6
      Source/error.h
  9. 1
      Source/gamemenu.cpp
  10. 120
      Source/inv.cpp
  11. 7
      Source/inv.h
  12. 33
      Source/pack.cpp
  13. 64
      Source/qol/stash.cpp
  14. 46
      Translations/sv.po
  15. 60
      docs/CHANGELOG.md
  16. BIN
      test/fixtures/timedemo/WarriorLevel1to2/demo_0.dmo
  17. 1
      test/pack_test.cpp
  18. 2
      vcpkg.json

6
.bettercodehub.yml

@ -1,6 +0,0 @@
exclude:
- /Packaging/.*
- /3rdParty/.*
languages:
- cpp
component_depth: 2

4
.github/workflows/Windows_MSVC_x64.yml

@ -31,9 +31,9 @@ jobs:
uses: lukka/get-cmake@latest
- name: Restore or setup vcpkg
uses: lukka/run-vcpkg@v11
uses: lukka/run-vcpkg@v11.1
with:
vcpkgGitCommitId: '78b61582c9e093fda56a01ebb654be15a0033897'
vcpkgGitCommitId: '927bc12e31148b0d44ae9d174b96c20e3bcf08eb'
- name: Fetch test data
run: |

17
Packaging/nix/devilutionx.metainfo.xml

@ -64,6 +64,23 @@
</screenshot>
</screenshots>
<releases>
<release version="1.5.1" date="2023-xx-xx">
<description>
<p>This is a primarily bugfix release, which includes the following updates:</p>
<ul>
<li>Resolve various gameplay and graphical issues</li>
<li>Revamped settings menu for better organization</li>
<li>Rectification of crashes identified in version 1.5.0</li>
<li>Reduced RAM usage for improved performance</li>
<li>Updates to PVP arenas</li>
<li>Increased reliability in multiplayer functionality</li>
<li>Improved translations</li>
<li>Fixed gameplay recording playback issues</li>
</ul>
<p>Please visit the full changelog for more detailed notes</p>
</description>
<url>https://github.com/diasurgical/devilutionX/releases/tag/1.5.1</url>
</release>
<release version="1.5.0" date="2023-06-13">
<description>
<p>This release includes a lot of features, such as:</p>

28
Source/automap.cpp

@ -116,16 +116,28 @@ void DrawDiamond(const Surface &out, Point center, uint8_t color)
void DrawMapVerticalDoor(const Surface &out, Point center, uint8_t colorBright, uint8_t colorDim)
{
DrawMapLineNE(out, { center.x + AmLine(8), center.y - AmLine(4) }, AmLine(4), colorDim);
DrawMapLineNE(out, { center.x - AmLine(16), center.y + AmLine(8) }, AmLine(4), colorDim);
DrawDiamond(out, center, colorBright);
if (leveltype != DTYPE_CATACOMBS) {
DrawMapLineNE(out, { center.x + AmLine(8), center.y - AmLine(4) }, AmLine(4), colorDim);
DrawMapLineNE(out, { center.x - AmLine(16), center.y + AmLine(8) }, AmLine(4), colorDim);
DrawDiamond(out, center, colorBright);
} else {
DrawMapLineNE(out, { center.x - AmLine(8), center.y + AmLine(4) }, AmLine(8), colorDim);
DrawMapLineNE(out, { center.x - AmLine(16), center.y + AmLine(8) }, AmLine(4), colorDim);
DrawDiamond(out, { center.x + AmLine(16), center.y - AmLine(8) }, colorBright);
}
}
void DrawMapHorizontalDoor(const Surface &out, Point center, uint8_t colorBright, uint8_t colorDim)
{
DrawMapLineSE(out, { center.x - AmLine(16), center.y - AmLine(8) }, AmLine(4), colorDim);
DrawMapLineSE(out, { center.x + AmLine(8), center.y + AmLine(4) }, AmLine(4), colorDim);
DrawDiamond(out, center, colorBright);
if (leveltype != DTYPE_CATACOMBS) {
DrawMapLineSE(out, { center.x - AmLine(16), center.y - AmLine(8) }, AmLine(4), colorDim);
DrawMapLineSE(out, { center.x + AmLine(8), center.y + AmLine(4) }, AmLine(4), colorDim);
DrawDiamond(out, center, colorBright);
} else {
DrawMapLineSE(out, { center.x - AmLine(8), center.y - AmLine(4) }, AmLine(8), colorDim);
DrawMapLineSE(out, { center.x + AmLine(8), center.y + AmLine(4) }, AmLine(4), colorDim);
DrawDiamond(out, { center.x - AmLine(16), center.y - AmLine(8) }, colorBright);
}
}
void DrawDirt(const Surface &out, Point center, uint8_t color)
@ -897,6 +909,7 @@ void DrawAutomap(const Surface &out)
Displacement myPlayerOffset = {};
if (myPlayer.isWalking())
myPlayerOffset = GetOffsetForWalking(myPlayer.AnimInfo, myPlayer._pdir, true);
myPlayerOffset += Displacement { -1, (leveltype != DTYPE_CAVES) ? TILE_HEIGHT - 1 : -1 };
int d = (AutoMapScale * 64) / 100;
int cells = 2 * (gnScreenWidth / 2 / d) + 1;
@ -958,6 +971,8 @@ void DrawAutomap(const Surface &out)
screen.y += AmLine(32);
}
if (leveltype == DTYPE_CAVES)
myPlayerOffset.deltaY += TILE_HEIGHT;
for (size_t playerId = 0; playerId < Players.size(); playerId++) {
Player &player = Players[playerId];
if (player.isOnActiveLevel() && player.plractive && !player._pLvlChanging && (&player == MyPlayer || player.friendlyMode)) {
@ -965,6 +980,7 @@ void DrawAutomap(const Surface &out)
}
}
myPlayerOffset.deltaY -= TILE_HEIGHT / 2;
if (AutoMapShowItems)
SearchAutomapItem(out, myPlayerOffset, 8, [](Point position) { return dItem[position.x][position.y] != 0; });
#ifdef _DEBUG

151
Source/controls/plrctrls.cpp

@ -655,10 +655,10 @@ Point InvGetEquipSlotCoordFromInvSlot(const inv_xy_slot slot)
Point GetSlotCoord(int slot)
{
if (slot >= SLOTXY_BELT_FIRST && slot <= SLOTXY_BELT_LAST) {
return GetPanelPosition(UiPanels::Main, InvRect[slot].position);
return GetPanelPosition(UiPanels::Main, InvRect[slot].Center());
}
return GetPanelPosition(UiPanels::Inventory, InvRect[slot].position);
return GetPanelPosition(UiPanels::Inventory, InvRect[slot].Center());
}
/**
@ -734,25 +734,12 @@ void ResetInvCursorPosition()
} else {
mousePos = GetSlotCoord(Slot);
}
if (!MyPlayer->HoldItem.isEmpty()) {
mousePos += Displacement { -INV_SLOT_HALF_SIZE_PX, -INV_SLOT_HALF_SIZE_PX };
}
} else if (Slot >= SLOTXY_BELT_FIRST && Slot <= SLOTXY_BELT_LAST) {
mousePos = GetSlotCoord(Slot);
if (!MyPlayer->HoldItem.isEmpty())
mousePos += Displacement { -INV_SLOT_HALF_SIZE_PX, -INV_SLOT_HALF_SIZE_PX };
} else {
mousePos = InvGetEquipSlotCoordFromInvSlot((inv_xy_slot)Slot);
if (!MyPlayer->HoldItem.isEmpty()) {
Size itemSize = GetInventorySize(MyPlayer->HoldItem);
mousePos += Displacement { -INV_SLOT_HALF_SIZE_PX, -INV_SLOT_HALF_SIZE_PX * itemSize.height };
}
}
mousePos.x += (InventorySlotSizeInPixels.width / 2);
mousePos.y -= (InventorySlotSizeInPixels.height / 2);
SetCursorPos(mousePos);
}
@ -760,7 +747,6 @@ int FindClosestInventorySlot(Point mousePos)
{
int shortestDistance = std::numeric_limits<int>::max();
int bestSlot = 0;
mousePos += Displacement { -INV_SLOT_HALF_SIZE_PX, INV_SLOT_HALF_SIZE_PX };
for (int i = 0; i < NUM_XY_SLOTS; i++) {
int distance = mousePos.ManhattanDistance(GetSlotCoord(i));
@ -777,7 +763,6 @@ Point FindClosestStashSlot(Point mousePos)
{
int shortestDistance = std::numeric_limits<int>::max();
Point bestSlot = {};
mousePos += Displacement { -INV_SLOT_HALF_SIZE_PX, -INV_SLOT_HALF_SIZE_PX };
for (Point point : PointsInRectangle(Rectangle { { 0, 0 }, Size { 10, 10 } })) {
int distance = mousePos.ManhattanDistance(GetStashSlotCoord(point));
@ -820,7 +805,7 @@ void InventoryMove(AxisDirection dir)
const Item &heldItem = MyPlayer->HoldItem;
const bool isHoldingItem = !heldItem.isEmpty();
Size itemSize = GetInventorySize(heldItem);
Size itemSize = isHoldingItem ? GetInventorySize(heldItem) : Size { 1 };
// when item is on cursor (pcurs > 1), this is the real cursor XY
if (dir.x == AxisDirectionX_LEFT) {
@ -949,7 +934,7 @@ void InventoryMove(AxisDirection dir)
if (Slot == SLOTXY_HEAD || Slot == SLOTXY_CHEST) {
Slot = SLOTXY_INV_ROW1_FIRST + 4;
} else if (Slot == SLOTXY_RING_LEFT || Slot == SLOTXY_HAND_LEFT) {
Slot = SLOTXY_INV_ROW1_FIRST + 1;
Slot = SLOTXY_INV_ROW1_FIRST + (itemSize.width > 1 ? 0 : 1);
} else if (Slot == SLOTXY_RING_RIGHT || Slot == SLOTXY_HAND_RIGHT || Slot == SLOTXY_AMULET) {
Slot = SLOTXY_INV_ROW1_LAST - 1;
} else if (Slot <= (SLOTXY_INV_ROW4_LAST - (itemSize.height * INV_ROW_SLOT_SIZE))) {
@ -1007,28 +992,27 @@ void InventoryMove(AxisDirection dir)
} else {
mousePos = GetSlotCoord(Slot);
}
// move cursor to the center of the slot if not holding anything or top left is holding an object
if (isHoldingItem) {
if (Slot < SLOTXY_INV_FIRST) {
// The coordinates we get for body slots are based on the centre of the region relative to the hand cursor
// Need to adjust the position for items larger than 1x1 so they're aligned as expected
mousePos.x -= itemSize.width * INV_SLOT_HALF_SIZE_PX;
mousePos.y -= itemSize.height * INV_SLOT_HALF_SIZE_PX;
}
} else {
// get item under new slot if navigating on the inventory
if (Slot >= SLOTXY_INV_FIRST && Slot <= SLOTXY_BELT_LAST) {
// If we're in the inventory we may need to move the cursor to an area that doesn't line up with the center of a cell
if (Slot >= SLOTXY_INV_FIRST && Slot <= SLOTXY_INV_LAST) {
if (!isHoldingItem) {
// If we're not holding an item
int8_t itemInvId = GetItemIdOnSlot(Slot);
int itemSlot = FindFirstSlotOnItem(itemInvId);
if (itemSlot < 0)
itemSlot = Slot;
// offset the cursor so it shows over the center of the item
mousePos = GetSlotCoord(itemSlot);
itemSize = GetItemSizeOnSlot(itemSlot);
mousePos.x += (itemSize.width * InventorySlotSizeInPixels.width) / 2;
mousePos.y += (itemSize.height * InventorySlotSizeInPixels.height) / 2;
if (itemInvId != 0) {
// but the cursor moved over an item
int itemSlot = FindFirstSlotOnItem(itemInvId);
if (itemSlot < 0)
itemSlot = Slot;
// then we need to offset the cursor so it shows over the center of the item
mousePos = GetSlotCoord(itemSlot);
itemSize = GetItemSizeOnSlot(itemSlot);
}
}
// At this point itemSize is either the size of the cell/item the hand cursor is over, or the size of the item we're currently holding.
// mousePos is the center of the top left cell of the item under the hand cursor, or the top left cell of the region that could fit the item we're holding.
// either way we need to offset the mouse position to account for items (we're holding or hovering over) with a dimension larger than a single cell.
mousePos.x += ((itemSize.width - 1) * InventorySlotSizeInPixels.width) / 2;
mousePos.y += ((itemSize.height - 1) * InventorySlotSizeInPixels.height) / 2;
}
if (mousePos == MousePosition) {
@ -1184,9 +1168,9 @@ void StashMove(AxisDirection dir)
if (ActiveStashSlot != InvalidStashPoint) {
Point mousePos = GetStashSlotCoord(ActiveStashSlot);
if (pcurs == CURSOR_HAND) {
mousePos += Displacement { INV_SLOT_HALF_SIZE_PX, INV_SLOT_HALF_SIZE_PX };
}
// Stash coordinates are all the top left of the cell, so we need to shift the mouse to the center of the held item
// or the center of the cell if we have a hand cursor (itemSize will be 1x1 here so we can use the same calculation)
mousePos += Displacement { itemSize.width * INV_SLOT_HALF_SIZE_PX, itemSize.height * INV_SLOT_HALF_SIZE_PX };
SetCursorPos(mousePos);
return;
}
@ -1827,46 +1811,49 @@ void PerformPrimaryAction()
} else if (GetRightPanel().contains(MousePosition) || GetMainPanel().contains(MousePosition)) {
int inventorySlot = (Slot >= 0) ? Slot : FindClosestInventorySlot(MousePosition);
const Size cursorSizeInCells = MyPlayer->HoldItem.isEmpty() ? Size { 1, 1 } : GetInventorySize(MyPlayer->HoldItem);
// Find any item occupying a slot that is currently under the cursor
int8_t itemUnderCursor = [](int inventorySlot, Size cursorSizeInCells) {
if (inventorySlot < SLOTXY_INV_FIRST || inventorySlot > SLOTXY_INV_LAST)
return 0;
for (int x = 0; x < cursorSizeInCells.width; x++) {
for (int y = 0; y < cursorSizeInCells.height; y++) {
int slotUnderCursor = inventorySlot + x + y * INV_ROW_SLOT_SIZE;
if (slotUnderCursor > SLOTXY_INV_LAST)
continue;
int itemId = GetItemIdOnSlot(slotUnderCursor);
if (itemId != 0)
return itemId;
int jumpSlot = inventorySlot; // If the cursor is over an inventory slot we may need to adjust it due to pasting items of different sizes over each other
if (inventorySlot >= SLOTXY_INV_FIRST && inventorySlot <= SLOTXY_INV_LAST) {
const Size cursorSizeInCells = MyPlayer->HoldItem.isEmpty() ? Size { 1, 1 } : GetInventorySize(MyPlayer->HoldItem);
// Find any item occupying a slot that is currently under the cursor
int8_t itemUnderCursor = [](int inventorySlot, Size cursorSizeInCells) {
if (inventorySlot < SLOTXY_INV_FIRST || inventorySlot > SLOTXY_INV_LAST)
return 0;
for (int x = 0; x < cursorSizeInCells.width; x++) {
for (int y = 0; y < cursorSizeInCells.height; y++) {
int slotUnderCursor = inventorySlot + x + y * INV_ROW_SLOT_SIZE;
if (slotUnderCursor > SLOTXY_INV_LAST)
continue;
int itemId = GetItemIdOnSlot(slotUnderCursor);
if (itemId != 0)
return itemId;
}
}
}
return 0;
}(inventorySlot, cursorSizeInCells);
return 0;
}(inventorySlot, cursorSizeInCells);
// The cursor will need to be shifted to
// this slot if the item is swapped or lifted
int jumpSlot = FindFirstSlotOnItem(itemUnderCursor);
// Capture the first slot of the first item (if any) under the cursor
if (itemUnderCursor > 0)
jumpSlot = FindFirstSlotOnItem(itemUnderCursor);
}
CheckInvItem();
// If we don't find the item in the same position as before,
// it suggests that the item was swapped or lifted
int newSlot = FindFirstSlotOnItem(itemUnderCursor);
if (jumpSlot >= 0 && jumpSlot != newSlot) {
if (inventorySlot >= SLOTXY_INV_FIRST && inventorySlot <= SLOTXY_INV_LAST) {
Point mousePos = GetSlotCoord(jumpSlot);
Slot = jumpSlot;
const Size newCursorSizeInCells = MyPlayer->HoldItem.isEmpty() ? GetItemSizeOnSlot(jumpSlot) : GetInventorySize(MyPlayer->HoldItem);
mousePos.x += ((newCursorSizeInCells.width - 1) * InventorySlotSizeInPixels.width) / 2;
mousePos.y += ((newCursorSizeInCells.height - 1) * InventorySlotSizeInPixels.height) / 2;
SetCursorPos(mousePos);
}
} else if (IsStashOpen && GetLeftPanel().contains(MousePosition)) {
Point stashSlot = (ActiveStashSlot != InvalidStashPoint) ? ActiveStashSlot : FindClosestStashSlot(MousePosition);
const Size cursorSizeInCells = MyPlayer->HoldItem.isEmpty() ? Size { 1, 1 } : GetInventorySize(MyPlayer->HoldItem);
Size cursorSizeInCells = MyPlayer->HoldItem.isEmpty() ? Size { 1, 1 } : GetInventorySize(MyPlayer->HoldItem);
// Find any item occupying a slot that is currently under the cursor
StashStruct::StashCell itemUnderCursor = [](Point stashSlot, Size cursorSizeInCells) -> StashStruct::StashCell {
if (stashSlot != InvalidStashPoint)
if (stashSlot == InvalidStashPoint)
return StashStruct::EmptyCell;
for (Point slotUnderCursor : PointsInRectangle(Rectangle { stashSlot, cursorSizeInCells })) {
if (slotUnderCursor.x >= 10 || slotUnderCursor.y >= 10)
@ -1878,20 +1865,26 @@ void PerformPrimaryAction()
return StashStruct::EmptyCell;
}(stashSlot, cursorSizeInCells);
// The cursor will need to be shifted to
// this slot if the item is swapped or lifted
Point jumpSlot = FindFirstStashSlotOnItem(itemUnderCursor);
Point jumpSlot = itemUnderCursor == StashStruct::EmptyCell ? stashSlot : FindFirstStashSlotOnItem(itemUnderCursor);
CheckStashItem(MousePosition);
// If we don't find the item in the same position as before,
// it suggests that the item was swapped or lifted
Point newSlot = FindFirstStashSlotOnItem(itemUnderCursor);
if (jumpSlot != InvalidStashPoint && jumpSlot != newSlot) {
Point mousePos = GetStashSlotCoord(jumpSlot);
mousePos.y -= InventorySlotSizeInPixels.height;
ActiveStashSlot = jumpSlot;
SetCursorPos(mousePos);
Point mousePos = GetStashSlotCoord(jumpSlot);
ActiveStashSlot = jumpSlot;
if (MyPlayer->HoldItem.isEmpty()) {
// For inventory cut/paste we can combine the cases where we swap or simply paste items. Because stash movement is always cell based (there's no fast
// movement over large items) it looks better if we offset the hand cursor to the bottom right cell of the item we just placed.
ActiveStashSlot += Displacement { cursorSizeInCells - 1 }; // shift the active stash slot coordinates to account for items larger than 1x1
// Then we displace the mouse position to the bottom right corner of the item, then shift it back half a cell to center it.
// Could also be written as (cursorSize - 1) * InventorySlotSize + HalfInventorySlotSize, same thing in the end.
mousePos += Displacement { cursorSizeInCells } * Displacement { InventorySlotSizeInPixels } - Displacement { InventorySlotSizeInPixels } / 2;
} else {
// If we've picked up an item then use the same logic as the inventory so that the cursor is offset to the center of where the old item location was
// (in this case jumpSlot was the top left cell of where it used to be in the grid, and we need to update the cursor size since we're now holding the item)
cursorSizeInCells = GetInventorySize(MyPlayer->HoldItem);
mousePos.x += ((cursorSizeInCells.width) * InventorySlotSizeInPixels.width) / 2;
mousePos.y += ((cursorSizeInCells.height) * InventorySlotSizeInPixels.height) / 2;
}
SetCursorPos(mousePos);
}
return;
}

8
Source/engine/render/scrollrt.cpp

@ -240,13 +240,15 @@ void DrawCursor(const Surface &out)
// Copy the buffer before the item cursor and its 1px outline are drawn to a temporary buffer.
const int outlineWidth = !MyPlayer->HoldItem.isEmpty() ? 1 : 0;
Displacement offset = !MyPlayer->HoldItem.isEmpty() ? Displacement { cursSize / 2 } : Displacement { 0 };
Point cursPosition = MousePosition - offset;
Rectangle &rect = cursor.rect;
rect.position.x = MousePosition.x - outlineWidth;
rect.position.x = cursPosition.x - outlineWidth;
rect.size.width = cursSize.width + 2 * outlineWidth;
Clip(rect.position.x, rect.size.width, out.w());
rect.position.y = MousePosition.y - outlineWidth;
rect.position.y = cursPosition.y - outlineWidth;
rect.size.height = cursSize.height + 2 * outlineWidth;
Clip(rect.position.y, rect.size.height, out.h());
@ -254,7 +256,7 @@ void DrawCursor(const Surface &out)
return;
BlitCursor(cursor.behindBuffer, rect.size.width, &out[rect.position], out.pitch(), rect.size.width, rect.size.height);
DrawSoftwareCursor(out, MousePosition + Displacement { 0, cursSize.height - 1 }, pcurs);
DrawSoftwareCursor(out, cursPosition + Displacement { 0, cursSize.height - 1 }, pcurs);
}
/**

51
Source/error.cpp

@ -22,19 +22,23 @@ namespace devilution {
namespace {
std::deque<std::string> DiabloMessages;
struct MessageEntry {
std::string text;
uint32_t duration; // Duration in milliseconds
};
std::deque<MessageEntry> DiabloMessages;
uint32_t msgStartTime = 0;
std::vector<std::string> TextLines;
uint32_t msgdelay;
int ErrorWindowHeight = 54;
const int LineHeight = 12;
const int LineWidth = 418;
void InitNextLines()
{
msgdelay = GetMillisecondsSinceStartup();
TextLines.clear();
const std::string paragraphs = WordWrapString(DiabloMessages.front(), LineWidth, GameFont12, 1);
const std::string paragraphs = WordWrapString(DiabloMessages.front().text, LineWidth, GameFont12, 1);
size_t previous = 0;
while (true) {
@ -53,7 +57,7 @@ void InitNextLines()
/** Maps from error_id to error message. */
const char *const MsgStrings[] = {
"",
N_("No automap available in town"),
N_("Game saved"),
N_("No multiplayer functions in demo"),
N_("Direct Sound Creation Failed"),
N_("Not available in shareware version"),
@ -109,22 +113,25 @@ const char *const MsgStrings[] = {
N_(/* TRANSLATORS: Shrine Text. Keep atmospheric. :) */ "That which can break will."),
};
void InitDiabloMsg(diablo_message e)
void InitDiabloMsg(diablo_message e, uint32_t duration /*= 3500*/)
{
InitDiabloMsg(LanguageTranslate(MsgStrings[e]));
InitDiabloMsg(LanguageTranslate(MsgStrings[e]), duration);
}
void InitDiabloMsg(std::string_view msg)
void InitDiabloMsg(std::string_view msg, uint32_t duration /*= 3500*/)
{
if (DiabloMessages.size() >= MAX_SEND_STR_LEN)
return;
if (c_find(DiabloMessages, msg) != DiabloMessages.end())
if (c_find_if(DiabloMessages, [&msg](const MessageEntry &entry) { return entry.text == msg; })
!= DiabloMessages.end())
return;
DiabloMessages.push_back(std::string(msg));
if (DiabloMessages.size() == 1)
DiabloMessages.push_back({ std::string(msg), duration });
if (DiabloMessages.size() == 1) {
InitNextLines();
msgStartTime = SDL_GetTicks();
}
}
bool IsDiabloMsgAvailable()
@ -134,7 +141,13 @@ bool IsDiabloMsgAvailable()
void CancelCurrentDiabloMsg()
{
msgdelay = 0;
if (!DiabloMessages.empty()) {
DiabloMessages.pop_front();
if (!DiabloMessages.empty()) {
InitNextLines();
msgStartTime = SDL_GetTicks();
}
}
}
void ClrDiabloMsg()
@ -173,13 +186,17 @@ void DrawDiabloMsg(const Surface &out)
lineNumber += 1;
}
if (msgdelay > 0 && msgdelay <= GetMillisecondsSinceStartup() - 3500) {
msgdelay = 0;
}
if (msgdelay == 0) {
// Calculate the time the current message has been displayed
uint32_t currentTime = SDL_GetTicks();
uint32_t messageElapsedTime = currentTime - msgStartTime;
// Check if the current message's duration has passed
if (!DiabloMessages.empty() && messageElapsedTime >= DiabloMessages.front().duration) {
DiabloMessages.pop_front();
if (!DiabloMessages.empty())
if (!DiabloMessages.empty()) {
InitNextLines();
msgStartTime = currentTime;
}
}
}

6
Source/error.h

@ -15,7 +15,7 @@ namespace devilution {
enum diablo_message : uint8_t {
EMSG_NONE,
EMSG_NO_AUTOMAP_IN_TOWN,
EMSG_GAME_SAVED,
EMSG_NO_MULTIPLAYER_IN_DEMO,
EMSG_DIRECT_SOUND_FAILED,
EMSG_NOT_IN_SHAREWARE,
@ -71,8 +71,8 @@ enum diablo_message : uint8_t {
EMSG_SHRINE_MURPHYS,
};
void InitDiabloMsg(diablo_message e);
void InitDiabloMsg(std::string_view msg);
void InitDiabloMsg(diablo_message e, uint32_t duration = 3500);
void InitDiabloMsg(std::string_view msg, uint32_t duration = 3500);
bool IsDiabloMsgAvailable();
void CancelCurrentDiabloMsg();
void ClrDiabloMsg();

1
Source/gamemenu.cpp

@ -332,6 +332,7 @@ void gamemenu_save_game(bool /*bActivate*/)
DrawAndBlit();
SaveGame();
ClrDiabloMsg();
InitDiabloMsg(EMSG_GAME_SAVED, 1000);
RedrawEverything();
NewCursor(CURSOR_HAND);
if (CornerStone.activated) {

120
Source/inv.cpp

@ -45,21 +45,21 @@ bool invflag;
* arranged as follows:
*
* @code{.unparsed}
* 00 01
* 02 03 06
* 00 00
* 00 00 03
*
* 07 08 19 20 13 14
* 09 10 21 22 15 16
* 11 12 23 24 17 18
* 04 04 06 06 05 05
* 04 04 06 06 05 05
* 04 04 06 06 05 05
*
* 04 05
* 01 02
*
* 25 26 27 28 29 30 31 32 33 34
* 35 36 37 38 39 40 41 42 43 44
* 45 46 47 48 49 50 51 52 53 54
* 55 56 57 58 59 60 61 62 63 64
* 07 08 09 10 11 12 13 14 15 16
* 17 18 19 20 21 22 23 24 25 26
* 27 28 29 30 31 32 33 34 35 36
* 37 38 39 40 41 42 43 44 45 46
*
* 65 66 67 68 69 70 71 72
* 47 48 49 50 51 52 53 54
* @endcode
*/
const Rectangle InvRect[] = {
@ -284,36 +284,43 @@ bool AutoEquip(Player &player, const Item &item, inv_body_loc bodyLocation, bool
return true;
}
int FindSlotUnderCursor(Point cursorPosition, Size itemSize)
int FindTargetSlotUnderItemCursor(Point cursorPosition, Size itemSize)
{
int i = cursorPosition.x;
int j = cursorPosition.y;
if (!IsHardwareCursor()) {
// offset the cursor position to match the hot pixel we'd use for a hardware cursor
i += itemSize.width * INV_SLOT_HALF_SIZE_PX;
j += itemSize.height * INV_SLOT_HALF_SIZE_PX;
Displacement panelOffset = Point { 0, 0 } - GetRightPanel().position;
for (int r = SLOTXY_EQUIPPED_FIRST; r <= SLOTXY_EQUIPPED_LAST; r++) {
if (InvRect[r].contains(cursorPosition + panelOffset))
return r;
}
for (int r = 0; r < NUM_XY_SLOTS; r++) {
int xo = GetRightPanel().position.x;
int yo = GetRightPanel().position.y;
if (r >= SLOTXY_BELT_FIRST) {
xo = GetMainPanel().position.x;
yo = GetMainPanel().position.y;
for (int r = SLOTXY_INV_FIRST; r <= SLOTXY_INV_LAST; r++) {
if (InvRect[r].contains(cursorPosition + panelOffset)) {
// When trying to paste into the inventory we need to determine the top left cell of the nearest area that could fit the item, not the slot under the center/hot pixel.
if (itemSize.height <= 1 && itemSize.width <= 1) {
// top left cell of a 1x1 item is the same cell as the hot pixel, no work to do
return r;
}
// Otherwise work out how far the central cell is from the top-left cell
Displacement hotPixelCellOffset = { (itemSize.width - 1) / 2, (itemSize.height - 1) / 2 };
// For even dimension items we need to work out if the cursor is in the left/right (or top/bottom) half of the central cell and adjust the offset so the item lands in the area most covered by the cursor.
if (itemSize.width % 2 == 0 && InvRect[r].contains(cursorPosition + panelOffset + Displacement { INV_SLOT_HALF_SIZE_PX, 0 })) {
// hot pixel was in the left half of the cell, so we want to increase the offset to preference the column to the left
hotPixelCellOffset.deltaX++;
}
if (itemSize.height % 2 == 0 && InvRect[r].contains(cursorPosition + panelOffset + Displacement { 0, INV_SLOT_HALF_SIZE_PX })) {
// hot pixel was in the top half of the cell, so we want to increase the offset to preference the row above
hotPixelCellOffset.deltaY++;
}
// Then work out the top left cell of the nearest area that could fit this item (as pasting on the edge of the inventory would otherwise put it out of bounds)
int hotPixelCell = r - SLOTXY_INV_FIRST;
int targetRow = std::clamp((hotPixelCell / InventorySizeInSlots.width) - hotPixelCellOffset.deltaY, 0, InventorySizeInSlots.height - itemSize.height);
int targetColumn = std::clamp((hotPixelCell % InventorySizeInSlots.width) - hotPixelCellOffset.deltaX, 0, InventorySizeInSlots.width - itemSize.width);
return SLOTXY_INV_FIRST + targetRow * InventorySizeInSlots.width + targetColumn;
}
}
if (r == SLOTXY_INV_FIRST) {
if (itemSize.width % 2 == 0)
i -= INV_SLOT_HALF_SIZE_PX;
if (itemSize.height % 2 == 0)
j -= INV_SLOT_HALF_SIZE_PX;
}
if (InvRect[r].contains(i - xo, j - yo)) {
panelOffset = Point { 0, 0 } - GetMainPanel().position;
for (int r = SLOTXY_BELT_FIRST; r <= SLOTXY_BELT_LAST; r++) {
if (InvRect[r].contains(cursorPosition + panelOffset))
return r;
}
if (r == SLOTXY_INV_LAST && itemSize.height % 2 == 0)
j += INV_SLOT_HALF_SIZE_PX;
}
return NUM_XY_SLOTS;
}
@ -322,7 +329,7 @@ void CheckInvPaste(Player &player, Point cursorPosition)
{
Size itemSize = GetInventorySize(player.HoldItem);
int slot = FindSlotUnderCursor(cursorPosition, itemSize);
int slot = FindTargetSlotUnderItemCursor(cursorPosition, itemSize);
if (slot == NUM_XY_SLOTS)
return;
@ -359,26 +366,25 @@ void CheckInvPaste(Player &player, Point cursorPosition)
}
}
} else {
int yy = std::max(INV_ROW_SLOT_SIZE * ((ii / INV_ROW_SLOT_SIZE) - ((itemSize.height - 1) / 2)), 0);
for (int j = 0; j < itemSize.height; j++) {
if (yy >= InventoryGridCells)
return;
int xx = std::max((ii % INV_ROW_SLOT_SIZE) - ((itemSize.width - 1) / 2), 0);
for (int i = 0; i < itemSize.width; i++) {
if (xx >= INV_ROW_SLOT_SIZE)
return;
if (player.InvGrid[xx + yy] != 0) {
int8_t iv = std::abs(player.InvGrid[xx + yy]);
// check that the item we're pasting only overlaps one other item (or is going into empty space)
unsigned originCell = static_cast<unsigned>(slot - SLOTXY_INV_FIRST);
for (unsigned rowOffset = 0; rowOffset < static_cast<unsigned>(itemSize.height * InventorySizeInSlots.width); rowOffset += InventorySizeInSlots.width) {
for (unsigned columnOffset = 0; columnOffset < static_cast<unsigned>(itemSize.width); columnOffset++) {
unsigned testCell = originCell + rowOffset + columnOffset;
// FindTargetSlotUnderItemCursor returns the top left slot of the inventory region that fits the item, we can be confident this calculation is not going to read out of range.
assert(testCell < sizeof(player.InvGrid));
if (player.InvGrid[testCell] != 0) {
int8_t iv = std::abs(player.InvGrid[testCell]);
if (it != 0) {
if (it != iv)
if (it != iv) {
// Found two different items that would be displaced by the held item, can't paste the item here.
return;
}
} else {
it = iv;
}
}
xx++;
}
yy += INV_ROW_SLOT_SIZE;
}
}
} else if (il == ILOC_BELT) {
@ -526,13 +532,8 @@ void CheckInvPaste(Player &player, Point cursorPosition)
itemIndex = 0;
}
}
int ii = slot - SLOTXY_INV_FIRST;
// Calculate top-left position of item for InvGrid and then add item to InvGrid
int xx = std::max(ii % INV_ROW_SLOT_SIZE - ((itemSize.width - 1) / 2), 0);
int yy = std::max(INV_ROW_SLOT_SIZE * (ii / INV_ROW_SLOT_SIZE - ((itemSize.height - 1) / 2)), 0);
AddItemToInvGrid(player, xx + yy, it, itemSize);
AddItemToInvGrid(player, slot - SLOTXY_INV_FIRST, it, itemSize);
}
break;
case ILOC_BELT: {
@ -555,8 +556,6 @@ void CheckInvPaste(Player &player, Point cursorPosition)
}
CalcPlrInv(player, true);
if (&player == MyPlayer) {
if (player.HoldItem.isEmpty() && !IsHardwareCursor())
SetCursorPos(MousePosition + Displacement { itemSize * INV_SLOT_HALF_SIZE_PX });
NewCursor(player.HoldItem);
}
}
@ -820,11 +819,6 @@ void CheckInvCut(Player &player, Point cursorPosition, bool automaticMove, bool
holdItem.clear();
} else {
NewCursor(holdItem);
if (!IsHardwareCursor() && !dropItem) {
// For a hardware cursor, we set the "hot point" to the center of the item instead.
Size cursSize = GetInvItemSize(holdItem._iCurs + CURSOR_FIRSTITEM);
SetCursorPos(cursorPosition - Displacement(cursSize / 2));
}
}
}
}

7
Source/inv.h

@ -18,7 +18,8 @@ namespace devilution {
#define INV_SLOT_SIZE_PX 28
#define INV_SLOT_HALF_SIZE_PX (INV_SLOT_SIZE_PX / 2)
#define INV_ROW_SLOT_SIZE 10
constexpr Size InventorySizeInSlots { 10, 4 };
#define INV_ROW_SLOT_SIZE InventorySizeInSlots.width
constexpr Size InventorySlotSizeInPixels { INV_SLOT_SIZE_PX };
enum inv_item : int8_t {
@ -44,12 +45,14 @@ enum inv_item : int8_t {
enum inv_xy_slot : uint8_t {
// clang-format off
SLOTXY_HEAD = 0,
SLOTXY_EQUIPPED_FIRST = SLOTXY_HEAD,
SLOTXY_RING_LEFT = 1,
SLOTXY_RING_RIGHT = 2,
SLOTXY_AMULET = 3,
SLOTXY_HAND_LEFT = 4,
SLOTXY_HAND_RIGHT = 5,
SLOTXY_CHEST = 6,
SLOTXY_EQUIPPED_LAST = SLOTXY_CHEST,
// regular inventory
SLOTXY_INV_FIRST = 7,
@ -80,7 +83,7 @@ enum item_color : uint8_t {
};
extern bool invflag;
extern const Rectangle InvRect[73];
extern const Rectangle InvRect[NUM_XY_SLOTS];
void InvDrawSlotBack(const Surface &out, Point targetPosition, Size size, item_quality itemQuality);
/**

33
Source/pack.cpp

@ -556,6 +556,28 @@ bool UnPackNetPlayer(const PlayerNetPack &packed, Player &player)
for (int i = 0; i < NUM_INVLOC; i++) {
if (!UnPackNetItem(player, packed.InvBody[i], player.InvBody[i]))
return false;
if (player.InvBody[i].isEmpty())
continue;
auto loc = static_cast<int8_t>(player.GetItemLocation(player.InvBody[i]));
switch (i) {
case INVLOC_HEAD:
ValidateField(loc, loc == ILOC_HELM);
break;
case INVLOC_RING_LEFT:
case INVLOC_RING_RIGHT:
ValidateField(loc, loc == ILOC_RING);
break;
case INVLOC_AMULET:
ValidateField(loc, loc == ILOC_AMULET);
break;
case INVLOC_HAND_LEFT:
case INVLOC_HAND_RIGHT:
ValidateField(loc, IsAnyOf(loc, ILOC_ONEHAND, ILOC_TWOHAND));
break;
case INVLOC_CHEST:
ValidateField(loc, loc == ILOC_ARMOR);
break;
}
}
player._pNumInv = packed._pNumInv;
@ -568,8 +590,17 @@ bool UnPackNetPlayer(const PlayerNetPack &packed, Player &player)
player.InvGrid[i] = packed.InvGrid[i];
for (int i = 0; i < MaxBeltItems; i++) {
if (!UnPackNetItem(player, packed.SpdList[i], player.SpdList[i]))
Item &item = player.SpdList[i];
if (!UnPackNetItem(player, packed.SpdList[i], item))
return false;
if (item.isEmpty())
continue;
Size beltItemSize = GetInventorySize(item);
int8_t beltItemType = static_cast<int8_t>(item._itype);
bool beltItemUsable = item.isUsable();
ValidateFields(beltItemSize.width, beltItemSize.height, (beltItemSize == Size { 1, 1 }));
ValidateField(beltItemType, item._itype != ItemType::Gold);
ValidateField(beltItemUsable, beltItemUsable);
}
CalcPlrInv(player, false);

64
Source/qol/stash.cpp

@ -50,7 +50,8 @@ constexpr Rectangle StashButtonRect[] = {
// clang-format on
};
constexpr PointsInRectangle<int> StashGridRange { { { 0, 0 }, Size { 10, 10 } } };
constexpr Size StashGridSize { 10, 10 };
constexpr PointsInRectangle<int> StashGridRange { { { 0, 0 }, StashGridSize } };
OptionalOwnedClxSpriteList StashPanelArt;
OptionalOwnedClxSpriteList StashNavButtonArt;
@ -68,7 +69,7 @@ void AddItemToStashGrid(unsigned page, Point position, uint16_t stashListIndex,
}
}
Point FindSlotUnderCursor(Point cursorPosition)
std::optional<Point> FindTargetSlotUnderItemCursor(Point cursorPosition, Size itemSize)
{
for (auto point : StashGridRange) {
Rectangle cell {
@ -77,11 +78,30 @@ Point FindSlotUnderCursor(Point cursorPosition)
};
if (cell.contains(cursorPosition)) {
// When trying to paste into the stash we need to determine the top left cell of the nearest area that could fit the item, not the slot under the center/hot pixel.
if (itemSize.height <= 1 && itemSize.width <= 1) {
// top left cell of a 1x1 item is the same cell as the hot pixel, no work to do
return point;
}
// Otherwise work out how far the central cell is from the top-left cell
Displacement hotPixelCellOffset = { (itemSize.width - 1) / 2, (itemSize.height - 1) / 2 };
// For even dimension items we need to work out if the cursor is in the left/right (or top/bottom) half of the central cell and adjust the offset so the item lands in the area most covered by the cursor.
if (itemSize.width % 2 == 0 && cell.contains(cursorPosition + Displacement { INV_SLOT_HALF_SIZE_PX, 0 })) {
// hot pixel was in the left half of the cell, so we want to increase the offset to preference the column to the left
hotPixelCellOffset.deltaX++;
}
if (itemSize.height % 2 == 0 && cell.contains(cursorPosition + Displacement { 0, INV_SLOT_HALF_SIZE_PX })) {
// hot pixel was in the top half of the cell, so we want to increase the offset to preference the row above
hotPixelCellOffset.deltaY++;
}
// Then work out the top left cell of the nearest area that could fit this item (as pasting on the edge of the stash would otherwise put it out of bounds)
point.y = std::clamp(point.y - hotPixelCellOffset.deltaY, 0, StashGridSize.height - itemSize.height);
point.x = std::clamp(point.x - hotPixelCellOffset.deltaX, 0, StashGridSize.width - itemSize.width);
return point;
}
}
return InvalidStashPoint;
return {};
}
bool IsItemAllowedInStash(const Item &item)
@ -93,14 +113,6 @@ void CheckStashPaste(Point cursorPosition)
{
Player &player = *MyPlayer;
const Size itemSize = GetInventorySize(player.HoldItem);
const Displacement hotPixelOffset = Displacement(itemSize * INV_SLOT_HALF_SIZE_PX);
if (IsHardwareCursor()) {
// It's more natural to select the top left cell of the region the sprite is overlapping when putting an item
// into an inventory grid, so compensate for the adjusted hot pixel of hardware cursors.
cursorPosition -= hotPixelOffset;
}
if (!IsItemAllowedInStash(player.HoldItem))
return;
@ -111,23 +123,17 @@ void CheckStashPaste(Point cursorPosition)
player.HoldItem.clear();
PlaySFX(IS_GOLD);
Stash.dirty = true;
if (!IsHardwareCursor()) {
// To make software cursors behave like hardware cursors we need to adjust the hand cursor position manually
SetCursorPos(cursorPosition + hotPixelOffset);
}
NewCursor(CURSOR_HAND);
return;
}
// Make the hot pixel the center of the top-left cell of the item, this favors the cell which contains more of the
// item sprite
Point firstSlot = FindSlotUnderCursor(cursorPosition + Displacement(INV_SLOT_HALF_SIZE_PX));
if (firstSlot == InvalidStashPoint)
const Size itemSize = GetInventorySize(player.HoldItem);
std::optional<Point> targetSlot = FindTargetSlotUnderItemCursor(cursorPosition, itemSize);
if (!targetSlot)
return;
if (firstSlot.x + itemSize.width > 10 || firstSlot.y + itemSize.height > 10) {
return; // Item does not fit
}
Point firstSlot = *targetSlot;
// Check that no more than 1 item is replaced by the move
StashStruct::StashCell stashIndex = StashStruct::EmptyCell;
@ -144,6 +150,7 @@ void CheckStashPaste(Point cursorPosition)
PlaySFX(ItemInvSnds[ItemCAnimTbl[player.HoldItem._iCurs]]);
// Need to set the item anchor position to the bottom left so drawing code functions correctly.
player.HoldItem.position = firstSlot + Displacement { 0, itemSize.height - 1 };
if (stashIndex == StashStruct::EmptyCell) {
@ -151,8 +158,9 @@ void CheckStashPaste(Point cursorPosition)
// stashList will have at most 10 000 items, up to 65 535 are supported with uint16_t indexes
stashIndex = static_cast<uint16_t>(Stash.stashList.size() - 1);
} else {
// remove item from stash grid
// swap the held item and whatever was in the stash at this position
std::swap(Stash.stashList[stashIndex], player.HoldItem);
// then clear the space occupied by the old item
for (auto &row : Stash.GetCurrentGrid()) {
for (auto &itemId : row) {
if (itemId - 1 == stashIndex)
@ -161,14 +169,11 @@ void CheckStashPaste(Point cursorPosition)
}
}
// Finally mark the area now occupied by the pasted item in the current page/grid.
AddItemToStashGrid(Stash.GetPage(), firstSlot, stashIndex, itemSize);
Stash.dirty = true;
if (player.HoldItem.isEmpty() && !IsHardwareCursor()) {
// To make software cursors behave like hardware cursors we need to adjust the hand cursor position manually
SetCursorPos(cursorPosition + hotPixelOffset);
}
NewCursor(player.HoldItem);
}
@ -243,11 +248,6 @@ void CheckStashCut(Point cursorPosition, bool automaticMove)
holdItem.clear();
} else {
NewCursor(holdItem);
if (!IsHardwareCursor()) {
// For a hardware cursor, we set the "hot point" to the center of the item instead.
Size cursSize = GetInvItemSize(holdItem._iCurs + CURSOR_FIRSTITEM);
SetCursorPos(cursorPosition - Displacement(cursSize / 2));
}
}
}
}

46
Translations/sv.po

@ -1087,7 +1087,7 @@ msgstr ""
#: Source/cursor.cpp:220
msgid "Town Portal"
msgstr "Stadsportal"
msgstr "Byportal"
#: Source/cursor.cpp:221
msgid "from {:s}"
@ -1481,7 +1481,7 @@ msgstr "fel: läste 0 bytes från servern"
#: Source/error.cpp:55
msgid "No automap available in town"
msgstr "Ingen autokarta tillgänglig i staden"
msgstr "Ingen autokarta tillgänglig i byn"
#: Source/error.cpp:56
msgid "No multiplayer functions in demo"
@ -1501,7 +1501,7 @@ msgstr "Inte tillräckligt med utrymme för att spara"
#: Source/error.cpp:60
msgid "No Pause in town"
msgstr "Ingen Paus i staden"
msgstr "Ingen Paus i byn"
#: Source/error.cpp:61
msgid "Copying to a hard disk is recommended"
@ -1745,7 +1745,7 @@ msgstr "Avsluta Spel"
#: Source/gamemenu.cpp:51
msgid "Restart In Town"
msgstr "Starta Om i Staden"
msgstr "Starta Om i Byn"
#: Source/gamemenu.cpp:63
msgid "Gamma"
@ -2063,7 +2063,7 @@ msgstr "Köttyxa"
#: Source/itemdat.cpp:60 Source/itemdat.cpp:434
msgid "The Undead Crown"
msgstr "De Vandödas Krona"
msgstr "De Odödas Krona"
#: Source/itemdat.cpp:61 Source/itemdat.cpp:435
msgid "Empyrean Band"
@ -2143,7 +2143,7 @@ msgstr "Identifiera-Skriftrulle"
#: Source/itemdat.cpp:80 Source/itemdat.cpp:151
msgid "Scroll of Town Portal"
msgstr "Stadsportal-Skriftrulle"
msgstr "Byportal-Skriftrulle"
#: Source/itemdat.cpp:81 Source/itemdat.cpp:440
msgid "Arkaine's Valor"
@ -2483,7 +2483,7 @@ msgstr "Huggare"
#: Source/itemdat.cpp:175
msgid "Claymore"
msgstr "Höglandssvärd"
msgstr "Slagsvärd"
#: Source/itemdat.cpp:176
msgid "Blade"
@ -2560,7 +2560,7 @@ msgstr "Spikklubba"
#: Source/itemdat.cpp:194
msgid "Flail"
msgstr "Slaga"
msgstr "Stridsgissel"
#: Source/itemdat.cpp:195
msgid "Maul"
@ -4413,7 +4413,7 @@ msgstr "extra PK mot demoner"
#: Source/items.cpp:3754
msgid "extra AC vs undead"
msgstr "extra PK mot vandöda"
msgstr "extra PK mot ödöda"
#: Source/items.cpp:3756
msgid "50% Mana moved to Health"
@ -4788,7 +4788,7 @@ msgstr "Likdemon"
#: Source/monstdat.cpp:91
msgctxt "monster"
msgid "Undead Balrog"
msgstr "Vandöd Balrog"
msgstr "Odöd Balrog"
#: Source/monstdat.cpp:92
msgctxt "monster"
@ -5614,7 +5614,7 @@ msgstr "Demon"
#: Source/monster.cpp:3386
msgid "Undead"
msgstr "Vandöd"
msgstr "Odöd"
#: Source/monster.cpp:4648
msgid "Type: {:s} Kills: {:d}"
@ -6848,7 +6848,7 @@ msgstr "Magi"
#: Source/panels/spell_list.cpp:172
msgid "Damages undead only"
msgstr "Skadar endast vandöda"
msgstr "Skadar endast odöda"
#: Source/panels/spell_list.cpp:183
msgid "Scroll"
@ -7080,7 +7080,7 @@ msgstr "Eldmur"
#: Source/spelldat.cpp:22
msgctxt "spell"
msgid "Town Portal"
msgstr "Stadsportal"
msgstr "Byportal"
#: Source/spelldat.cpp:23
msgctxt "spell"
@ -7683,7 +7683,7 @@ msgstr ""
"\n"
"Här tar historien en ännu mörkare vändning än vad jag trodde var möjligt! "
"Vår forne kung har stigit upp från sin eviga sömn och styr nu en legion av "
"vandöda tjänare inne i Labyrinten. Hans kropp begravdes i en grift tre "
"odöda tjänare inne i Labyrinten. Hans kropp begravdes i en grift tre "
"våningar under Katedralen. Snälla, kära mästare, släpp hans själ fri genom "
"att förgöra hans förbannade nuvarande form..."
@ -7768,7 +7768,7 @@ msgid ""
"all who still live here."
msgstr ""
"De döda som går bland de levande följer den förbannade Kungen. Han har "
"makten att väcka upp ännu fler krigare för de vandödas evigt växande armé. "
"makten att väcka upp ännu fler krigare för de odödas evigt växande armé. "
"Om du inte stoppar hans styre kommer han säkerligen tåga över detta land och "
"slå ned alla som ännu bor här."
@ -7782,7 +7782,7 @@ msgid ""
msgstr ""
"Lyssna, jag har affärer att sköta här. Jag säljer inte information, och jag "
"bryr mig inte om någon Kung som varit död längre än jag levat. Men om du "
"behöver något att använda mot den här vandöda Kungen, så kanske jag kan "
"behöver något att använda mot den här odöda Kungen, så kanske jag kan "
"hjälpa dig..."
#. TRANSLATORS: Quest dialog spoken by The Skeleton King (Hostile)
@ -8142,7 +8142,7 @@ msgid ""
"Please, do what you can or I don't know what we will do."
msgstr ""
"Jag har alltid försökt hålla stora förråd av mat och dryck i vår källare, "
"men om hela staden saknar färskvatten kommer till och med våra förråd sina "
"men om hela byn saknar färskvatten kommer till och med våra förråd sina "
"snart.\n"
"\n"
"Snälla, gör vad du kan, annars vet jag inte vad vi ska ta oss till."
@ -10005,7 +10005,7 @@ msgid ""
"little skeletons!"
msgstr ""
"Om du är ute efter ett bra vapen så se här. Ta ett vanligt trubbigt vapen, "
"som en stridsklubba. Funkar utmärkt mot de där vandöda fasorna där nere, och "
"som en stridsklubba. Funkar utmärkt mot de där odöda fasorna där nere, och "
"det finns inget bättre för att spräcka smala små skelett!"
#. TRANSLATORS: Neutral dialog spoken by Griswold (Gossip)
@ -10030,7 +10030,7 @@ msgid ""
msgstr ""
"Titta på den här klingan, balansen. Ett svärd i rätt händer och mot rätt "
"fiende, är alla vapens mästare. Dess skarpa egg har inte mycket att skära "
"eller hugga igenom mot de vandöda, men mot levande fiender kan ett svärd på "
"eller hugga igenom mot de odöda, men mot levande fiender kan ett svärd på "
"riktigt skära deras kött!"
#. TRANSLATORS: Neutral dialog spoken by Griswold (Gossip)
@ -10945,7 +10945,7 @@ msgid ""
"the talk of an old sick woman, but anything seems possible these days."
msgstr ""
"Min mormor berättar ofta historier om mystiska krafter som bebor kyrkogården "
"utanför stade. Och du kanske vill höra en av dem. Hon sade att om man lämnar "
"utanför byn. Och du kanske vill höra en av dem. Hon sade att om man lämnar "
"en lämplig gåva i kyrkogården, går in i katedralen för att be för de döda, "
"och sedan kommer tillbaka, så skulle gåvan ändrats på något konstigt sätt. "
"Jag vet inte om det bara är en sjuk gammal kvinnas prat, men allt verkar "
@ -11463,7 +11463,7 @@ msgstr "Krögaren Ogden"
#: Source/towners.cpp:110
msgid "Wounded Townsman"
msgstr "Skadad Stadsbo"
msgstr "Skadad Bybo"
#: Source/towners.cpp:132
msgid "Adria the Witch"
@ -11499,7 +11499,7 @@ msgstr "Celia"
#: Source/towners.cpp:277
msgid "Slain Townsman"
msgstr "Dräpt Stadsbo"
msgstr "Dräpt Bybo"
#: Source/trigs.cpp:343
msgid "Down to dungeon"
@ -11533,7 +11533,7 @@ msgstr "Upp till nivå {:d}"
#: Source/trigs.cpp:416 Source/trigs.cpp:471 Source/trigs.cpp:523
#: Source/trigs.cpp:602 Source/trigs.cpp:619 Source/trigs.cpp:666
msgid "Up to town"
msgstr "Upp till staden"
msgstr "Upp till byn"
#: Source/trigs.cpp:427 Source/trigs.cpp:505 Source/trigs.cpp:558
#: Source/trigs.cpp:583 Source/trigs.cpp:648

60
docs/CHANGELOG.md

@ -15,41 +15,50 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Update the pvp arenas
- Rename "Loopback" to "Offline"
#### Stability / Performance / System
- Move hp/mana display and item graphics to gameplay options
- Validate properties when reloading items
- Demomode: Improve replay stability
- Update [Discord link](https://discord.gg/devilutionx)
- Display save game confirmation
- Reduce ram usage
#### Translations
- Update Simplified Chinese translation
- Update French translation
- Update German translation
- Update Greek translation
- Update Japanese translation
- Update Korean translation
- Update Portuguese translation
- Update Spanish translation
- Update Swedish translation
- Update Ukrainian translation
#### Platforms
- Android TV: Update banner to include app name
#### Stability / Performance / System
- Move hp/mana display and item graphics to gameplay options
- Validate properties when reloading items
- Demomode: Improve replay stability
- Update [Discord link](https://discord.gg/devilutionx)
- Reduce ram usage
### Bugfixes
#### Gameplay
- Being able to enter Lazarus' chamber before opening the portal
- Book requirements not updating
- Diablo: Incorrect level 4 layout when Magic Banner quest is active
- Halls of the Blind not being compleated by picking up the amulet
- Some monsters not walking
- Missiles not traveling the full distance at some angles
- Diablo: Incorrect level 4 layout when the Magic Banner quest is active
- Halls of the Blind not being completed by picking up the amulet
- Shareware: Bucklers not dropping
- Player animation stuttering
#### Multiplayer
- Potions dropped by Divine shrines not being synced
#### Platforms
- Linux: Add sdl-image dependency for deb package
- Linux: Add sdl-image dependency for the deb package
- Linux: Include discord dependency
- Xbox One: Missing assets
#### Graphics / Audio
@ -57,14 +66,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Incorrect outlines at the right edge of the screen
- NPC speech continuing after starting a new game
- Correct various font rendering issues
- Hide the hit indicator when only one player is in the game
- Issues with flashing lights
- Floating number still appearing after death
- Misaligned automap
#### Controls
- Inconsistencies with placing items in to the stash
- Gamepad: Being stuck in dialogs
- Gamepad: Unable to use some scrolls directly
#### Stability / Performance / System
- Unable to playback new demo files
- Various crashes
### Bugfixes for original Diablo bugs
@ -72,11 +87,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
#### Gameplay
- Durability overflowing when reloading items
- Teleporting onto an occupied tile
- Right-click during dialogs casts spells
#### Graphics / Audio
- Cursor jitter when interacting with the inventory
- Broken lava tiles
#### Controls
- Inconsistencies with placing items in to the inventory
### Bugfixes for original Hellfire bugs
#### Gameplay
- Warping onto a solid tile
## DevilutionX 1.5.0
### Features
@ -399,7 +427,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Improve Romanian localization
- Improve Russian localization ([optional dub](https://github.com/diasurgical/devilutionx-assets/releases/download/v2/ru.mpq) by Stream)
- Improve Spanish localization
#### Gameplay
- Added a stash at Gillian's house

BIN
test/fixtures/timedemo/WarriorLevel1to2/demo_0.dmo vendored

Binary file not shown.

1
test/pack_test.cpp

@ -871,6 +871,7 @@ public:
{
Players.resize(2);
MyPlayer = &Players[0];
gbIsMultiplayer = true;
PlayerPack testPack {
0, 0, -1, 9, 0, 2, 61, 24, 0, 0, "MP-Warrior", 0, 120, 25, 60, 60, 37, 0, 85670061, 3921, 13568, 13568, 3904, 3904,

2
vcpkg.json

@ -6,7 +6,7 @@
"bzip2",
"simpleini"
],
"builtin-baseline": "78b61582c9e093fda56a01ebb654be15a0033897",
"builtin-baseline": "927bc12e31148b0d44ae9d174b96c20e3bcf08eb",
"features": {
"sdl1": {
"description": "Use SDL1.2 instead of SDL2",

Loading…
Cancel
Save