Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions HeavenClient/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ if(HEAVENCLIENT_BUILD_TESTS)
add_test(NAME slide_menu COMMAND slide_menu_tests)
add_executable(quiz_tests tests/quiz.cpp)
add_test(NAME quiz COMMAND quiz_tests)
add_executable(floating_lines_tests tests/floating_lines.cpp)
add_test(NAME floating_lines COMMAND floating_lines_tests)
add_executable(world_map_lookup_tests tests/world_map_lookup.cpp)
add_test(NAME world_map_lookup COMMAND world_map_lookup_tests)
add_executable(swindle_words_tests tests/swindle_words.cpp)
Expand Down Expand Up @@ -691,6 +693,7 @@ add_executable(HeavenClient
IO/Components/EquipTooltip.h
IO/Components/ContextMenuLayout.h
IO/Components/FadeNotice.h
IO/Components/FloatingLines.h
IO/Components/Gauge.cpp
IO/Components/Gauge.h
IO/Components/Icon.cpp
Expand Down Expand Up @@ -818,6 +821,8 @@ add_executable(HeavenClient
IO/UITypes/UIQuiz.h
IO/UITypes/UIRpsGame.cpp
IO/UITypes/UIRpsGame.h
IO/UITypes/UIScriptMessage.cpp
IO/UITypes/UIScriptMessage.h
IO/UITypes/UIFieldClock.cpp
IO/UITypes/UIFieldClock.h
IO/UITypes/UIAvatarMegaphone.cpp
Expand Down
4 changes: 3 additions & 1 deletion HeavenClient/Graphics/Color.h
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ namespace ms
KITEMSG,
JOBHEAD,
JOBDISABLED,
SCRIPTGOLD,
NUM_COLORS
};

Expand Down Expand Up @@ -247,7 +248,8 @@ namespace ms
{0.14f, 0.28f, 0.41f}, // Parcel select (#244768, the parcel Duey selects)
{0.80f, 0.80f, 0.60f}, // Kite msg (#CCCC99, the item's words after a kite's flier)
{0.47f, 0.50f, 0.53f}, // Job head (#777F88, a class's box in the Party Search window)
{0.55f, 0.54f, 0.55f} // Job disabled (#8C8A8C, a greyed box's label there)
{0.55f, 0.54f, 0.55f}, // Job disabled (#8C8A8C, a greyed box's label there)
{1.00f, 0.78f, 0.00f} // Script gold (#FFC600, the floating script lines)
};

// Create a color by an array of real numbers [0.0f, 1.0f]
Expand Down
74 changes: 74 additions & 0 deletions HeavenClient/IO/Components/FloatingLines.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#pragma once

#include <array>
#include <cstddef>
#include <cstdint>
#include <optional>
#include <string>

namespace ms
{
// v83's five floating lines near the top of the screen (CWvsContext+0x3680, FUN_006ce251), which
// SCRIPT_PROGRESS_MESSAGE writes to. A line takes the first slot that is free: never used, or shown for
// more than 3 s, though it may still be fading. With all five busy the line is dropped. A line stays for
// 3 s and then fades out over 3 more.
class FloatingLines
{
public:
static constexpr size_t SLOTS = 5;
static constexpr int32_t SHOWN = 3000;
static constexpr int32_t FADE = 3000;

// The slot `text` went to, if any was free.
std::optional<size_t> add(const std::string &text)
{
for (size_t i = 0; i < SLOTS; i++)
{
Slot &slot = slots[i];

if (slot.age < 0 || slot.age > SHOWN)
{
slot = { text, 0 };
return i;
}
}

return std::nullopt;
}

void update(int32_t elapsed)
{
for (Slot &slot : slots)
if (slot.age >= 0 && slot.age < SHOWN + FADE)
slot.age += elapsed;
}

float opacity(size_t i) const
{
int32_t age = slots[i].age;

if (age < 0 || age >= SHOWN + FADE)
return 0.0f;

if (age <= SHOWN)
return 1.0f;

return 1.0f - static_cast<float>(age - SHOWN) / FADE;
}

const std::string &text(size_t i) const
{
return slots[i].text;
}

private:
struct Slot
{
std::string text;
// Since it was shown, in ms; -1 when never used.
int32_t age = -1;
};

std::array<Slot, SLOTS> slots;
};
}
1 change: 1 addition & 0 deletions HeavenClient/IO/UIElement.h
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ namespace ms
PARTYSEARCH,
PARTYSEARCHICON,
RPSGAME,
SCRIPTMESSAGE,
NUM_TYPES
};

Expand Down
4 changes: 4 additions & 0 deletions HeavenClient/IO/UIStateGame.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
#include "UITypes/UIMessenger.h"
#include "UITypes/UIMiniMap.h"
#include "UITypes/UIQuestComplete.h"
#include "UITypes/UIScriptMessage.h"
#include "UITypes/UIQuestLog.h"
#include "UITypes/UIQuit.h"
#include "UITypes/UIShop.h"
Expand Down Expand Up @@ -160,6 +161,7 @@ namespace ms
emplace<UIBuffList>();
emplace<UIQuestHelper>(Stage::get().get_player().get_quests(), inventory);
emplace<UIQuestComplete>(Stage::get().get_player().get_quests(), inventory);
emplace<UIScriptMessage>();
emplace<UIPartyHP>(Stage::get().get_player().get_party(), stats);
emplace<UIShop>(look, inventory);
emplace<UIStorage>(inventory);
Expand Down Expand Up @@ -765,6 +767,7 @@ namespace ms
UIElement::Type::BUFFLIST,
UIElement::Type::QUESTHELPER,
UIElement::Type::QUESTCOMPLETE,
UIElement::Type::SCRIPTMESSAGE,
UIElement::Type::PARTYHP,
UIElement::Type::NPCTALK,
UIElement::Type::AVATARSELECT,
Expand Down Expand Up @@ -866,6 +869,7 @@ namespace ms
case UIElement::Type::INVITECARD:
case UIElement::Type::MEMONOTICE:
case UIElement::Type::QUESTCOMPLETE:
case UIElement::Type::SCRIPTMESSAGE:
case UIElement::Type::NOTICE:
case UIElement::Type::ENTERNUMBER:
case UIElement::Type::FRIENDGROUP:
Expand Down
33 changes: 22 additions & 11 deletions HeavenClient/IO/UITypes/UIQuestComplete.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,13 @@ namespace ms
bool changed = false;

// Quests handed in or forfeited take their notice with them; the rest
// fade, and those below a notice that leaves move down.
// fade, and those below a notice that leaves move down. The server's
// notices may be for quests the catalog lacks, as the explorer titles.
for (auto it = notices.begin(); it != notices.end();)
{
it->life.update(Constants::TIMESTEP);

if (it->life.gone() || !started.count(it->id))
if (it->life.gone() || !quests.is_started(it->id))
{
it = notices.erase(it);
changed = true;
Expand All @@ -96,15 +97,7 @@ namespace ms

for (int16_t id : completion.update(started))
{
const QuestInfo* quest = QuestCatalog::get().find(id);
Text name(Text::A12M, Text::LEFT, Color::Name::WHITE, quest ? quest->name : std::to_string(id));
string_format::format_with_ellipsis(name, NAME_WIDTH);

notices.erase(std::remove_if(notices.begin(), notices.end(), [&](const Notice& n) { return n.id == id; }),
notices.end());
notices.push_back({ id, std::move(name), FadeNotice(LIFETIME) });
Sound(Sound::Name::INVITE).play();

push_notice(id);
changed = true;
}

Expand All @@ -115,6 +108,24 @@ namespace ms
UIElement::update();
}

void UIQuestComplete::add_notice(int16_t id)
{
push_notice(id);
rebuild();
}

void UIQuestComplete::push_notice(int16_t id)
{
std::string title = nl::nx::quest["QuestInfo.img"][std::to_string(id)]["name"];
Text name(Text::A12M, Text::LEFT, Color::Name::WHITE, title.empty() ? std::to_string(id) : title);
string_format::format_with_ellipsis(name, NAME_WIDTH);

notices.erase(std::remove_if(notices.begin(), notices.end(), [&](const Notice& n) { return n.id == id; }),
notices.end());
notices.push_back({ id, std::move(name), FadeNotice(LIFETIME) });
Sound(Sound::Name::INVITE).play();
}

void UIQuestComplete::rebuild()
{
buttons.clear();
Expand Down
7 changes: 6 additions & 1 deletion HeavenClient/IO/UITypes/UIQuestComplete.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
namespace ms
{
// v83's red "Quest Complete!" notice (UIWindow.img/FadeYesNo/backgrnd4)
// above the status bar, shown when a quest's last objective is met. O
// above the status bar, shown when a quest's last objective is met, or
// when the server says so (QUEST_CLEAR, as for an explorer title). O
// opens the quest in the quest log and X dismisses it. Each notice stays
// until then, or until the quest is no longer in progress; newer notices
// stack above older ones, invitations included.
Expand All @@ -35,6 +36,9 @@ namespace ms

UIElement::Type get_type() const override;

// A notice for quest `id`, replacing one it already has.
void add_notice(int16_t id);

protected:
Button::State button_pressed(uint16_t buttonid) override;

Expand All @@ -49,6 +53,7 @@ namespace ms
FadeNotice life;
};

void push_notice(int16_t id);
void rebuild();
// From the bottom of the stack to `notice`.
Point<int16_t> offset(const Notice& notice) const;
Expand Down
17 changes: 12 additions & 5 deletions HeavenClient/IO/UITypes/UIQuestLog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -184,9 +184,10 @@ namespace ms
}

// Fill in the quest-only placeholders, which NpcTextMarkup leaves:
// #c how many of an item the player holds, and #a a kill count
// (#a<index># for this quest's mobs, or #a<quest><index>#). Line
// breaks become the renderer's escapes.
// #c how many of an item the player holds, #a a kill count
// (#a<index># for this quest's mobs, or #a<quest><index>#), and #R
// a quest's record as it stands (#R27010# an explorer title's count of
// places). Line breaks become the renderer's escapes.
std::string quest_counts(const std::string& text, int16_t quest_id)
{
auto& player = Stage::get().get_player();
Expand All @@ -208,8 +209,8 @@ namespace ms
size_t close = c == '#' && i + 2 < text.size() ? text.find('#', i + 2) : std::string::npos;
char tag = i + 1 < text.size() ? text[i + 1] : 0;

if (close == std::string::npos || (tag != 'c' && tag != 'a')
|| !NpcTextMarkup::is_digits(text.substr(i + 2, close - i - 2)) || close - i - 2 > 9)
if (close == std::string::npos || (tag != 'c' && tag != 'a' && tag != 'R')
|| !NpcTextMarkup::is_digits(text.substr(i + 2, close - i - 2)) || close - i - 2 > (tag == 'R' ? 5 : 9))
{
out += c;
continue;
Expand All @@ -221,6 +222,12 @@ namespace ms
{
out += std::to_string(player.get_inventory().get_total_item_count(id));
}
else if (tag == 'R')
{
const auto& started = player.get_quests().started_quests();
auto record = started.find(static_cast<int16_t>(id));
out += record == started.end() || record->second.empty() ? "0" : record->second;
}
else
{
int16_t quest = id < 10 ? quest_id : static_cast<int16_t>(id / 10);
Expand Down
73 changes: 73 additions & 0 deletions HeavenClient/IO/UITypes/UIScriptMessage.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#include "UIScriptMessage.h"

#include "../../Constants.h"

namespace ms
{
namespace
{
// The first line's middle, from the top; later lines sit higher. v83's line is a 20 px layer around it.
constexpr int16_t FIRST_Y = 150;
constexpr int16_t LINE_HEIGHT = 20;
constexpr int16_t HALF_LINE = 10;
// Where the black copies go under the gold.
const std::array<Point<int16_t>, 4> EDGE = {
Point<int16_t>(-1, 0), Point<int16_t>(1, 0), Point<int16_t>(0, -1), Point<int16_t>(0, 1)
};
}

UIScriptMessage::UIScriptMessage() : UIElement(Point<int16_t>(), Point<int16_t>())
{
update_screen(Constants::Constants::get().get_viewwidth(), Constants::Constants::get().get_viewheight());
}

void UIScriptMessage::draw(float) const
{
for (size_t i = 0; i < FloatingLines::SLOTS; i++)
{
float opacity = lines.opacity(i);

if (opacity <= 0.0f)
continue;

Point<int16_t> at = position + Point<int16_t>(0, FIRST_Y - HALF_LINE - LINE_HEIGHT * static_cast<int16_t>(i));

// v83 fades the outlined line as one layer. Drawn apart, edges at full fade would darken the gold
// they lie under, so they fade faster.
for (const Point<int16_t> &edge : EDGE)
edges[i].draw(DrawArgument(at + edge, opacity * opacity));

texts[i].draw(DrawArgument(at, opacity));
}
}

void UIScriptMessage::update()
{
lines.update(Constants::TIMESTEP);
}

void UIScriptMessage::update_screen(int16_t new_width, int16_t)
{
position = Point<int16_t>(new_width / 2, 0);
}

bool UIScriptMessage::is_in_range(Point<int16_t>) const
{
return false;
}

UIElement::Type UIScriptMessage::get_type() const
{
return TYPE;
}

void UIScriptMessage::show(const std::string &text)
{
if (auto slot = lines.add(text))
{
// One plain line: no colour codes or breaks.
texts[*slot] = Text(Text::Font::A15B, Text::Alignment::CENTER, Color::Name::SCRIPTGOLD, text, 0, false);
edges[*slot] = Text(Text::Font::A15B, Text::Alignment::CENTER, Color::Name::BLACK, text, 0, false);
}
}
}
38 changes: 38 additions & 0 deletions HeavenClient/IO/UITypes/UIScriptMessage.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#pragma once

#include "../UIElement.h"
#include "../Components/FloatingLines.h"

#include "../../Graphics/Text.h"

#include <array>
#include <string>

namespace ms
{
// SCRIPT_PROGRESS_MESSAGE's lines (FUN_00a13f20): gold Arial bold 15 with a black edge, centred across the
// screen, the first slot 150 px from the top and each later one 20 px higher (ctor FUN_006cd6ba). See
// FloatingLines for how long they stay. Clicks go through them.
class UIScriptMessage : public UIElement
{
public:
static constexpr Type TYPE = UIElement::Type::SCRIPTMESSAGE;
static constexpr bool FOCUSED = false;
static constexpr bool TOGGLED = false;

UIScriptMessage();

void draw(float inter) const override;
void update() override;
void update_screen(int16_t new_width, int16_t new_height) override;
bool is_in_range(Point<int16_t> cursorpos) const override;

UIElement::Type get_type() const override;

void show(const std::string &text);

private:
FloatingLines lines;
std::array<Text, FloatingLines::SLOTS> texts, edges;
};
}
Loading
Loading