osb/source/frontend/StarChatBubbleManager.cpp
2023-06-20 14:33:09 +10:00

345 lines
15 KiB
C++

#include "StarChatBubbleManager.hpp"
#include "StarJson.hpp"
#include "StarJsonExtra.hpp"
#include "StarRoot.hpp"
#include "StarConfiguration.hpp"
#include "StarWorldClient.hpp"
#include "StarChattyEntity.hpp"
#include "StarAssets.hpp"
#include "StarAssetTextureGroup.hpp"
#include "StarImageMetadataDatabase.hpp"
#include "StarGuiContext.hpp"
namespace Star {
ChatBubbleManager::ChatBubbleManager()
: m_textTemplate(Vec2F()), m_portraitTextTemplate(Vec2F()) {
auto assets = Root::singleton().assets();
m_guiContext = GuiContext::singletonPtr();
auto jsonData = assets->json("/interface/windowconfig/chatbubbles.config");
m_color = jsonToColor(jsonData.get("textColor"));
m_fontSize = jsonData.getInt("fontSize");
m_textPadding = jsonToVec2F(jsonData.get("textPadding"));
m_zoom = jsonData.getInt("textZoom");
m_bubbleOffset = jsonToVec2F(jsonData.get("bubbleOffset"));
m_maxAge = jsonData.getFloat("maxAge");
m_portraitMaxAge = jsonData.getFloat("portraitMaxAge");
unsigned textWrapWidth = jsonData.getUInt("textWrapWidth");
m_textTemplate = TextPositioning{Vec2F(), HorizontalAnchor::HMidAnchor, VerticalAnchor::TopAnchor, textWrapWidth * m_zoom};
m_interBubbleMargin = jsonData.getFloat("interBubbleMargin");
m_maxMessagePerEntity = jsonData.getInt("maxMessagePerEntity");
m_bubbles.setTweenFactor(jsonData.getFloat("tweenFactor"));
m_bubbles.setMovementThreshold(jsonData.getFloat("movementThreshold"));
m_portraitBackgroundImage = jsonData.getString("portraitBackgroundImage");
m_portraitMoreImage = jsonData.getString("portraitMoreImage");
m_portraitMorePosition = jsonToVec2I(jsonData.get("portraitMorePosition"));
m_portraitBackgroundSize = jsonToVec2I(jsonData.get("portraitBackgroundSize"));
m_portraitPosition = jsonToVec2I(jsonData.get("portraitPosition"));
m_portraitSize = jsonToVec2I(jsonData.get("portraitSize"));
m_portraitTextPosition = jsonToVec2I(jsonData.get("portraitTextPosition"));
m_portraitTextWidth = jsonData.getUInt("portraitTextWidth");
m_portraitChatterFramerate = jsonData.getFloat("portraitChatterFramerate");
m_portraitChatterDuration = jsonData.getFloat("portraitChatterDuration");
m_portraitTextTemplate = TextPositioning{Vec2F(m_portraitTextPosition), HorizontalAnchor::LeftAnchor, VerticalAnchor::TopAnchor, m_portraitTextWidth * m_zoom};
// This is a factor(0.0 - 1.0) based on the window size.
// 0.0 is directly over the player, 1.0 is the edge of the window
m_furthestVisibleTextDistance = jsonData.getFloat("furthestTextDistance");
String textFadeFunctionName = jsonData.getString("textFadeFunction");
m_textFadeFunction = Root::singleton().functionDatabase()->function(textFadeFunctionName);
String bubbleFadeFunctionName = jsonData.getString("bubbleFadeFunction");
m_bubbleFadeFunction = Root::singleton().functionDatabase()->function(bubbleFadeFunctionName);
}
void ChatBubbleManager::setCamera(WorldCamera const& camera) {
float oldPixelRatio = m_camera.pixelRatio();
m_camera = camera;
if (m_camera.pixelRatio() != oldPixelRatio) {
List<ChatAction> actions;
m_bubbles.forEach([&actions](BubbleState<Bubble> const& state, Bubble& bubble) {
actions.append(SayChatAction{bubble.entity, bubble.text, state.idealDestination, bubble.config});
});
m_bubbles.clear();
for (auto portraitBubble : m_portraitBubbles)
actions.append(PortraitChatAction{
portraitBubble.entity,
portraitBubble.portrait,
portraitBubble.text,
portraitBubble.position,
portraitBubble.config
});
m_portraitBubbles.clear();
addChatActions(actions, true);
}
}
void ChatBubbleManager::update(WorldClientPtr world) {
m_bubbles.forEach([this, &world](BubbleState<Bubble>& bubbleState, Bubble& bubble) {
bubble.age += WorldTimestep;
if (auto entity = world->get<ChattyEntity>(bubble.entity)) {
bubble.onscreen = m_camera.worldGeometry().rectIntersectsRect(
m_camera.worldScreenRect(), entity->metaBoundBox().translated(entity->position()));
bubbleState.idealDestination = m_camera.worldToScreen(entity->mouthPosition() + m_bubbleOffset);
}
});
for (auto& portraitBubble : m_portraitBubbles) {
portraitBubble.age += WorldTimestep;
if (auto entity = world->entity(portraitBubble.entity)) {
portraitBubble.onscreen = m_camera.worldGeometry().rectIntersectsRect(m_camera.worldScreenRect(), entity->metaBoundBox().translated(entity->position()));
if (auto chatter = as<ChattyEntity>(entity))
portraitBubble.position = chatter->mouthPosition();
else
portraitBubble.position = entity->position();
}
}
Map<EntityId, int> count;
filter(m_portraitBubbles, [&](PortraitBubble const& portraitBubble) -> bool {
count[portraitBubble.entity] += m_maxMessagePerEntity;
if (count[portraitBubble.entity] > m_maxMessagePerEntity)
return false;
if (world->get<ChattyEntity>(portraitBubble.entity))
return portraitBubble.age < m_portraitMaxAge;
return false;
});
m_bubbles.filter([&](BubbleState<Bubble> const&, Bubble const& bubble) -> bool {
if (++count[bubble.entity] > m_maxMessagePerEntity)
return false;
if (world->get<ChattyEntity>(bubble.entity))
return bubble.age < m_maxAge;
return false;
});
m_bubbles.update();
}
uint8_t ChatBubbleManager::calcDistanceFadeAlpha(Vec2F bubbleScreenPosition, StoredFunctionPtr fadeFunction) const {
// first calculate bubble position as a factor, distance from center to edge
// of screen (0.0-1.0)
float halfScreenwidth = m_camera.screenSize()[0] * 0.5f;
float distanceFactor = (fabsf(bubbleScreenPosition[0] - halfScreenwidth)) / halfScreenwidth;
// that distance factor is divided by the max allowable distance
// to re-space the distance as a 0 - 1 over the max allowable distance
distanceFactor = clamp(distanceFactor / m_furthestVisibleTextDistance, 0.0f, 1.0f);
int alpha = fadeFunction->evaluate(distanceFactor);
return clamp(alpha, 0, 255);
}
void ChatBubbleManager::render() {
if (m_bubbles.empty() && m_portraitBubbles.empty())
return;
if (!Root::singleton().configuration()->get("speechBubbles").toBool())
return;
m_bubbles.forEach([this](BubbleState<Bubble> const& state, Bubble& bubble) {
if (bubble.onscreen) {
int alpha = calcDistanceFadeAlpha(state.currentPosition, m_bubbleFadeFunction);
if (alpha) {
for (auto const& bubbleImage : bubble.backgroundImages)
drawBubbleImage(state.currentPosition, bubbleImage, m_zoom, alpha);
for (auto const& bubbleText : bubble.bubbleText)
drawBubbleText(state.currentPosition, bubbleText, m_zoom, alpha, false);
}
}
});
for (auto portraitBubble : m_portraitBubbles) {
if (portraitBubble.onscreen) {
Vec2F screenPos = m_camera.worldToScreen(portraitBubble.position + m_bubbleOffset);
int frame = 0;
if (portraitBubble.age <= m_portraitChatterDuration)
frame = int((portraitBubble.age / m_portraitChatterFramerate) * 2) % 2;
// 255 here because portrait bubbles are always full opacity
for (auto const& bubbleImage : portraitBubble.backgroundImages)
drawBubbleImage(screenPos, make_tuple(get<0>(bubbleImage).replace("<frame>", toString(frame)), get<1>(bubbleImage)), m_zoom, 255);
// 255 here because portrait bubbles are always full opacity
for (auto const& bubbleText : portraitBubble.bubbleText)
drawBubbleText(screenPos, bubbleText, m_zoom, 255, true);
}
}
}
void ChatBubbleManager::addChatActions(List<ChatAction> chatActions, bool silent) {
auto assets = Root::singleton().assets();
auto config = assets->json("/interface/windowconfig/chatbubbles.config");
float partSize = config.getFloat("partSize");
for (auto action : chatActions) {
Json config = JsonObject{};
Vec2F position;
if (action.is<SayChatAction>()) {
auto sayAction = action.get<SayChatAction>();
config = sayAction.config.optObject().value(JsonObject{});
position = sayAction.position;
// TODO: Get rid of this stupid fucking bullshit, this is the ugliest
// fragilest pointlessest horseshit code in the codebase. It wouldn't
// bother me so bad if it weren't so fucking easy to do right.
m_guiContext->setFontSize(m_fontSize, m_zoom);
auto result = m_guiContext->determineTextSize(sayAction.text, m_textTemplate);
float textWidth = result.width() / m_zoom + m_textPadding[0];
float textHeight = result.height() / m_zoom + m_textPadding[1];
Vec2I innerTiles = Vec2I::ceil(Vec2F((textWidth + 4) / partSize, (textHeight + 3) / partSize));
if (innerTiles[0] % 2 == 0)
innerTiles[0] += 1;
if (innerTiles[0] < 3)
innerTiles[0] = 3;
int middleIdx = (innerTiles[0] - 1) / 2;
List<BubbleImage> backgroundImages;
if (config.getBool("drawBorder", true)) {
for (int y = 0; y < innerTiles[1]; y++) {
for (int x = 0; x < innerTiles[0]; x++) {
auto partPosition = [partSize](int x, int y) {
return Vec2F(x * partSize, y * partSize);
};
if (y == 0) {
if (x == 0) {
backgroundImages.append(make_tuple("/interface/chatbubbles/cornerBottomLeft.png", partPosition(x, y)));
} else if (x == innerTiles[0] - 1) {
backgroundImages.append(make_tuple("/interface/chatbubbles/cornerBottomRight.png", partPosition(x, y)));
} else {
if (middleIdx == x)
backgroundImages.append(make_tuple("/interface/chatbubbles/point.png", partPosition(x, y - 1)));
else
backgroundImages.append(make_tuple("/interface/chatbubbles/sideDown.png", partPosition(x, y)));
}
} else if (y == innerTiles[1] - 1) {
if (x == 0)
backgroundImages.append(make_tuple("/interface/chatbubbles/cornerTopLeft.png", partPosition(x, y)));
else if (x == innerTiles[0] - 1)
backgroundImages.append(make_tuple("/interface/chatbubbles/cornerTopRight.png", partPosition(x, y)));
else
backgroundImages.append(make_tuple("/interface/chatbubbles/sideUp.png", partPosition(x, y)));
} else {
if (x == 0)
backgroundImages.append(make_tuple("/interface/chatbubbles/sideLeft.png", partPosition(x, y)));
else if (x == innerTiles[0] - 1)
backgroundImages.append(make_tuple("/interface/chatbubbles/sideRight.png", partPosition(x, y)));
else
backgroundImages.append(make_tuple("/interface/chatbubbles/center.png", partPosition(x, y)));
}
}
}
}
float textMultiLineShift = textHeight;
float horizontalCenter = partSize * innerTiles[0] * 0.5f;
float verticalShift = (partSize * innerTiles[1] - textMultiLineShift) * 0.5f + textMultiLineShift;
Vec2F position = Vec2F(horizontalCenter, verticalShift);
List<BubbleText> bubbleTexts;
auto fontSize = config.getUInt("fontSize", m_fontSize);
auto color = config.opt("color").apply(jsonToColor).value(m_color);
bubbleTexts.append(make_tuple(sayAction.text, fontSize, color.toRgba(), true, position));
for (auto& backgroundImage : backgroundImages)
get<1>(backgroundImage) += Vec2F(-horizontalCenter, partSize);
for (auto& bubbleText : bubbleTexts)
get<4>(bubbleText) += Vec2F(-horizontalCenter, partSize);
auto pos = m_camera.worldToScreen(sayAction.position + m_bubbleOffset);
RectF boundBox = fold(backgroundImages, RectF::null(), [pos, this](RectF const& boundBox, BubbleImage const& bubbleImage) {
return boundBox.combined(bubbleImageRect(pos, bubbleImage, m_zoom));
});
Bubble bubble = {sayAction.entity, sayAction.text, sayAction.config, 0, move(backgroundImages), move(bubbleTexts), false};
List<BubbleState<Bubble>> oldBubbles = m_bubbles.filtered([&sayAction](BubbleState<Bubble> const&, Bubble const& bubble) {
return bubble.entity == sayAction.entity;
});
m_bubbles.filter([&sayAction](BubbleState<Bubble> const&, Bubble const& bubble) { return bubble.entity != sayAction.entity; });
m_bubbles.addBubble(pos, boundBox, move(bubble), m_interBubbleMargin * m_zoom);
oldBubbles.sort([](BubbleState<Bubble> const& a, BubbleState<Bubble> const& b) { return a.contents.age < b.contents.age; });
for (auto bubble : oldBubbles.slice(0, m_maxMessagePerEntity - 1))
m_bubbles.addBubble(bubble.idealDestination, bubble.boundBox, bubble.contents, 0);
} else if (action.is<PortraitChatAction>()) {
auto portraitAction = action.get<PortraitChatAction>();
config = portraitAction.config.optObject().value(JsonObject{});
position = portraitAction.position;
List<BubbleImage> backgroundImages;
backgroundImages.append(make_tuple(m_portraitBackgroundImage, Vec2F()));
if (config.getBool("drawMoreIndicator", false))
backgroundImages.append(make_tuple(m_portraitMoreImage, Vec2F(m_portraitMorePosition)));
backgroundImages.append(make_tuple(portraitAction.portrait, Vec2F(m_portraitPosition)));
List<BubbleText> bubbleTexts;
bubbleTexts.append(make_tuple(portraitAction.text, m_fontSize, m_color.toRgba(), false, Vec2F(m_portraitTextPosition)));
for (auto& backgroundImage : backgroundImages)
get<1>(backgroundImage) += Vec2F(-m_portraitBackgroundSize[0] / 2, 0);
for (auto& bubbleText : bubbleTexts)
get<4>(bubbleText) += Vec2F(-m_portraitBackgroundSize[0] / 2, 0);
m_portraitBubbles.prepend({
portraitAction.entity,
portraitAction.portrait,
portraitAction.text,
portraitAction.position,
portraitAction.config,
0,
move(backgroundImages),
move(bubbleTexts),
false
});
}
if (!silent) {
if (auto sound = config.optString("sound")) {
auto assets = Root::singleton().assets();
AudioInstancePtr audioInstance = make_shared<AudioInstance>(*assets->audio(*sound));
audioInstance->setPosition(position);
m_guiContext->playAudio(audioInstance);
}
}
}
}
RectF ChatBubbleManager::bubbleImageRect(Vec2F screenPos, BubbleImage const& bubbleImage, int pixelRatio) {
auto imgMetadata = Root::singleton().imageMetadataDatabase();
auto image = get<0>(bubbleImage);
return RectF::withSize(screenPos + get<1>(bubbleImage) * pixelRatio, Vec2F(imgMetadata->imageSize(image)) * pixelRatio);
}
void ChatBubbleManager::drawBubbleImage(Vec2F screenPos, BubbleImage const& bubbleImage, int pixelRatio, int alpha) {
auto image = get<0>(bubbleImage);
auto offset = get<1>(bubbleImage) * pixelRatio;
m_guiContext->drawQuad(image, screenPos + offset, pixelRatio, {255, 255, 255, alpha});
}
void ChatBubbleManager::drawBubbleText(Vec2F screenPos, BubbleText const& bubbleText, int pixelRatio, int alpha, bool isPortrait) {
Vec4B const& baseColor = get<2>(bubbleText);
// use the alpha as a blend value for the text colour as pulled from data.
Vec4B const& displayColor = Vec4B(baseColor[0], baseColor[1], baseColor[2], (baseColor[3] * alpha) / 255);
m_guiContext->setFontColor(displayColor);
m_guiContext->setFontSize(get<1>(bubbleText), m_zoom);
auto offset = get<4>(bubbleText) * pixelRatio;
TextPositioning tp = isPortrait ? m_portraitTextTemplate : m_textTemplate;
tp.pos = screenPos + offset;
m_guiContext->renderText(get<0>(bubbleText), tp);
}
}