feat(game): add stamina, drops and sweeps
This commit is contained in:
1 parent
5b58096dcf
commit
ab75080134
12 files changed
+744
-41
No files matched your search
+36
-1
@@ -3,6 +3,7 @@
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
@@ -211,8 +212,11 @@ bool parseBody(const net::Packet& packet, T& out) {
|
||||
} // namespace
|
||||
|
||||
void runFlowTests() {
|
||||
// Keep the test off the real save file.
|
||||
// Keep the test off the real save file, and start it fresh: stamina and claimed
|
||||
// rewards carry over between runs otherwise.
|
||||
core::Config::get().paths.playerFile = "build/test-player.json";
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(core::Config::get().paths.playerFile, ec);
|
||||
game::registerAllHandlers();
|
||||
|
||||
net::Gateway gateway;
|
||||
@@ -1061,6 +1065,37 @@ void runFlowTests() {
|
||||
"a custom 50/50 pool is refused with a body");
|
||||
core::Config::get().gameplay.limitedWarpPools = configuredPools;
|
||||
|
||||
// Stamina, farming and the bag.
|
||||
client.sendEmpty(cmd::ExchangeStaminaCsReq);
|
||||
proto::ExchangeStaminaScRsp exchange;
|
||||
check(client.await(cmd::ExchangeStaminaScRsp, packet) && parseBody(packet, exchange) &&
|
||||
exchange.retcode == 0 && exchange.stamina_add == 60 && exchange.item_cost_list.size() == 1,
|
||||
"stamina can be bought with jade");
|
||||
|
||||
proto::CocoonSweepCsReq sweep;
|
||||
sweep.cocoon_id = 1001;
|
||||
sweep.world_level = 6;
|
||||
client.send(cmd::CocoonSweepCsReq, sweep);
|
||||
proto::CocoonSweepScRsp swept;
|
||||
check(client.await(cmd::CocoonSweepScRsp, packet) && parseBody(packet, swept) && swept.retcode == 0 &&
|
||||
swept.NCEFJFOHGBI > 0 && swept.multiple_drop_data.has() &&
|
||||
!swept.multiple_drop_data->item_list.empty(),
|
||||
"a calyx sweep spends the stamina and drops items");
|
||||
check(client.sawNotify(cmd::StaminaInfoScNotify) && client.sawNotify(cmd::PlayerSyncScNotify),
|
||||
"and tells the client about both");
|
||||
|
||||
client.send(cmd::CocoonSweepCsReq, sweep);
|
||||
proto::CocoonSweepScRsp dry;
|
||||
check(client.await(cmd::CocoonSweepScRsp, packet) && parseBody(packet, dry) &&
|
||||
dry.retcode == static_cast<uint32_t>(proto::Retcode::RET_LACK_STAMINA),
|
||||
"with no stamina left the next sweep is refused");
|
||||
|
||||
client.sendEmpty(cmd::GetBagCsReq);
|
||||
proto::GetBagScRsp bagAfter;
|
||||
check(client.await(cmd::GetBagScRsp, packet) && parseBody(packet, bagAfter) &&
|
||||
bagAfter.material_list.size() > 2,
|
||||
"the drops are in the bag");
|
||||
|
||||
// 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);
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include "game/battle.h"
|
||||
#include "game/challenge.h"
|
||||
#include "game/gacha.h"
|
||||
#include "game/inventory.h"
|
||||
#include "game/lineup.h"
|
||||
#include "game/player.h"
|
||||
#include "game/player_store.h"
|
||||
@@ -413,6 +414,93 @@ void testGacha() {
|
||||
core::Config::get().paths.playerFile = realPath;
|
||||
}
|
||||
|
||||
void testInventory() {
|
||||
const data::Tables& tables = data::Tables::get();
|
||||
if (!tables.loaded()) {
|
||||
std::printf("SKIP inventory (no data source configured)\n");
|
||||
return;
|
||||
}
|
||||
namespace inventory = game::inventory;
|
||||
|
||||
const data::StaminaRules& rules = tables.staminaRules();
|
||||
check(rules.max == 300 && rules.recoverSeconds == 360 && rules.reserveMax == 2400 &&
|
||||
rules.reserveRecoverSeconds == 1080,
|
||||
"stamina rules come from the tables");
|
||||
check(tables.staminaPrices().size() == 8 && tables.staminaPrices()[0] == 50 &&
|
||||
tables.staminaPerPurchase() == 60,
|
||||
"and the stamina prices");
|
||||
|
||||
game::Player player(1);
|
||||
game::Inventory& bag = player.inventory();
|
||||
bag.stamina = 100;
|
||||
bag.staminaUpdatedAt = 1000;
|
||||
inventory::refreshStamina(player, 1000 + 360 * 5 + 10);
|
||||
check(bag.stamina == 105 && bag.staminaUpdatedAt == 1000 + 360 * 5,
|
||||
"stamina recovers a point every six minutes");
|
||||
check(inventory::nextRecoverTime(player) == 1000 + 360 * 6, "and says when the next is due");
|
||||
|
||||
bag.stamina = 299;
|
||||
bag.staminaUpdatedAt = 5000;
|
||||
bag.reserveStamina = 0;
|
||||
inventory::refreshStamina(player, 5000 + 360 + 1080 * 2);
|
||||
check(bag.stamina == 300 && bag.reserveStamina == 2, "once full, time runs into reserve stamina");
|
||||
check(inventory::nextRecoverTime(player) == 0, "and nothing more is due");
|
||||
|
||||
inventory::chargeStamina(player, 40, 9000);
|
||||
check(bag.stamina == 260 && bag.staminaUpdatedAt == 9000, "spending from full starts the clock");
|
||||
|
||||
uint32_t jade = bag.hcoin;
|
||||
uint32_t credits = bag.scoin;
|
||||
inventory::grant(player, {{1, 50}, {2, 1000}, {211, 3}, {22, 150}, {11, 60}});
|
||||
check(bag.hcoin == jade + 50 && bag.scoin == credits + 1000, "jade and credits go to the wallet");
|
||||
check(bag.items[211] == 3 && bag.stamina == 320, "materials to the bag, stamina to stamina");
|
||||
check(bag.items.count(22) == 0, "and trailblaze exp nowhere");
|
||||
|
||||
const data::MappingInfo* calyx = inventory::dropTable(1001, 6);
|
||||
check(calyx != nullptr && !calyx->display.empty(), "a calyx has a drop table at world level 6");
|
||||
if (calyx != nullptr) {
|
||||
bool credit = false;
|
||||
for (const data::ItemStack& drop : inventory::rollDrops(*calyx, [] { return 0.5; })) {
|
||||
credit |= drop.id == 2 && drop.num > 0;
|
||||
}
|
||||
check(credit, "and a run drops credits");
|
||||
}
|
||||
const data::MappingInfo* shadow = inventory::dropTable(1101, 6);
|
||||
check(shadow != nullptr && shadow->worldLevel == 5, "a shadow's empty level falls back a level");
|
||||
if (shadow != nullptr) {
|
||||
bool boss = false;
|
||||
for (const data::ItemStack& drop : inventory::rollDrops(*shadow, [] { return 0.5; })) {
|
||||
boss |= drop.id == 110406 && drop.num == 5;
|
||||
}
|
||||
check(boss, "and its boss material scales with the world level");
|
||||
}
|
||||
check(inventory::dropTable(0, 6) == nullptr, "no table, no drops");
|
||||
|
||||
const data::RewardInfo* reward = tables.reward(101101);
|
||||
check(reward != nullptr, "RewardData loads");
|
||||
if (reward != nullptr) {
|
||||
std::vector<data::ItemStack> items = inventory::rewardItems(*reward);
|
||||
check(items.size() == 2 && items[0].id == 1 && items[0].num == 200 && items[1].id == 2 &&
|
||||
items[1].num == 20000,
|
||||
"a reward carries its jade and items");
|
||||
}
|
||||
std::vector<data::ItemStack> merged = inventory::merged({{2, 10}, {211, 1}, {2, 5}});
|
||||
check(merged.size() == 2 && merged[0].num == 15, "repeats merge");
|
||||
proto::PlayerSyncScNotify sync = inventory::sync(player, {{211, 3}, {1, 50}});
|
||||
check(sync.material_list.size() == 1 && sync.material_list[0].tid == 211 &&
|
||||
sync.material_list[0].num == 3 && sync.basic_info.has(),
|
||||
"a sync names bag totals and carries the wallet");
|
||||
|
||||
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");
|
||||
core::Config::get().paths.playerFile = realPath;
|
||||
}
|
||||
|
||||
void testChallengeHistory() {
|
||||
const data::Tables& tables = data::Tables::get();
|
||||
if (!tables.loaded()) return;
|
||||
@@ -1046,6 +1134,7 @@ void runGameTests() {
|
||||
testElementBuffs();
|
||||
testTables();
|
||||
testGacha();
|
||||
testInventory();
|
||||
testChallengeHistory();
|
||||
testSceneRes();
|
||||
testRosterProtos();
|
||||
|
||||
Reference in new issue
Block a user