Skip to content
Merged
15 changes: 15 additions & 0 deletions HeavenClient/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ if(HEAVENCLIENT_BUILD_TESTS)
add_test(NAME text_layout COMMAND text_layout_tests)
add_executable(chat_wrap_tests tests/chat_wrap.cpp)
add_test(NAME chat_wrap COMMAND chat_wrap_tests)
add_executable(megaphones_tests tests/megaphones.cpp)
add_test(NAME megaphones COMMAND megaphones_tests)
add_executable(chalkboard_tests tests/chalkboard.cpp)
add_test(NAME chalkboard COMMAND chalkboard_tests)
add_executable(teleport_rock_tests tests/teleport_rock.cpp Character/TeleportRock.cpp)
add_test(NAME teleport_rock COMMAND teleport_rock_tests)
add_executable(curse_words_tests tests/curse_words.cpp)
add_test(NAME curse_words COMMAND curse_words_tests)
add_executable(character_name_tests tests/character_name.cpp)
Expand Down Expand Up @@ -293,6 +299,8 @@ add_executable(HeavenClient
Character/BuddyList.h
Character/Buff.cpp
Character/Buff.h
Character/ChalkboardBalloon.cpp
Character/ChalkboardBalloon.h
Character/Char.cpp
Character/Char.h
Character/CharEffect.cpp
Expand Down Expand Up @@ -594,6 +602,12 @@ add_executable(HeavenClient
IO/UITypes/UIShop.h
IO/UITypes/UIStorage.cpp
IO/UITypes/UIStorage.h
IO/UITypes/UITeleportRock.cpp
IO/UITypes/UITeleportRock.h
IO/UITypes/UIMegaphone.cpp
IO/UITypes/UIMegaphone.h
IO/UITypes/UIItemMegaphone.cpp
IO/UITypes/UIItemMegaphone.h
IO/UITypes/UIInviteCard.cpp
IO/UITypes/UIInviteCard.h
IO/UITypes/UITradingRoom.cpp
Expand Down Expand Up @@ -746,6 +760,7 @@ add_executable(HeavenClient
Template/TimedQueue.h
Template/TypeMap.h
Util/CharacterName.h
Util/Chalkboard.h
Util/ChatWrap.h
Util/CurseWords.h
Util/HardwareInfo.h
Expand Down
184 changes: 184 additions & 0 deletions HeavenClient/Character/ChalkboardBalloon.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
#include "ChalkboardBalloon.h"

#include "../Util/Chalkboard.h"
#include "../Util/NxFiles.h"

#include <algorithm>

#include <nlnx/nx.hpp>

namespace ms
{
namespace
{
// The board floats this far over the head, its arrow 3 right of the
// feet (FUN_0048e298).
constexpr int16_t GAP = 5;
constexpr int16_t ARROW_SHIFT = 3;
// The close button sits 16 from the board's right and 4 from its top.
constexpr Point<int16_t> CLOSE_OFFSET = Point<int16_t>(-16, 4);
// The balloon font's descent; see ChatBalloon.
constexpr int16_t TEXT_DESCENT = 3;
}

ChalkboardBalloon::ChalkboardBalloon() : shown(false), closable(false), armed(false), close_state(NORMAL), left(0),
right(0), top(0), bottom(0), tiles(0)
{}

void ChalkboardBalloon::change(const std::optional<std::string> &note, bool can_close)
{
shown = note.has_value();
closable = can_close;
armed = false;
close_state = NORMAL;
text = note.value_or("");
lines.clear();

if (!shown)
return;

nl::node adboard = NxFiles::ui_classic["ChatBalloon.img"]["adboard"]["0"];

constexpr const char *names[NUM_PIECES] = { "c", "n", "s", "w", "e", "nw", "ne", "sw", "se", "arrow" };

for (size_t i = 0; i < NUM_PIECES; i++)
pieces[i] = adboard[names[i]];

nl::node button = NxFiles::ui_classic["Basic.img"]["BtClose"];
close[NORMAL] = button["normal"]["0"];
close[MOUSEOVER] = button["mouseOver"]["0"];
close[PRESSED] = button["pressed"]["0"];

// The margins reach as far as the pieces stick out of the inner area.
auto ox = [this](Piece p) { return pieces[p].get_origin().x(); };
auto oy = [this](Piece p) { return pieces[p].get_origin().y(); };
auto w = [this](Piece p) { return pieces[p].get_dimensions().x(); };
auto h = [this](Piece p) { return pieces[p].get_dimensions().y(); };

left = std::max({ ox(SOUTHWEST), ox(NORTHWEST), ox(WEST) });
right = std::max({ w(SOUTHEAST) - ox(SOUTHEAST), w(NORTHEAST) - ox(NORTHEAST), w(EAST) - ox(EAST) });
top = std::max({ oy(NORTHEAST), oy(NORTHWEST), oy(NORTH) });
bottom = std::max(h(ARROW) - oy(ARROW), h(SOUTH) - oy(SOUTH));

Text measure(Text::Font::BALLOON, Text::Alignment::LEFT, Color::Name::WHITE, "", 0, false);
auto width = [&measure](const std::string &part)
{
measure.change_text(part);

return measure.width();
};

for (const std::string &line : Chalkboard::wrap(text, width))
lines.emplace_back(Text::Font::BALLOON, Text::Alignment::LEFT, Color::Name::WHITE, line, 0, false);

int16_t first_width = lines.empty() ? 0 : lines.front().width();
tiles = Chalkboard::tiles(lines.size(), first_width);

int16_t rows = static_cast<int16_t>(lines.size());
size = Point<int16_t>(Chalkboard::TILE * tiles + left + right, Chalkboard::TILE * rows + top + bottom);
}

bool ChalkboardBalloon::is_shown() const
{
return shown;
}

const std::string &ChalkboardBalloon::get_text() const
{
return text;
}

Point<int16_t> ChalkboardBalloon::top_left(Point<int16_t> feet, int16_t avatar_height) const
{
int16_t inner = Chalkboard::TILE * tiles;

return feet + Point<int16_t>(ARROW_SHIFT - inner / 2 - right, -size.y() - avatar_height - GAP);
}

Rectangle<int16_t> ChalkboardBalloon::close_bounds(Point<int16_t> feet, int16_t avatar_height) const
{
Point<int16_t> lt = top_left(feet, avatar_height) + Point<int16_t>(size.x(), 0) + CLOSE_OFFSET;

return Rectangle<int16_t>(lt, lt + close[NORMAL].get_dimensions());
}

void ChalkboardBalloon::draw(Point<int16_t> feet, int16_t avatar_height) const
{
if (!shown)
return;

// Pieces are placed through their origins, as in the original.
Point<int16_t> origin = top_left(feet, avatar_height);
int16_t inner_right = size.x() - right;
int16_t inner_bottom = size.y() - bottom;
auto at = [origin](int16_t x, int16_t y) { return DrawArgument(origin + Point<int16_t>(x, y)); };

for (int16_t i = 0; i < tiles; i++)
{
int16_t x = left + Chalkboard::TILE * i;

pieces[NORTH].draw(at(x, top));
pieces[i == tiles / 2 ? ARROW : SOUTH].draw(at(x, inner_bottom));
}

for (size_t j = 0; j < lines.size(); j++)
{
int16_t y = top + Chalkboard::TILE * static_cast<int16_t>(j);

pieces[WEST].draw(at(left, y));

for (int16_t k = 0; k < tiles; k++)
pieces[CENTER].draw(at(left + Chalkboard::TILE * k, y));

pieces[EAST].draw(at(inner_right, y));
}

pieces[NORTHWEST].draw(at(left, top));
pieces[NORTHEAST].draw(at(inner_right, top));
pieces[SOUTHWEST].draw(at(left, inner_bottom));
pieces[SOUTHEAST].draw(at(inner_right, inner_bottom));

int16_t inner = Chalkboard::TILE * tiles;

for (size_t j = 0; j < lines.size(); j++)
{
int16_t x = left + (inner - lines[j].width()) / 2;
int16_t y = top + Chalkboard::TILE * static_cast<int16_t>(j) - TEXT_DESCENT;

lines[j].draw(at(x, y));
}

if (closable)
close[close_state].draw(DrawArgument(close_bounds(feet, avatar_height).get_left_top()));
}

bool ChalkboardBalloon::on_close(Point<int16_t> feet, int16_t avatar_height, Point<int16_t> cursor) const
{
return shown && closable && close_bounds(feet, avatar_height).contains(cursor);
}

bool ChalkboardBalloon::send_cursor(Point<int16_t> feet, int16_t avatar_height, Point<int16_t> cursor, bool pressed,
bool held)
{
bool inside = on_close(feet, avatar_height, cursor);
bool closed = false;

if (pressed)
{
armed = inside;
} else if (!held && armed)
{
armed = false;
closed = inside;
}

close_state = !inside ? NORMAL : armed && held ? PRESSED : MOUSEOVER;

return closed;
}

void ChalkboardBalloon::release_cursor()
{
armed = false;
}
}
74 changes: 74 additions & 0 deletions HeavenClient/Character/ChalkboardBalloon.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#pragma once

#include "../Graphics/Text.h"
#include "../Graphics/Texture.h"
#include "../Template/Rectangle.h"

#include <array>
#include <optional>
#include <string>
#include <vector>

namespace ms
{
// The board over someone using a chalkboard (v83 FUN_004890c4 with the
// ChatBalloon.img "adboard" style): white text on a tiled board that stays
// up until the server takes it down. The player's own board has a close
// button.
class ChalkboardBalloon
{
public:
ChalkboardBalloon();

// Shows `text`, or nothing.
void change(const std::optional<std::string> &text, bool closable);

bool is_shown() const;

const std::string &get_text() const;

void draw(Point<int16_t> feet, int16_t avatar_height) const;

// Tracks the close button under the cursor. True when a left click on
// it is released over it.
bool send_cursor(Point<int16_t> feet, int16_t avatar_height, Point<int16_t> cursor, bool pressed, bool held);

// Forgets a press on the close button whose release went elsewhere.
void release_cursor();

// Whether the cursor is over the close button.
bool on_close(Point<int16_t> feet, int16_t avatar_height, Point<int16_t> cursor) const;

private:
Point<int16_t> top_left(Point<int16_t> feet, int16_t avatar_height) const;

Rectangle<int16_t> close_bounds(Point<int16_t> feet, int16_t avatar_height) const;

// The board pieces, named in ChatBalloon.img as c, n, s, w, e, nw, ne, sw, se, and arrow.
enum Piece : uint8_t
{
CENTER, NORTH, SOUTH, WEST, EAST, NORTHWEST, NORTHEAST, SOUTHWEST, SOUTHEAST, ARROW, NUM_PIECES
};

enum CloseState : uint8_t
{
NORMAL, MOUSEOVER, PRESSED, NUM_STATES
};

bool shown;
bool closable;
bool armed;
CloseState close_state;
std::string text;
std::vector<Text> lines;
std::array<Texture, NUM_PIECES> pieces;
std::array<Texture, NUM_STATES> close;
// The frame's margins around the inner area.
int16_t left;
int16_t right;
int16_t top;
int16_t bottom;
int16_t tiles;
Point<int16_t> size;
};
}
29 changes: 29 additions & 0 deletions HeavenClient/Character/Char.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ namespace ms
guildmark.draw(guildpos + Point<int16_t>(-guildlabel.width() / 2, 0) + EMBLEM_OFFSET);
}

chalkboard.draw(absp, AVATAR_HEIGHT);
chatballoon.draw(absp - Point<int16_t>(0, 85));
hintballoon.draw(absp - Point<int16_t>(0, 85));
// v83 puts a room's balloon in front of the chat balloon.
Expand Down Expand Up @@ -693,6 +694,34 @@ namespace ms
return miniroom.serial_at(feet, AVATAR_HEIGHT, cursor);
}

void Char::set_chalkboard(const std::optional<std::string> &text, bool closable)
{
chalkboard.change(text, closable);
}

std::optional<std::string> Char::get_chalkboard() const
{
if (!chalkboard.is_shown())
return std::nullopt;

return chalkboard.get_text();
}

bool Char::send_chalkboard_cursor(Point<int16_t> feet, Point<int16_t> cursor, bool pressed, bool held)
{
return chalkboard.send_cursor(feet, AVATAR_HEIGHT, cursor, pressed, held);
}

void Char::release_chalkboard_cursor()
{
chalkboard.release_cursor();
}

bool Char::on_chalkboard_close(Point<int16_t> feet, Point<int16_t> cursor) const
{
return chalkboard.on_close(feet, AVATAR_HEIGHT, cursor);
}

CharLook &Char::get_look()
{
return look;
Expand Down
13 changes: 13 additions & 0 deletions HeavenClient/Character/Char.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

#include <cstdint>

#include "ChalkboardBalloon.h"
#include "CharEffect.h"
#include "GuildMark.h"
#include "MiniRoomBalloon.h"
Expand Down Expand Up @@ -242,6 +243,17 @@ namespace ms
// on the screen.
int32_t mini_room_at(Point<int16_t> feet, Point<int16_t> cursor) const;

// Puts up the character's chalkboard with `text`, or takes it down.
// Only the player's own board can be closed.
void set_chalkboard(const std::optional<std::string> &text, bool closable);
// The note on the character's chalkboard, if one is up.
std::optional<std::string> get_chalkboard() const;
// Tracks the chalkboard's close button, with the feet at `feet` on the
// screen. True when it was clicked.
bool send_chalkboard_cursor(Point<int16_t> feet, Point<int16_t> cursor, bool pressed, bool held);
void release_chalkboard_cursor();
bool on_chalkboard_close(Point<int16_t> feet, Point<int16_t> cursor) const;

// Return if the char is in the Char::State::SIT state
bool is_sitting() const;

Expand Down Expand Up @@ -310,6 +322,7 @@ namespace ms
Text namelabel;
Text guildlabel;
GuildMark guildmark;
ChalkboardBalloon chalkboard;
ChatBalloon chatballoon;
ChatBalloon hintballoon;
MiniRoomBalloon miniroom;
Expand Down
10 changes: 10 additions & 0 deletions HeavenClient/Character/Inventory/Pet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ namespace ms
closeness(closeness), fullness(fullness)
{}

int32_t Pet::get_item_id() const
{
return item_id;
}

const std::string &Pet::get_name() const
{
return petname;
}

uint8_t Pet::get_level() const
{
return petlevel;
Expand Down
Loading
Loading