mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-28 00:42:19 -07:00
Player pawns are looked up in QPixmapCache under a key built from the rendered size, user level, and the avatar pixmap's cacheKey(). A null pixmap reports cacheKey() 0, so all players without a custom avatar collided: the first pawn rendered for a given size and user level was reused for the next one, showing the wrong player's pawn. Extend the key with the rendered height, the lowercased privlevel (matching UserLevelPixmapGenerator), and both pawn colors so that every visually distinct pawn gets its own cache entry. Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
236 lines
8.8 KiB
C++
236 lines
8.8 KiB
C++
#include "player_target.h"
|
|
|
|
#include "../../client/settings/cache_settings.h"
|
|
#include "../../game/player/player_logic.h"
|
|
#include "../../interface/pixel_map_generator.h"
|
|
#include "../game_scene.h"
|
|
|
|
#include <QApplication>
|
|
#include <QDebug>
|
|
#include <QPainter>
|
|
#include <QPixmapCache>
|
|
#include <QtMath>
|
|
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
|
|
|
|
PlayerCounter::PlayerCounter(CounterState *state, PlayerLogic *player, QGraphicsItem *parent)
|
|
: AbstractCounter(state, player, false, false, parent)
|
|
{
|
|
}
|
|
|
|
QRectF PlayerCounter::boundingRect() const
|
|
{
|
|
return {0, 0, 50, 30};
|
|
}
|
|
|
|
void PlayerCounter::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/)
|
|
{
|
|
const int radius = 15;
|
|
const qreal border = 1.5;
|
|
// The box is drawn with a border-wide stroke straddling the path, so the
|
|
// visible outline spans [inset, inset + border]. Fills that must not cover
|
|
// the outline (e.g. the life-change flash) use a path inset by `border`.
|
|
const auto makePath = [](qreal inset) {
|
|
QPainterPath path(QPointF(50 - inset, inset));
|
|
path.lineTo(radius, inset);
|
|
path.arcTo(inset, inset, 2 * radius, 2 * radius, 90, 90);
|
|
path.lineTo(inset, 30 - inset);
|
|
path.lineTo(50 - inset, 30 - inset);
|
|
path.closeSubpath();
|
|
return path;
|
|
};
|
|
QPainterPath path = makePath(border / 2);
|
|
|
|
QPen pen(QColor(100, 100, 100));
|
|
pen.setWidthF(border);
|
|
painter->setPen(pen);
|
|
painter->setBrush(hovered ? QColor(50, 50, 50, 160) : QColor(0, 0, 0, 160));
|
|
|
|
painter->drawPath(path);
|
|
|
|
QRectF translatedRect = path.controlPointRect();
|
|
QSize translatedSize = translatedRect.size().toSize();
|
|
QFont font("Serif");
|
|
font.setWeight(QFont::Bold);
|
|
font.setPixelSize(qMax(qRound(translatedSize.height() / 1.3), 9));
|
|
painter->setFont(font);
|
|
painter->setPen(Qt::white);
|
|
painter->drawText(translatedRect, Qt::AlignCenter, QString::number(value));
|
|
|
|
// Life-change flash: emerald on gain, red on loss, decaying over a few ticks.
|
|
if (flashAlpha > 0) {
|
|
painter->save();
|
|
QColor flashColor = flashDelta > 0 ? QColor(52, 224, 122) : QColor(239, 68, 68);
|
|
flashColor.setAlphaF(0.45 * flashAlpha);
|
|
painter->setPen(Qt::NoPen);
|
|
painter->setBrush(flashColor);
|
|
painter->setOpacity(0.85);
|
|
painter->drawPath(makePath(border));
|
|
painter->restore();
|
|
}
|
|
}
|
|
|
|
void PlayerCounter::onValueChanged(int oldValue, int newValue, bool skipDamageAnimation)
|
|
{
|
|
flashDelta = newValue - oldValue;
|
|
if (flashDelta == 0) {
|
|
return;
|
|
}
|
|
|
|
if (!SettingsCache::instance().userInterface().getLifeCounterAnimationsEnabled()) {
|
|
flashAlpha = 0.0;
|
|
return;
|
|
}
|
|
|
|
if (skipDamageAnimation) {
|
|
flashAlpha = 0.0;
|
|
return;
|
|
}
|
|
|
|
flashAlpha = 1.0;
|
|
flashClock.start();
|
|
if (scene()) {
|
|
static_cast<GameScene *>(scene())->registerAnimationItem(this);
|
|
}
|
|
}
|
|
|
|
bool PlayerCounter::animationEvent()
|
|
{
|
|
flashAlpha = 1.0 - flashClock.elapsed() / flashDurationMs;
|
|
if (flashAlpha <= 0.0) {
|
|
flashAlpha = 0.0;
|
|
return false;
|
|
}
|
|
update();
|
|
return true;
|
|
}
|
|
|
|
PlayerTarget::PlayerTarget(PlayerLogic *_owner, QGraphicsItem *parentItem)
|
|
: ArrowTarget(_owner, parentItem), playerCounter(nullptr)
|
|
{
|
|
setCacheMode(DeviceCoordinateCache);
|
|
|
|
const std::string &bmp = _owner->getPlayerInfo()->getUserInfo()->avatar_bmp();
|
|
if (!fullPixmap.loadFromData((const uchar *)bmp.data(), static_cast<uint>(bmp.size()))) {
|
|
fullPixmap = QPixmap();
|
|
}
|
|
}
|
|
|
|
PlayerTarget::~PlayerTarget()
|
|
{
|
|
// Explicit deletion is necessary in spite of parent/child relationship
|
|
// as we need this object to be alive to receive the destroyed() signal.
|
|
delete playerCounter;
|
|
}
|
|
|
|
QRectF PlayerTarget::boundingRect() const
|
|
{
|
|
return {0, 0, 160, 64};
|
|
}
|
|
|
|
void PlayerTarget::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/)
|
|
{
|
|
const ServerInfo_User *const info = owner->getPlayerInfo()->getUserInfo();
|
|
|
|
const qreal border = 2;
|
|
|
|
QRectF avatarBoundingRect = boundingRect().adjusted(border, border, -border, -border);
|
|
QRectF translatedRect = painter->combinedTransform().mapRect(avatarBoundingRect);
|
|
QSize translatedSize = translatedRect.size().toSize();
|
|
QPixmap cachedPixmap;
|
|
// The key must cover everything the generated pawn depends on: the rendered
|
|
// size, the user level, and the pixmap being drawn. fullPixmap.cacheKey() is
|
|
// 0 for every null pixmap, so the default-pawn branch additionally needs the
|
|
// pawn's privlevel (lowercased, matching UserLevelPixmapGenerator) and colors
|
|
// in the key — otherwise two players without a custom avatar (and the same
|
|
// user level) would share one cached pawn.
|
|
const QString cacheKey = "avatar" + QString::number(translatedSize.width()) + "_" +
|
|
QString::number(translatedSize.height()) + "_" + QString::number(info->user_level()) +
|
|
"_" + QString::number(fullPixmap.cacheKey()) + "_" +
|
|
QString::fromStdString(info->privlevel()).toLower() + "_" +
|
|
QString::fromStdString(info->pawn_colors().left_side()) + "_" +
|
|
QString::fromStdString(info->pawn_colors().right_side());
|
|
if (!QPixmapCache::find(cacheKey, &cachedPixmap)) {
|
|
cachedPixmap = QPixmap(translatedSize.width(), translatedSize.height());
|
|
|
|
QPainter tempPainter(&cachedPixmap);
|
|
// pow(foo, 0.5) equals to sqrt(foo), but using sqrt(foo) in this context will produce a compile error with
|
|
// MSVC++
|
|
QRadialGradient grad(translatedRect.center(), qPow(translatedSize.width() * translatedSize.width() +
|
|
translatedSize.height() * translatedSize.height(),
|
|
0.5) /
|
|
2);
|
|
grad.setColorAt(1, Qt::black);
|
|
grad.setColorAt(0, QColor(180, 180, 180));
|
|
tempPainter.fillRect(QRectF(0, 0, translatedSize.width(), translatedSize.height()), grad);
|
|
|
|
if (fullPixmap.isNull()) {
|
|
int sideLength = translatedSize.height();
|
|
QPixmap tempPixmap = UserLevelPixmapGenerator::generatePixmap(
|
|
sideLength, UserLevelFlags(info->user_level()), info->pawn_colors(), false,
|
|
QString::fromStdString(info->privlevel()));
|
|
int x = (translatedSize.width() - sideLength) / 2;
|
|
int y = 0;
|
|
tempPainter.drawPixmap(x, y, tempPixmap);
|
|
} else {
|
|
QPixmap tempPixmap = fullPixmap.scaled(translatedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
|
int x = (translatedSize.width() - tempPixmap.width()) / 2;
|
|
int y = (translatedSize.height() - tempPixmap.height()) / 2;
|
|
tempPainter.drawPixmap(x, y, tempPixmap);
|
|
}
|
|
|
|
QPixmapCache::insert(cacheKey, cachedPixmap);
|
|
}
|
|
|
|
painter->save();
|
|
resetPainterTransform(painter);
|
|
painter->translate((translatedSize.width() - cachedPixmap.width()) / 2.0, 0);
|
|
painter->drawPixmap(translatedRect, cachedPixmap, cachedPixmap.rect());
|
|
painter->restore();
|
|
|
|
QRectF nameRect = QRectF(0, boundingRect().height() - 20, 110, 20);
|
|
painter->fillRect(nameRect, QColor(0, 0, 0, 160));
|
|
QRectF translatedNameRect = painter->combinedTransform().mapRect(nameRect);
|
|
|
|
painter->save();
|
|
resetPainterTransform(painter);
|
|
|
|
QString name = QString::fromStdString(info->name());
|
|
if (name.size() > 13) {
|
|
name = name.mid(0, 10) + "...";
|
|
}
|
|
|
|
QFont font;
|
|
font.setPixelSize(qMax(qRound(translatedNameRect.height() / 1.5), 9));
|
|
painter->setFont(font);
|
|
painter->setPen(Qt::white);
|
|
painter->drawText(translatedNameRect, Qt::AlignVCenter | Qt::AlignLeft, " " + name);
|
|
painter->restore();
|
|
|
|
QPen pen(QColor(100, 100, 100));
|
|
pen.setWidth(border);
|
|
pen.setJoinStyle(Qt::RoundJoin);
|
|
painter->setPen(pen);
|
|
painter->drawRect(boundingRect().adjusted(border / 2, border / 2, -border / 2, -border / 2));
|
|
|
|
if (getBeingPointedAt()) {
|
|
painter->fillRect(boundingRect(), QBrush(QColor(255, 0, 0, 100)));
|
|
}
|
|
}
|
|
|
|
AbstractCounter *PlayerTarget::addCounter(CounterState *state)
|
|
{
|
|
if (playerCounter) {
|
|
disconnect(playerCounter, nullptr, this, nullptr);
|
|
playerCounter->delCounter();
|
|
}
|
|
playerCounter = new PlayerCounter(state, owner, this);
|
|
playerCounter->setPos(boundingRect().width() - playerCounter->boundingRect().width(),
|
|
boundingRect().height() - playerCounter->boundingRect().height());
|
|
connect(playerCounter, &PlayerCounter::destroyed, this, &PlayerTarget::counterDeleted);
|
|
return playerCounter;
|
|
}
|
|
|
|
void PlayerTarget::counterDeleted()
|
|
{
|
|
playerCounter = nullptr;
|
|
}
|