You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

323 lines
10 KiB

#include "panels/partypanel.hpp"
#include <expected.hpp>
#include <optional>
#include "automap.h"
#include "control/control.hpp"
#include "engine/backbuffer_state.hpp"
#include "engine/clx_sprite.hpp"
#include "engine/load_cel.hpp"
#include "engine/load_clx.hpp"
#include "engine/palette.h"
#include "engine/rectangle.hpp"
#include "engine/render/clx_render.hpp"
#include "engine/render/primitive_render.hpp"
#include "engine/size.hpp"
#include "inv.h"
#include "options.h"
#include "pfile.h"
#include "playerdat.hpp"
#include "qol/monhealthbar.h"
#include "qol/stash.h"
#include "stores.h"
#include "utils/status_macros.hpp"
#include "utils/surface_to_clx.hpp"
namespace devilution {
namespace {
struct PartySpriteOffset {
Point inTownOffset;
Point inDungeonOffset;
Point isDeadOffset;
};
const PartySpriteOffset ClassSpriteOffsets[] = {
{ { -4, -18 }, { 6, -21 }, { -6, -50 } },
{ { -2, -18 }, { 1, -20 }, { -8, -35 } },
{ { -2, -16 }, { 3, -20 }, { 0, -50 } },
{ { -2, -19 }, { 1, -19 }, { 28, -60 } }
};
OptionalOwnedClxSpriteList PartyMemberFrame;
OptionalOwnedClxSpriteList PlayerTags;
Point PartyPanelPos = { 8, 8 };
Rectangle PortraitFrameRects[MAX_PLRS];
int RightClickedPortraitIndex = -1;
constexpr int HealthBarHeight = 7;
constexpr int ManaBarHeight = 7;
constexpr int FrameGap = 15;
constexpr int FrameBorderSize = 3;
constexpr int FrameSpriteSize = 12;
constexpr Size FrameSections = { 4, 4 }; // x/y can't be less than 2
constexpr Size PortraitFrameSize = { FrameSections.width * FrameSpriteSize, FrameSections.height *FrameSpriteSize };
constexpr uint8_t FrameBackgroundColor = PAL16_BLUE + 14;
void DrawBar(const Surface &out, Rectangle rect, uint8_t color)
{
for (int x = 0; x < rect.size.width; x++) {
DrawVerticalLine(out, { rect.position.x + x, rect.position.y }, rect.size.height, color);
}
}
void DrawMemberFrame(const Surface &out, OwnedClxSpriteList &frame, Point pos)
{
// Draw the frame background
FillRect(out, pos.x, pos.y, PortraitFrameSize.width, PortraitFrameSize.height, FrameBackgroundColor);
// Now draw the frame border
const Size adjustedFrame = { FrameSections.width - 1, FrameSections.height - 1 };
for (int x = 0; x <= adjustedFrame.width; x++) {
for (int y = 0; y <= adjustedFrame.height; y++) {
// Get what section of the frame we're drawing
int spriteIndex = -1;
if (x == 0 && y == 0)
spriteIndex = 0; // Top-left corner
else if (x == 0 && y == adjustedFrame.height)
spriteIndex = 1; // Bottom-left corner
else if (x == adjustedFrame.width && y == adjustedFrame.height)
spriteIndex = 2; // Bottom-right corner
else if (x == adjustedFrame.width && y == 0)
spriteIndex = 3; // Top-right corner
else if (y == 0)
spriteIndex = 4; // Top border
else if (x == 0)
spriteIndex = 5; // Left border
else if (y == adjustedFrame.height)
spriteIndex = 6; // Bottom border
else if (x == adjustedFrame.width)
spriteIndex = 7; // Right border
if (spriteIndex != -1) {
// Draw the frame section
RenderClxSprite(out, frame[spriteIndex], { pos.x + (x * FrameSpriteSize), pos.y + (y * FrameSpriteSize) });
}
}
}
}
void HandleRightClickPortait()
{
Player &player = Players[RightClickedPortraitIndex];
if (player.plractive && &player != MyPlayer) {
InspectPlayer = &player;
OpenCharPanel();
if (!SpellbookFlag)
invflag = true;
RedrawEverything();
RightClickedPortraitIndex = -1;
}
}
PartySpriteOffset GetClassSpriteOffset(HeroClass hClass)
{
switch (hClass) {
case HeroClass::Bard:
hClass = HeroClass::Rogue;
break;
case HeroClass::Barbarian:
hClass = HeroClass::Warrior;
break;
default:
break;
}
return ClassSpriteOffsets[static_cast<size_t>(hClass)];
}
} // namespace
bool PartySidePanelOpen = true;
bool InspectingFromPartyPanel;
int PortraitIdUnderCursor = -1;
tl::expected<void, std::string> LoadPartyPanel()
{
ASSIGN_OR_RETURN(OwnedClxSpriteList frame, LoadCelWithStatus("data\\textslid", FrameSpriteSize));
ASSIGN_OR_RETURN(PlayerTags, LoadClxWithStatus("data\\monstertags.clx"));
const OwnedSurface out(PortraitFrameSize.width, PortraitFrameSize.height + HealthBarHeight + ManaBarHeight);
// Draw the health bar background
DrawBar(out, { { 0, 0 }, { PortraitFrameSize.width, HealthBarHeight } }, PAL16_GRAY + 10);
// Draw the frame the character portrait sprite will go
DrawMemberFrame(out, frame, { 0, HealthBarHeight });
// Draw the mana bar background
DrawBar(out, { { 0, HealthBarHeight + PortraitFrameSize.height }, { PortraitFrameSize.width, ManaBarHeight } }, PAL16_GRAY + 10);
PartyMemberFrame = SurfaceToClx(out);
return {};
}
void FreePartyPanel()
{
PartyMemberFrame = std::nullopt;
PlayerTags = std::nullopt;
}
void DrawPartyMemberInfoPanel(const Surface &out)
{
// Don't draw based on these criteria
if (CharFlag || !gbIsMultiplayer || !MyPlayer->friendlyMode || IsPlayerInStore() || IsStashOpen) {
if (PortraitIdUnderCursor != -1)
PortraitIdUnderCursor = -1;
return;
}
Point pos = PartyPanelPos;
if (AutomapActive)
pos.y += (FrameGap * 4);
if (*GetOptions().Graphics.showFPS)
pos.y += FrameGap;
int currentLongestNameWidth = PortraitFrameSize.width;
bool portraitUnderCursor = false;
for (Player &player : Players) {
if (!player.plractive || !player.friendlyMode)
continue;
#ifndef _DEBUG
if (&player == MyPlayer)
continue;
#endif
// Get the rect of the portrait to use later
const Rectangle currentPortraitRect = { pos, PortraitFrameSize };
const Surface gameScreen = out.subregionY(0, gnViewportHeight);
// Draw the characters frame
RenderClxSprite(gameScreen, (*PartyMemberFrame)[0], pos);
// Get the players remaining life
// If the player is using mana shield change the color
const int lifeTicks = ((player._pHitPoints * PortraitFrameSize.width) + (player._pMaxHP / 2)) / player._pMaxHP;
const uint8_t hpBarColor = (player.pManaShield) ? PAL8_YELLOW + 5 : PAL8_RED + 4;
// Now draw the characters remaining life
DrawBar(gameScreen, { pos, { lifeTicks, HealthBarHeight } }, hpBarColor);
// Add to the position before continuing to the next item
pos.y += HealthBarHeight;
// Get the players current portrait sprite
const ClxSprite playerPortraitSprite = GetPlayerPortraitSprite(player);
// Get the offset of the sprite based on the players class so it get's rendered in the correct position
const PartySpriteOffset offsets = GetClassSpriteOffset(player._pClass);
Point offset = (player.isOnLevel(0)) ? offsets.inTownOffset : offsets.inDungeonOffset;
if (player._pHitPoints <= 0 && IsPlayerUnarmed(player))
offset = offsets.isDeadOffset;
// Calculate the players portait position
const Point portraitPos = { ((-(playerPortraitSprite.width() / 2)) + (PortraitFrameSize.width / 2)) + offset.x, offset.y };
// Get a subregion of the surface so the portrait doesn't get drawn over the frame
const Surface frameSubregion = gameScreen.subregion(
pos.x + FrameBorderSize,
pos.y + FrameBorderSize,
PortraitFrameSize.width - (FrameBorderSize * 2),
PortraitFrameSize.height - (FrameBorderSize * 2));
PortraitFrameRects[player.getId()] = {
{ frameSubregion.region.x, frameSubregion.region.y },
{ frameSubregion.region.w, frameSubregion.region.h }
};
// Draw the portrait sprite
RenderClxSprite(
frameSubregion,
playerPortraitSprite,
portraitPos);
if ((player.getId() + 1U) < (*PlayerTags).numSprites()) {
// Draw the player tag
const int tagWidth = (*PlayerTags)[player.getId() + 1].width();
RenderClxSprite(
frameSubregion,
(*PlayerTags)[player.getId() + 1],
{ PortraitFrameSize.width - (tagWidth + (tagWidth / 2)), 0 });
}
// Check to see if the player is dead and if so we draw a half transparent red rect over the portrait
if (player._pHitPoints <= 0) {
DrawHalfTransparentRectTo(
frameSubregion,
0, 0,
PortraitFrameSize.width,
PortraitFrameSize.height,
PAL8_RED + 4);
}
// Add to the position before continuing to the next item
pos.y += PortraitFrameSize.height;
// Get the players remaining mana
const int manaTicks = ((player._pMana * PortraitFrameSize.width) + (player._pMaxMana / 2)) / player._pMaxMana;
const uint8_t manaBarColor = PAL8_BLUE + 3;
// Now draw the characters remaining mana
DrawBar(gameScreen, { pos, { manaTicks, ManaBarHeight } }, manaBarColor);
// Add to the position before continuing to the next item
pos.y += ManaBarHeight;
// Draw the players name under the frame
DrawString(
gameScreen,
player._pName,
pos,
{ .flags = UiFlags::ColorGold | UiFlags::Outlined | UiFlags::FontSize12 });
// Add to the position before continuing onto the next player
pos.y += FrameGap + 5;
// Check to see if the player is hovering over this portrait and if so draw a string under the cursor saying they can right click to inspect
if (currentPortraitRect.contains(MousePosition)) {
PortraitIdUnderCursor = player.getId();
portraitUnderCursor = true;
}
// Get the current players name width
const int width = GetLineWidth(player._pName);
// Now check to see if it's the current longest name
if (width >= currentLongestNameWidth)
currentLongestNameWidth = width;
// Check to see if the Y position is more then the main panel position
if (pos.y >= GetMainPanel().position.y - PortraitFrameSize.height - 10) {
// If so we need to draw the next set of portraits back at the top and to the right of the original position
pos.y = PartyPanelPos.y;
if (AutomapActive)
pos.y += (FrameGap * 4);
if (*GetOptions().Graphics.showFPS)
pos.y += FrameGap;
// Add the current longest name width to the X position
pos.x += currentLongestNameWidth + (FrameGap / 2);
}
}
if (RightClickedPortraitIndex != -1)
HandleRightClickPortait();
if (!portraitUnderCursor)
PortraitIdUnderCursor = -1;
}
bool DidRightClickPartyPortrait()
{
for (size_t i = 0; i < sizeof(PortraitFrameRects) / sizeof(PortraitFrameRects[0]); i++) {
if (PortraitFrameRects[i].contains(MousePosition)) {
RightClickedPortraitIndex = static_cast<int>(i);
InspectingFromPartyPanel = true;
return true;
}
}
return false;
}
} // namespace devilution