feat: 최대 강화 단계, 현재 골드 기반 랭크 추가
This commit is contained in:
parent
30debc44f5
commit
71e09915b7
6 changed files with 197 additions and 9 deletions
|
|
@ -17,6 +17,10 @@ class PacketID(IntEnum):
|
|||
CS_RankingRequest = 40
|
||||
SC_RankingList = 41
|
||||
|
||||
class RankingType(IntEnum):
|
||||
LEVEL = 0
|
||||
GOLD = 1
|
||||
|
||||
class SwordGameClient(ctk.CTk):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
|
@ -57,6 +61,7 @@ class SwordGameClient(ctk.CTk):
|
|||
# 메인 레이아웃
|
||||
self.grid_columnconfigure(0, weight=1)
|
||||
self.grid_columnconfigure(1, weight=2)
|
||||
self.grid_columnconfigure(2, weight=1)
|
||||
self.grid_rowconfigure(0, weight=1)
|
||||
|
||||
# 왼쪽: 유저 정보 창
|
||||
|
|
@ -78,6 +83,9 @@ class SwordGameClient(ctk.CTk):
|
|||
self.sell_btn = ctk.CTkButton(self.info_frame, text="판매 하기", fg_color="#dc3545", hover_color="#c82333", command=self.send_sell)
|
||||
self.sell_btn.pack(pady=10, fill="x", padx=20)
|
||||
|
||||
self.ranking_btn = ctk.CTkButton(self.info_frame, text="랭킹 갱신", fg_color="#17a2b8", hover_color="#138496", command=self.request_rankings)
|
||||
self.ranking_btn.pack(pady=10, fill="x", padx=20)
|
||||
|
||||
# 오른쪽: 채팅 창
|
||||
self.chat_frame = ctk.CTkFrame(self)
|
||||
self.chat_frame.grid(row=0, column=1, padx=20, pady=20, sticky="nsew")
|
||||
|
|
@ -89,7 +97,24 @@ class SwordGameClient(ctk.CTk):
|
|||
self.chat_entry.pack(fill="x", padx=10, pady=(0, 10))
|
||||
self.chat_entry.bind("<Return>", lambda e: self.send_chat())
|
||||
|
||||
# TODO: 채팅창 오른 쪽에 랭킹?
|
||||
# 오른쪽: 랭킹 창
|
||||
self.ranking_frame = ctk.CTkFrame(self)
|
||||
self.ranking_frame.grid(row=0, column=2, padx=20, pady=20, sticky="nsew")
|
||||
|
||||
self.rank_title = ctk.CTkLabel(self.ranking_frame, text="실시간 랭킹", font=("Arial", 18, "bold"))
|
||||
self.rank_title.pack(pady=10)
|
||||
|
||||
self.rank_tab = ctk.CTkSegmentedButton(self.ranking_frame, values=["강화 단계", "자산"], command=self.on_rank_tab_changed)
|
||||
self.rank_tab.set("강화 단계")
|
||||
self.rank_tab.pack(pady=5, padx=10, fill="x")
|
||||
|
||||
self.rank_list_box = ctk.CTkTextbox(self.ranking_frame, state="disabled", font=("Courier New", 12))
|
||||
self.rank_list_box.pack(expand=True, fill="both", padx=10, pady=10)
|
||||
|
||||
self.my_rank_label = ctk.CTkLabel(self.ranking_frame, text="내 순위: -", font=("Arial", 14, "bold"), text_color="#FFD700")
|
||||
self.my_rank_label.pack(pady=10)
|
||||
|
||||
self.request_rankings()
|
||||
|
||||
def connect_to_server(self):
|
||||
self.nickname = self.nickname_entry.get()
|
||||
|
|
@ -171,6 +196,11 @@ class SwordGameClient(ctk.CTk):
|
|||
msg = payload.decode('utf-8')
|
||||
self.after(0, lambda: self.log_chat(msg))
|
||||
|
||||
elif pkt_id == PacketID.SC_RankingList:
|
||||
res = Protocol.SC_RankingList()
|
||||
res.ParseFromString(payload)
|
||||
self.after(0, lambda: self.update_ranking_ui(res))
|
||||
|
||||
def update_stats(self):
|
||||
self.level_label.configure(text=f"검 레벨: {self.current_level}")
|
||||
self.gold_label.configure(text=f"골드: {self.current_gold:,}")
|
||||
|
|
@ -194,6 +224,42 @@ class SwordGameClient(ctk.CTk):
|
|||
self.chat_box.see("end")
|
||||
self.chat_box.configure(state="disabled")
|
||||
|
||||
def request_rankings(self):
|
||||
tab = self.rank_tab.get()
|
||||
rtype = RankingType.LEVEL if tab == "강화 단계" else RankingType.GOLD
|
||||
pkt = Protocol.CS_RankingRequest()
|
||||
pkt.type = rtype
|
||||
self.send_packet(PacketID.CS_RankingRequest, pkt)
|
||||
|
||||
def on_rank_tab_changed(self, value):
|
||||
self.request_rankings()
|
||||
|
||||
def update_ranking_ui(self, res):
|
||||
self.rank_list_box.configure(state="normal")
|
||||
self.rank_list_box.delete("1.0", "end")
|
||||
|
||||
header = f"{'순위':<4} {'닉네임':<12} {'값':<10}\n"
|
||||
self.rank_list_box.insert("end", header)
|
||||
self.rank_list_box.insert("end", "-" * 30 + "\n")
|
||||
|
||||
for entry in res.entries:
|
||||
val_str = f"{entry.value:,}"
|
||||
if res.type == RankingType.LEVEL:
|
||||
val_str = f"{entry.value}단계"
|
||||
|
||||
line = f"{entry.rank:<4} {entry.nickname:<12} {val_str:<10}\n"
|
||||
self.rank_list_box.insert("end", line)
|
||||
|
||||
self.rank_list_box.configure(state="disabled")
|
||||
|
||||
if res.my_rank != -1:
|
||||
my_val_str = f"{res.my_value:,}"
|
||||
if res.type == RankingType.LEVEL:
|
||||
my_val_str = f"{res.my_value}단계"
|
||||
self.my_rank_label.configure(text=f"내 순위: {res.my_rank}위 ({my_val_str})")
|
||||
else:
|
||||
self.my_rank_label.configure(text="내 순위: 기록 없음")
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = SwordGameClient()
|
||||
app.mainloop()
|
||||
|
|
|
|||
|
|
@ -19,11 +19,25 @@ public:
|
|||
std::string nickname;
|
||||
uint64_t gold;
|
||||
uint32_t swordLevel;
|
||||
uint32_t maxSwordLevel;
|
||||
};
|
||||
|
||||
struct RankingResult {
|
||||
struct Entry {
|
||||
uint32_t rank;
|
||||
std::string nickname;
|
||||
uint64_t value;
|
||||
};
|
||||
std::vector<Entry> topEntries;
|
||||
int32_t myRank;
|
||||
uint64_t myValue;
|
||||
};
|
||||
|
||||
boost::asio::awaitable<UserData> LoadUser(std::string nickname);
|
||||
boost::asio::awaitable<void> SaveUser(std::string nickname, uint64_t gold,
|
||||
uint32_t swordLevel);
|
||||
boost::asio::awaitable<RankingResult> GetRanking(int type,
|
||||
std::string nickname);
|
||||
|
||||
private:
|
||||
DatabaseManager() = default;
|
||||
|
|
|
|||
|
|
@ -23,11 +23,24 @@ message SC_SellResult {
|
|||
uint64 total_gold = 2;
|
||||
}
|
||||
|
||||
enum RankingType {
|
||||
RANKING_TYPE_LEVEL = 0;
|
||||
RANKING_TYPE_GOLD = 1;
|
||||
}
|
||||
|
||||
message CS_RankingRequest {
|
||||
RankingType type = 1;
|
||||
}
|
||||
|
||||
message RankingEntry {
|
||||
string nickname = 1;
|
||||
uint32 level = 2;
|
||||
uint32 rank = 1;
|
||||
string nickname = 2;
|
||||
uint64 value = 3;
|
||||
}
|
||||
|
||||
message SC_RankingList {
|
||||
repeated RankingEntry entries = 1;
|
||||
RankingType type = 1;
|
||||
repeated RankingEntry entries = 2;
|
||||
int32 my_rank = 3;
|
||||
uint64 my_value = 4;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,5 +6,8 @@ CREATE TABLE IF NOT EXISTS users (
|
|||
username VARCHAR(32) UNIQUE NOT NULL,
|
||||
gold BIGINT UNSIGNED DEFAULT 10000,
|
||||
sword_level INT DEFAULT 0,
|
||||
last_login TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
max_sword_level INT DEFAULT 0,
|
||||
last_login TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX (gold),
|
||||
INDEX (max_sword_level)
|
||||
);
|
||||
|
|
|
|||
|
|
@ -87,7 +87,8 @@ DatabaseManager::LoadUser(std::string nickname) {
|
|||
|
||||
boost::mysql::results result;
|
||||
boost::mysql::statement stmt = co_await conn_->async_prepare_statement(
|
||||
"SELECT username, gold, sword_level FROM users WHERE username = ?",
|
||||
"SELECT username, gold, sword_level, max_sword_level FROM users WHERE "
|
||||
"username = ?",
|
||||
boost::asio::use_awaitable);
|
||||
co_await conn_->async_execute(stmt.bind(nickname), result,
|
||||
boost::asio::use_awaitable);
|
||||
|
|
@ -109,6 +110,7 @@ DatabaseManager::LoadUser(std::string nickname) {
|
|||
data.nickname = row.at(0).as_string();
|
||||
data.gold = row.at(1).as_uint64();
|
||||
data.swordLevel = (uint32_t)row.at(2).as_int64();
|
||||
data.maxSwordLevel = (uint32_t)row.at(3).as_int64();
|
||||
Unlock();
|
||||
co_return data;
|
||||
}
|
||||
|
|
@ -132,11 +134,12 @@ boost::asio::awaitable<void> DatabaseManager::SaveUser(std::string nickname,
|
|||
|
||||
boost::mysql::results result;
|
||||
boost::mysql::statement stmt = co_await conn_->async_prepare_statement(
|
||||
"UPDATE users SET gold = ?, sword_level = ? WHERE username = ?",
|
||||
"UPDATE users SET gold = ?, sword_level = ?, max_sword_level = "
|
||||
"GREATEST(max_sword_level, ?) WHERE username = ?",
|
||||
boost::asio::use_awaitable);
|
||||
co_await conn_->async_execute(
|
||||
stmt.bind(gold, (int64_t)swordLevel, nickname), result,
|
||||
boost::asio::use_awaitable);
|
||||
stmt.bind(gold, (int64_t)swordLevel, (int64_t)swordLevel, nickname),
|
||||
result, boost::asio::use_awaitable);
|
||||
} catch (const std::exception &e) {
|
||||
Logger::Log("SaveUser 에러: ", e.what());
|
||||
}
|
||||
|
|
@ -144,3 +147,67 @@ boost::asio::awaitable<void> DatabaseManager::SaveUser(std::string nickname,
|
|||
Unlock();
|
||||
co_return;
|
||||
}
|
||||
|
||||
boost::asio::awaitable<DatabaseManager::RankingResult>
|
||||
DatabaseManager::GetRanking(int type, std::string nickname) {
|
||||
co_await Lock();
|
||||
|
||||
RankingResult res;
|
||||
res.myRank = -1;
|
||||
res.myValue = 0;
|
||||
|
||||
try {
|
||||
if (!conn_) {
|
||||
Unlock();
|
||||
co_return res;
|
||||
}
|
||||
|
||||
std::string columnName = (type == 0) ? "max_sword_level" : "gold";
|
||||
|
||||
boost::mysql::results topResult;
|
||||
std::string topQuery = "SELECT username, " + columnName +
|
||||
" FROM users ORDER BY " + columnName +
|
||||
" DESC LIMIT 10";
|
||||
co_await conn_->async_execute(topQuery, topResult,
|
||||
boost::asio::use_awaitable);
|
||||
|
||||
uint32_t rank = 1;
|
||||
for (auto row : topResult.rows()) {
|
||||
RankingResult::Entry entry;
|
||||
entry.rank = rank++;
|
||||
entry.nickname = row.at(0).as_string();
|
||||
// DB 타입이 달라 분기처리..
|
||||
if (type == 0) {
|
||||
entry.value = (uint64_t)row.at(1).as_int64();
|
||||
} else {
|
||||
entry.value = row.at(1).as_uint64();
|
||||
}
|
||||
res.topEntries.push_back(entry);
|
||||
}
|
||||
|
||||
boost::mysql::results myResult;
|
||||
std::string myQuery =
|
||||
"SELECT " + columnName + ", (SELECT COUNT(*) + 1 FROM users WHERE " +
|
||||
columnName + " > u." + columnName + ") FROM users u WHERE username = ?";
|
||||
|
||||
boost::mysql::statement stmt = co_await conn_->async_prepare_statement(
|
||||
myQuery, boost::asio::use_awaitable);
|
||||
co_await conn_->async_execute(stmt.bind(nickname), myResult,
|
||||
boost::asio::use_awaitable);
|
||||
|
||||
if (!myResult.rows().empty()) {
|
||||
auto row = myResult.rows().at(0);
|
||||
if (type == 0) {
|
||||
res.myValue = (uint64_t)row.at(0).as_int64();
|
||||
} else {
|
||||
res.myValue = row.at(0).as_uint64();
|
||||
}
|
||||
res.myRank = (int32_t)row.at(1).as_int64();
|
||||
}
|
||||
} catch (const std::exception &e) {
|
||||
Logger::Log("GetRanking 에러: ", e.what());
|
||||
}
|
||||
|
||||
Unlock();
|
||||
co_return res;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -139,6 +139,31 @@ PacketHandler::HandlePacket(std::shared_ptr<Session> session,
|
|||
SessionManager::GetInstance().Broadcast(packet.header, packet.payload);
|
||||
} break;
|
||||
|
||||
case PacketID::CS_RankingRequest: {
|
||||
Protocol::CS_RankingRequest pkt;
|
||||
if (!pkt.ParseFromArray(packet.payload.data(), packet.payload.size())) {
|
||||
Logger::Log("랭킹 요청 패킷 파싱 실패");
|
||||
co_return;
|
||||
}
|
||||
|
||||
auto rankResult = co_await DatabaseManager::GetInstance().GetRanking(
|
||||
(int)pkt.type(), session->GetNickname());
|
||||
|
||||
Protocol::SC_RankingList res;
|
||||
res.set_type(pkt.type());
|
||||
res.set_my_rank(rankResult.myRank);
|
||||
res.set_my_value(rankResult.myValue);
|
||||
|
||||
for (const auto &entry : rankResult.topEntries) {
|
||||
auto *newEntry = res.add_entries();
|
||||
newEntry->set_rank(entry.rank);
|
||||
newEntry->set_nickname(entry.nickname);
|
||||
newEntry->set_value(entry.value);
|
||||
}
|
||||
|
||||
session->SendPacket(PacketID::SC_RankingList, res);
|
||||
} break;
|
||||
|
||||
default:
|
||||
Logger::Log("알 수 없는 패킷 ID: ", packet.header.id);
|
||||
break;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue