feat(game): claim challenge rewards and keep records
This commit is contained in:
1 parent
ab75080134
commit
38880e4976
7 files changed
+337
-41
No files matched your search
+196
-6
@@ -1,11 +1,13 @@
|
||||
#include "game/challenge.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <bit>
|
||||
#include <utility>
|
||||
|
||||
#include "core/config.h"
|
||||
#include "core/logger.h"
|
||||
#include "data/excel.h"
|
||||
#include "game/inventory.h"
|
||||
#include "game/player.h"
|
||||
#include "net/cmd_ids.h"
|
||||
#include "net/session.h"
|
||||
@@ -21,6 +23,19 @@ constexpr std::pair<uint32_t, uint32_t> kMaxLevels[] = {{1, 12}, {2, 4}, {3, 4}}
|
||||
|
||||
uint32_t half(uint32_t stage) { return stage > 1 ? 1u : 0u; }
|
||||
|
||||
uint32_t fullStars(const data::ChallengeInfo& floor) {
|
||||
return floor.targetIds.empty() ? 0 : (uint32_t{1} << floor.targetIds.size()) - 1;
|
||||
}
|
||||
|
||||
// The floor's stars as the history reports them: all of them under
|
||||
// unlock_all_challenges, otherwise the best the player has cleared it with.
|
||||
uint32_t floorStars(const Player* player, const data::ChallengeInfo& floor) {
|
||||
if (core::Config::get().gameplay.unlockAllChallenges) return fullStars(floor);
|
||||
if (player == nullptr) return 0;
|
||||
auto it = player->challengeRecords().find(floor.id);
|
||||
return it == player->challengeRecords().end() ? 0 : it->second.stars;
|
||||
}
|
||||
|
||||
const data::ChallengeInfo* configOf(const Player& player) {
|
||||
const ChallengeRun& run = player.challenge();
|
||||
if (!run.active) return nullptr;
|
||||
@@ -42,11 +57,39 @@ proto::ExtraLineupType lineupTypeOf(uint32_t stage) {
|
||||
: proto::ExtraLineupType::ExtraLineupType_LineupChallenge;
|
||||
}
|
||||
|
||||
// Keeps the run as the floor's record when it beats the one there: more stars first, then
|
||||
// fewer cycles for Memory of Chaos or a higher score for the other two.
|
||||
void recordClear(Player& player) {
|
||||
const data::ChallengeInfo* config = configOf(player);
|
||||
if (config == nullptr) return;
|
||||
const ChallengeRun& run = player.challenge();
|
||||
|
||||
ChallengeRecord candidate;
|
||||
candidate.stars = run.stars;
|
||||
candidate.roundsUsed = config->roundLimit > run.roundsLeft ? config->roundLimit - run.roundsLeft : 0;
|
||||
candidate.score = run.totalScore();
|
||||
for (uint32_t i = 0; i < 2; ++i) {
|
||||
candidate.buffs[i] = run.buffs[i];
|
||||
candidate.teams[i] = run.party[i];
|
||||
}
|
||||
|
||||
auto [it, fresh] = player.challengeRecords().try_emplace(config->id, candidate);
|
||||
if (fresh) return;
|
||||
const ChallengeRecord& best = it->second;
|
||||
int have = std::popcount(best.stars);
|
||||
int got = std::popcount(candidate.stars);
|
||||
bool better = got > have ||
|
||||
(got == have && (config->kind == data::ChallengeKind::Memory
|
||||
? candidate.roundsUsed < best.roundsUsed
|
||||
: candidate.score > best.score));
|
||||
if (better) it->second = candidate;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace challenge {
|
||||
|
||||
proto::GetChallengeScRsp history() {
|
||||
proto::GetChallengeScRsp history(const Player* player) {
|
||||
const data::Tables& tables = data::Tables::get();
|
||||
bool cleared = core::Config::get().gameplay.unlockAllChallenges;
|
||||
|
||||
@@ -56,17 +99,20 @@ proto::GetChallengeScRsp history() {
|
||||
for (const data::ChallengeInfo& floor : tables.challenges()) {
|
||||
proto::Challenge entry;
|
||||
entry.challenge_id = floor.id;
|
||||
if (cleared && !floor.targetIds.empty()) {
|
||||
entry.star = (uint32_t{1} << floor.targetIds.size()) - 1;
|
||||
entry.taken_reward = entry.star;
|
||||
}
|
||||
entry.star = floorStars(player, floor);
|
||||
entry.taken_reward = entry.star;
|
||||
rsp.challenge_list.push_back(entry);
|
||||
}
|
||||
|
||||
for (const data::ChallengeGroupInfo& season : tables.challengeGroups()) {
|
||||
proto::ChallengeGroup group;
|
||||
group.group_id = season.id;
|
||||
if (cleared) {
|
||||
if (player != nullptr) {
|
||||
// Only what has actually been claimed; the rest shows as a reward to collect.
|
||||
const auto& taken = player->challengeRewardsTaken();
|
||||
auto it = taken.find(season.id);
|
||||
group.taken_stars_count_reward = it == taken.end() ? 0 : it->second;
|
||||
} else if (cleared) {
|
||||
group.taken_stars_count_reward = tables.challengeRewardStars(season.rewardLineGroupId);
|
||||
}
|
||||
rsp.challenge_group_list.push_back(group);
|
||||
@@ -149,6 +195,146 @@ bool enterArena(Player& player, proto::SceneInfo& out) {
|
||||
return scene::load(player, entryId, 0, true, out, filter);
|
||||
}
|
||||
|
||||
proto::TakeChallengeRewardScRsp takeRewards(Player& player, uint32_t groupId,
|
||||
std::vector<data::ItemStack>& granted) {
|
||||
const data::Tables& tables = data::Tables::get();
|
||||
proto::TakeChallengeRewardScRsp rsp;
|
||||
rsp.retcode = 0;
|
||||
rsp.group_id = groupId;
|
||||
granted.clear();
|
||||
|
||||
const data::ChallengeGroupInfo* season = tables.challengeGroup(groupId);
|
||||
const std::vector<data::ChallengeRewardLine>* line =
|
||||
season != nullptr ? tables.challengeRewardLine(season->rewardLineGroupId) : nullptr;
|
||||
if (line == nullptr) {
|
||||
rsp.OAKNHADPLJD.emplace();
|
||||
return rsp;
|
||||
}
|
||||
|
||||
uint32_t earned = 0;
|
||||
for (const data::ChallengeInfo& floor : tables.challenges()) {
|
||||
if (floor.groupId == groupId) earned += std::popcount(floorStars(&player, floor));
|
||||
}
|
||||
|
||||
uint64_t& taken = player.challengeRewardsTaken()[groupId];
|
||||
std::vector<data::ItemStack> all;
|
||||
for (const data::ChallengeRewardLine& step : *line) {
|
||||
uint64_t bit = uint64_t{1} << step.stars;
|
||||
if (step.stars > earned || (taken & bit) != 0) continue;
|
||||
taken |= bit;
|
||||
std::vector<data::ItemStack> items;
|
||||
if (const data::RewardInfo* reward = tables.reward(step.rewardId)) {
|
||||
items = inventory::rewardItems(*reward);
|
||||
}
|
||||
proto::TakenChallengeRewardInfo info;
|
||||
info.star_count = step.stars;
|
||||
info.reward.emplace() = inventory::itemList(items);
|
||||
rsp.taken_reward_list.push_back(std::move(info));
|
||||
all.insert(all.end(), items.begin(), items.end());
|
||||
}
|
||||
|
||||
granted = inventory::merged(all);
|
||||
inventory::grant(player, granted);
|
||||
rsp.OAKNHADPLJD.emplace() = inventory::itemList(granted);
|
||||
return rsp;
|
||||
}
|
||||
|
||||
proto::GetChallengeGroupStatisticsScRsp statistics(const Player& player, uint32_t groupId) {
|
||||
const data::Tables& tables = data::Tables::get();
|
||||
proto::GetChallengeGroupStatisticsScRsp rsp;
|
||||
rsp.retcode = 0;
|
||||
rsp.group_id = groupId;
|
||||
|
||||
const data::ChallengeGroupInfo* season = tables.challengeGroup(groupId);
|
||||
data::ChallengeKind kind = season != nullptr ? season->kind : data::ChallengeKind::Memory;
|
||||
|
||||
// The highest floor cleared, then the most stars, then the fewest cycles or best score.
|
||||
const data::ChallengeInfo* bestFloor = nullptr;
|
||||
const ChallengeRecord* best = nullptr;
|
||||
for (const data::ChallengeInfo& floor : tables.challenges()) {
|
||||
if (floor.groupId != groupId) continue;
|
||||
auto it = player.challengeRecords().find(floor.id);
|
||||
if (it == player.challengeRecords().end() || it->second.stars == 0) continue;
|
||||
const ChallengeRecord& record = it->second;
|
||||
bool better = best == nullptr || floor.floor > bestFloor->floor;
|
||||
if (!better && floor.floor == bestFloor->floor) {
|
||||
int have = std::popcount(best->stars);
|
||||
int got = std::popcount(record.stars);
|
||||
better = got > have || (got == have && (kind == data::ChallengeKind::Memory
|
||||
? record.roundsUsed < best->roundsUsed
|
||||
: record.score > best->score));
|
||||
}
|
||||
if (better) {
|
||||
best = &record;
|
||||
bestFloor = &floor;
|
||||
}
|
||||
}
|
||||
|
||||
Roster roster = player.roster();
|
||||
auto lineups = [&](std::vector<proto::ChallengeLineupList>& out) {
|
||||
for (const std::vector<uint32_t>& team : best->teams) {
|
||||
proto::ChallengeLineupList list;
|
||||
uint32_t index = 0;
|
||||
for (uint32_t avatarId : team) {
|
||||
proto::ChallengeAvatarInfo avatar;
|
||||
avatar.avatar_type = proto::AvatarType::AvatarType_AvatarFormalType;
|
||||
avatar.id = avatarId;
|
||||
const Avatar* source = roster.find(roster.resolvePath(avatarId));
|
||||
avatar.level = source != nullptr ? source->level : 1;
|
||||
avatar.index = index++;
|
||||
list.avatar_list.push_back(avatar);
|
||||
}
|
||||
out.push_back(std::move(list));
|
||||
}
|
||||
};
|
||||
|
||||
// The oneof has to be set to the arm that matches the season, or the client reads the
|
||||
// wrong one and throws; the record inside stays absent until a floor is cleared.
|
||||
switch (kind) {
|
||||
case data::ChallengeKind::Story: {
|
||||
auto& stats = rsp.challenge_story.emplace();
|
||||
rsp.EDKOHAAMONH_case = proto::GetChallengeGroupStatisticsScRsp::k_challenge_story;
|
||||
if (best == nullptr) break;
|
||||
stats.record_id = bestFloor->id;
|
||||
auto& record = stats.PPBHLLOJNEK.emplace();
|
||||
record.level = bestFloor->floor;
|
||||
record.EEJCPNAEKLJ = static_cast<uint32_t>(std::popcount(best->stars));
|
||||
record.buff_one = best->buffs[0];
|
||||
record.buff_two = best->buffs[1];
|
||||
record.score_id = best->score;
|
||||
lineups(record.lineup_list);
|
||||
break;
|
||||
}
|
||||
case data::ChallengeKind::Boss: {
|
||||
auto& stats = rsp.challenge_boss.emplace();
|
||||
rsp.EDKOHAAMONH_case = proto::GetChallengeGroupStatisticsScRsp::k_challenge_boss;
|
||||
if (best == nullptr) break;
|
||||
stats.record_id = bestFloor->id;
|
||||
auto& record = stats.PPBHLLOJNEK.emplace();
|
||||
record.level = bestFloor->floor;
|
||||
record.EEJCPNAEKLJ = static_cast<uint32_t>(std::popcount(best->stars));
|
||||
record.buff_one = best->buffs[0];
|
||||
record.buff_two = best->buffs[1];
|
||||
record.score_id = best->score;
|
||||
lineups(record.lineup_list);
|
||||
break;
|
||||
}
|
||||
case data::ChallengeKind::Memory: {
|
||||
auto& stats = rsp.challenge_default.emplace();
|
||||
rsp.EDKOHAAMONH_case = proto::GetChallengeGroupStatisticsScRsp::k_challenge_default;
|
||||
if (best == nullptr) break;
|
||||
stats.record_id = bestFloor->id;
|
||||
auto& record = stats.PPBHLLOJNEK.emplace();
|
||||
record.level = bestFloor->floor;
|
||||
record.EEJCPNAEKLJ = static_cast<uint32_t>(std::popcount(best->stars));
|
||||
record.round_count = best->roundsUsed;
|
||||
lineups(record.lineup_list);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return rsp;
|
||||
}
|
||||
|
||||
uint32_t stars(const Player& player) {
|
||||
const data::ChallengeInfo* config = configOf(player);
|
||||
if (config == nullptr) return 0;
|
||||
@@ -195,6 +381,10 @@ void settle(net::Session& session, Player& player, bool win) {
|
||||
run.status = win ? proto::ChallengeStatus::CHALLENGE_FINISH
|
||||
: proto::ChallengeStatus::CHALLENGE_FAILED;
|
||||
run.stars = win ? stars(player) : 0;
|
||||
if (win) {
|
||||
recordClear(player);
|
||||
player.saveNow();
|
||||
}
|
||||
|
||||
proto::ChallengeSettleNotify notify;
|
||||
notify.challenge_id = run.challengeId;
|
||||
|
||||
+12
-1
@@ -1,7 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include "data/excel.h"
|
||||
#include "game/scene.h"
|
||||
#include "proto/gen/protos.h"
|
||||
|
||||
@@ -20,7 +22,16 @@ namespace challenge {
|
||||
|
||||
// The player's challenge history: every floor of every mode, with its stars and its
|
||||
// season's rewards. Reports a full clear when gameplay.unlock_all_challenges is set.
|
||||
proto::GetChallengeScRsp history();
|
||||
// Without a player every reward reads as claimed.
|
||||
proto::GetChallengeScRsp history(const Player* player = nullptr);
|
||||
|
||||
// Pays out every star reward of the season that is earned and not yet claimed, returning
|
||||
// what was granted in `granted` so the caller can sync it.
|
||||
proto::TakeChallengeRewardScRsp takeRewards(Player& player, uint32_t groupId,
|
||||
std::vector<data::ItemStack>& granted);
|
||||
|
||||
// The season panel: the best cleared floor of `groupId`, with the teams that cleared it.
|
||||
proto::GetChallengeGroupStatisticsScRsp statistics(const Player& player, uint32_t groupId);
|
||||
|
||||
// The run in progress, as the client models it. Meaningless when none is.
|
||||
proto::CurChallenge current(const Player& player);
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
//
|
||||
// The history goes out as a full clear: star is a bitmask over the floor's
|
||||
// ChallengeTargetID entries, and the client locks a floor whose predecessor has none, so
|
||||
// an empty GetChallengeScRsp leaves every mode showing floor 1 and nothing else. Rewards
|
||||
// are reported as already taken -- there is no inventory to pay them into, and a
|
||||
// claimable reward would be a button that does nothing.
|
||||
// an empty GetChallengeScRsp leaves every mode showing floor 1 and nothing else. A
|
||||
// season's star rewards are claimable once earned and pay out into the inventory.
|
||||
//
|
||||
// Running a floor is the rest of this file. The client sends both teams with the start
|
||||
// request, so there is no separate lineup to edit: the run holds them, the arena is the
|
||||
@@ -14,6 +13,7 @@
|
||||
#include "data/excel.h"
|
||||
#include "game/challenge.h"
|
||||
#include "game/handlers.h"
|
||||
#include "game/inventory.h"
|
||||
#include "game/player.h"
|
||||
#include "game/scene.h"
|
||||
#include "net/cmd_ids.h"
|
||||
@@ -47,7 +47,7 @@ std::vector<uint32_t> teamOf(const std::vector<proto::AvatarIdentifier>& identif
|
||||
}
|
||||
|
||||
void onGetChallenge(net::Session& session, const proto::GetChallengeCsReq&) {
|
||||
session.send(cmd::GetChallengeScRsp, challenge::history());
|
||||
session.send(cmd::GetChallengeScRsp, challenge::history(session.player()));
|
||||
}
|
||||
|
||||
void onStartChallenge(net::Session& session, const proto::StartChallengeCsReq& req) {
|
||||
@@ -183,37 +183,21 @@ void onLeaveChallenge(net::Session& session, const proto::LeaveChallengeCsReq&)
|
||||
|
||||
void onGetChallengeGroupStatistics(net::Session& session,
|
||||
const proto::GetChallengeGroupStatisticsCsReq& req) {
|
||||
// Nobody's records are kept, but the oneof has to be set to the shape that matches
|
||||
// the season or the client reads the wrong arm of it and throws.
|
||||
proto::GetChallengeGroupStatisticsScRsp rsp;
|
||||
rsp.retcode = 0;
|
||||
rsp.group_id = req.group_id;
|
||||
|
||||
const data::ChallengeGroupInfo* season = data::Tables::get().challengeGroup(req.group_id);
|
||||
data::ChallengeKind kind = season != nullptr ? season->kind : data::ChallengeKind::Memory;
|
||||
switch (kind) {
|
||||
case data::ChallengeKind::Story:
|
||||
rsp.challenge_story.emplace();
|
||||
rsp.EDKOHAAMONH_case = proto::GetChallengeGroupStatisticsScRsp::k_challenge_story;
|
||||
break;
|
||||
case data::ChallengeKind::Boss:
|
||||
rsp.challenge_boss.emplace();
|
||||
rsp.EDKOHAAMONH_case = proto::GetChallengeGroupStatisticsScRsp::k_challenge_boss;
|
||||
break;
|
||||
case data::ChallengeKind::Memory:
|
||||
rsp.challenge_default.emplace();
|
||||
rsp.EDKOHAAMONH_case = proto::GetChallengeGroupStatisticsScRsp::k_challenge_default;
|
||||
break;
|
||||
}
|
||||
session.send(cmd::GetChallengeGroupStatisticsScRsp, rsp);
|
||||
Player* player = playerOf(session, "GetChallengeGroupStatistics");
|
||||
if (player == nullptr) return;
|
||||
session.send(cmd::GetChallengeGroupStatisticsScRsp, challenge::statistics(*player, req.group_id));
|
||||
}
|
||||
|
||||
void onTakeChallengeReward(net::Session& session, const proto::TakeChallengeRewardCsReq& req) {
|
||||
// The history already reports every reward as taken, so there is nothing left to
|
||||
// hand over -- but the request still has to complete or the screen hangs.
|
||||
proto::TakeChallengeRewardScRsp rsp;
|
||||
rsp.retcode = 0;
|
||||
rsp.group_id = req.group_id;
|
||||
Player* player = playerOf(session, "TakeChallengeReward");
|
||||
if (player == nullptr) return;
|
||||
|
||||
std::vector<data::ItemStack> granted;
|
||||
proto::TakeChallengeRewardScRsp rsp = challenge::takeRewards(*player, req.group_id, granted);
|
||||
if (!granted.empty()) {
|
||||
session.send(cmd::PlayerSyncScNotify, inventory::sync(*player, granted));
|
||||
player->saveNow();
|
||||
}
|
||||
session.send(cmd::TakeChallengeRewardScRsp, rsp);
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,15 @@ struct Inventory {
|
||||
std::map<uint32_t, uint32_t> items;
|
||||
};
|
||||
|
||||
// The best clear of one challenge floor. Saved; the season statistics panel shows it.
|
||||
struct ChallengeRecord {
|
||||
uint32_t stars = 0; // bitmask over the floor's targets
|
||||
uint32_t roundsUsed = 0;
|
||||
uint32_t score = 0;
|
||||
uint32_t buffs[2] = {0, 0};
|
||||
std::vector<uint32_t> teams[2];
|
||||
};
|
||||
|
||||
// A challenge run in progress. Not persisted: leaving the client mid-floor drops the
|
||||
// run, which is what the real server does with an expired season anyway.
|
||||
struct ChallengeRun {
|
||||
@@ -191,6 +200,12 @@ public:
|
||||
const PeakProgress& peakProgress() const { return peakProgress_; }
|
||||
GachaProgress& gacha() { return gacha_; }
|
||||
const GachaProgress& gacha() const { return gacha_; }
|
||||
// Keyed by floor id.
|
||||
std::map<uint32_t, ChallengeRecord>& challengeRecords() { return challengeRecords_; }
|
||||
const std::map<uint32_t, ChallengeRecord>& challengeRecords() const { return challengeRecords_; }
|
||||
// Season id -> bitmask over the star counts whose reward has been claimed.
|
||||
std::map<uint32_t, uint64_t>& challengeRewardsTaken() { return challengeRewardsTaken_; }
|
||||
const std::map<uint32_t, uint64_t>& challengeRewardsTaken() const { return challengeRewardsTaken_; }
|
||||
|
||||
uint32_t mainCharacter() const { return mainCharacter_; }
|
||||
void setMainCharacter(uint32_t value) { mainCharacter_ = value; }
|
||||
@@ -230,6 +245,8 @@ private:
|
||||
TierceRun tierce_;
|
||||
std::map<uint32_t, TierceProgress> tierceHistory_;
|
||||
GachaProgress gacha_;
|
||||
std::map<uint32_t, ChallengeRecord> challengeRecords_;
|
||||
std::map<uint32_t, uint64_t> challengeRewardsTaken_;
|
||||
|
||||
uint32_t mainCharacter_ = 8008;
|
||||
uint32_t marchType_ = 1224;
|
||||
|
||||
@@ -214,6 +214,35 @@ bool loadPlayerState(Player& player) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (auto floors = j.find("challenge_records"); floors != j.end() && floors->is_array()) {
|
||||
for (const json& entry : *floors) {
|
||||
uint32_t id = entry.is_object() ? u32(entry, "id", 0) : 0;
|
||||
if (id == 0) continue;
|
||||
ChallengeRecord& record = player.challengeRecords()[id];
|
||||
record.stars = u32(entry, "stars", 0);
|
||||
record.roundsUsed = u32(entry, "rounds_used", 0);
|
||||
record.score = u32(entry, "score", 0);
|
||||
std::vector<uint32_t> buffs = u32Array(entry, "buffs");
|
||||
for (size_t i = 0; i < buffs.size() && i < 2; ++i) record.buffs[i] = buffs[i];
|
||||
if (auto teams = entry.find("teams"); teams != entry.end() && teams->is_array()) {
|
||||
for (size_t i = 0; i < teams->size() && i < 2; ++i) {
|
||||
for (const json& avatar : (*teams)[i]) {
|
||||
if (avatar.is_number_unsigned()) record.teams[i].push_back(avatar.get<uint32_t>());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (auto taken = j.find("challenge_rewards_taken"); taken != j.end() && taken->is_array()) {
|
||||
for (const json& entry : *taken) {
|
||||
uint32_t group = entry.is_object() ? u32(entry, "group", 0) : 0;
|
||||
auto stars = entry.is_object() ? entry.find("stars") : entry.end();
|
||||
if (group == 0 || stars == entry.end() || !stars->is_number_unsigned()) continue;
|
||||
player.challengeRewardsTaken()[group] = stars->get<uint64_t>();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -313,6 +342,23 @@ std::string playerStateJson(const Player& player) {
|
||||
{"mcoin", bag.mcoin},
|
||||
{"items", items}};
|
||||
|
||||
json floors = json::array();
|
||||
for (const auto& [id, record] : player.challengeRecords()) {
|
||||
floors.push_back({{"id", id},
|
||||
{"stars", record.stars},
|
||||
{"rounds_used", record.roundsUsed},
|
||||
{"score", record.score},
|
||||
{"buffs", json::array({record.buffs[0], record.buffs[1]})},
|
||||
{"teams", json::array({record.teams[0], record.teams[1]})}});
|
||||
}
|
||||
j["challenge_records"] = floors;
|
||||
|
||||
json taken = json::array();
|
||||
for (const auto& [group, stars] : player.challengeRewardsTaken()) {
|
||||
taken.push_back({{"group", group}, {"stars", stars}});
|
||||
}
|
||||
j["challenge_rewards_taken"] = taken;
|
||||
|
||||
return j.dump(2);
|
||||
}
|
||||
|
||||
|
||||
@@ -1096,6 +1096,14 @@ void runFlowTests() {
|
||||
bagAfter.material_list.size() > 2,
|
||||
"the drops are in the bag");
|
||||
|
||||
proto::TakeChallengeRewardCsReq claim;
|
||||
claim.group_id = 100;
|
||||
client.send(cmd::TakeChallengeRewardCsReq, claim);
|
||||
proto::TakeChallengeRewardScRsp claimed;
|
||||
check(client.await(cmd::TakeChallengeRewardScRsp, packet) && parseBody(packet, claimed) &&
|
||||
claimed.retcode == 0 && !claimed.taken_reward_list.empty(),
|
||||
"season rewards can be claimed");
|
||||
|
||||
// An unimplemented request still has to complete, or the client hangs on it. This
|
||||
// one has no handler at all, so it exercises the name-derived fallback.
|
||||
client.sendEmpty(cmd::GetStarFightDataCsReq);
|
||||
|
||||
+42
-2
@@ -491,13 +491,53 @@ void testInventory() {
|
||||
sync.material_list[0].num == 3 && sync.basic_info.has(),
|
||||
"a sync names bag totals and carries the wallet");
|
||||
|
||||
// Season rewards: paid once, and the history marks them.
|
||||
bool unlocked = core::Config::get().gameplay.unlockAllChallenges;
|
||||
core::Config::get().gameplay.unlockAllChallenges = true;
|
||||
uint32_t before = bag.hcoin;
|
||||
std::vector<data::ItemStack> granted;
|
||||
proto::TakeChallengeRewardScRsp first = game::challenge::takeRewards(player, 100, granted);
|
||||
check(!first.taken_reward_list.empty() && !granted.empty() && bag.hcoin > before,
|
||||
"a cleared season's rewards pay out");
|
||||
std::vector<data::ItemStack> again;
|
||||
proto::TakeChallengeRewardScRsp second = game::challenge::takeRewards(player, 100, again);
|
||||
check(second.taken_reward_list.empty() && again.empty(), "and only once");
|
||||
const data::ChallengeGroupInfo* season = tables.challengeGroup(100);
|
||||
uint64_t line = season != nullptr ? tables.challengeRewardStars(season->rewardLineGroupId) : 0;
|
||||
uint64_t marked = 0;
|
||||
for (const proto::ChallengeGroup& group : game::challenge::history(&player).challenge_group_list) {
|
||||
if (group.group_id == 100) marked = group.taken_stars_count_reward;
|
||||
}
|
||||
check(marked != 0 && (marked & ~line) == 0, "the history marks what was claimed");
|
||||
core::Config::get().gameplay.unlockAllChallenges = unlocked;
|
||||
|
||||
// A cleared floor shows in its season's statistics.
|
||||
game::ChallengeRecord& record = player.challengeRecords()[1];
|
||||
record.stars = 7;
|
||||
record.roundsUsed = 12;
|
||||
record.teams[0] = {1001, 1002};
|
||||
record.teams[1] = {1003, 1004};
|
||||
proto::GetChallengeGroupStatisticsScRsp stats = game::challenge::statistics(player, 100);
|
||||
check(stats.challenge_default.has() && stats.challenge_default->record_id == 1 &&
|
||||
stats.challenge_default->PPBHLLOJNEK.has() &&
|
||||
stats.challenge_default->PPBHLLOJNEK->EEJCPNAEKLJ == 3 &&
|
||||
stats.challenge_default->PPBHLLOJNEK->round_count == 12 &&
|
||||
stats.challenge_default->PPBHLLOJNEK->lineup_list.size() == 2,
|
||||
"a season's best clear shows in its statistics");
|
||||
proto::GetChallengeGroupStatisticsScRsp none = game::challenge::statistics(player, 101);
|
||||
check(none.challenge_default.has() && !none.challenge_default->PPBHLLOJNEK.has(),
|
||||
"and a season with no clear shows none");
|
||||
|
||||
std::string realPath = core::Config::get().paths.playerFile;
|
||||
core::Config::get().paths.playerFile = "build/test-inventory-player.json";
|
||||
check(game::savePlayerState(player), "the inventory saves");
|
||||
game::Player loaded(1);
|
||||
check(game::loadPlayerState(loaded), "and loads");
|
||||
check(loaded.inventory().items[211] == 3 && loaded.inventory().reserveStamina == bag.reserveStamina,
|
||||
"the bag survives a restart");
|
||||
check(loaded.inventory().items[211] == 3 && loaded.inventory().reserveStamina == bag.reserveStamina &&
|
||||
loaded.challengeRecords().count(1) == 1 &&
|
||||
loaded.challengeRecords().at(1).teams[1].size() == 2 &&
|
||||
!loaded.challengeRewardsTaken().empty(),
|
||||
"bag, records and claimed rewards survive a restart");
|
||||
core::Config::get().paths.playerFile = realPath;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user