feat(game): add the new challenge modes
This commit is contained in:
1 parent
e77d7600ae
commit
9dd37f8087
31 files changed
+2536
-70
No files matched your search
@@ -439,6 +439,35 @@ void runFlowTests() {
|
||||
}
|
||||
}
|
||||
|
||||
// Every npc the client asks about comes back as already met, under the id it asked
|
||||
// about: an empty reply is what leaves an npc with no talk option.
|
||||
{
|
||||
proto::GetFirstTalkNpcCsReq talk;
|
||||
talk.npc_id_list = {1001, 1002, 1003};
|
||||
client.send(cmd::GetFirstTalkNpcCsReq, talk);
|
||||
check(client.await(cmd::GetFirstTalkNpcScRsp, packet), "the npc meet query answers");
|
||||
|
||||
proto::GetFirstTalkNpcScRsp talkRsp;
|
||||
check(parseBody(packet, talkRsp), "the npc meet query parses");
|
||||
check(talkRsp.npc_meet_status_list.size() == 3, "one status per npc asked about");
|
||||
bool echoed = talkRsp.npc_meet_status_list.size() == 3;
|
||||
for (size_t i = 0; i < talkRsp.npc_meet_status_list.size(); ++i) {
|
||||
if (talkRsp.npc_meet_status_list[i].npc_id != talk.npc_id_list[i] ||
|
||||
!talkRsp.npc_meet_status_list[i].is_meet) {
|
||||
echoed = false;
|
||||
}
|
||||
}
|
||||
check(echoed, "each carries its own id and is already met");
|
||||
|
||||
proto::GetNpcTakenRewardCsReq reward;
|
||||
reward.npc_id = 1003;
|
||||
client.send(cmd::GetNpcTakenRewardCsReq, reward);
|
||||
check(client.await(cmd::GetNpcTakenRewardScRsp, packet), "the npc reward query answers");
|
||||
proto::GetNpcTakenRewardScRsp rewardRsp;
|
||||
check(parseBody(packet, rewardRsp), "the npc reward query parses");
|
||||
check(rewardRsp.npc_id == 1003, "about the npc that was asked for");
|
||||
}
|
||||
|
||||
// A calyx, the way the Survival Index starts one: no prop to walk up to, just a
|
||||
// cocoon id and how many runs were bought.
|
||||
{
|
||||
@@ -618,6 +647,282 @@ void runFlowTests() {
|
||||
check(!curRsp.cur_challenge.has(), "and there is no longer one going");
|
||||
}
|
||||
|
||||
// The three-node floor: the packet 4.6 starts Memory of Chaos with. Unlike the older
|
||||
// one it carries its arena in the reply rather than in a notify.
|
||||
{
|
||||
proto::StartChallengeTierceCsReq start;
|
||||
start.challenge_id = 5213;
|
||||
for (uint32_t stage = 0; stage < 3; ++stage) {
|
||||
proto::ChallengeTierceStageLineupInfo info;
|
||||
proto::AvatarIdentifier one;
|
||||
one.id = 8001;
|
||||
proto::AvatarIdentifier two;
|
||||
two.id = 1001;
|
||||
info.lineup.push_back(one);
|
||||
info.lineup.push_back(two);
|
||||
start.stage_info_list.push_back(std::move(info));
|
||||
}
|
||||
client.send(cmd::StartChallengeTierceCsReq, start);
|
||||
check(client.await(cmd::StartChallengeTierceScRsp, packet), "starting the floor answers");
|
||||
|
||||
proto::StartChallengeTierceScRsp startRsp;
|
||||
check(parseBody(packet, startRsp), "the start parses");
|
||||
check(startRsp.retcode == 0, "and was accepted");
|
||||
check(startRsp.scene.has(), "with the arena in the reply itself");
|
||||
check(startRsp.challenge_tierce_info.has(), "and the run");
|
||||
if (startRsp.challenge_tierce_info) {
|
||||
check(startRsp.challenge_tierce_info->challenge_id == 5213, "for the floor asked for");
|
||||
check(startRsp.challenge_tierce_info->stage_index == 0, "starting on its first node");
|
||||
check(startRsp.challenge_tierce_info->lineup_list.size() == 3, "with all three teams");
|
||||
}
|
||||
|
||||
uint32_t nodeMonster = 0;
|
||||
size_t nodeMonsters = 0;
|
||||
if (startRsp.scene) {
|
||||
check(startRsp.scene->entry_id == 3014002, "on the floor's own map entrance");
|
||||
for (const proto::SceneEntityGroupInfo& group : startRsp.scene->entity_group_list) {
|
||||
for (const proto::SceneEntityInfo& entity : group.entity_list) {
|
||||
if (!entity.npc_monster) continue;
|
||||
++nodeMonsters;
|
||||
nodeMonster = entity.entity_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
check(nodeMonsters == 1, "holding only the node's own monster");
|
||||
|
||||
if (nodeMonster != 0) {
|
||||
proto::SceneCastSkillCsReq cast;
|
||||
cast.cast_entity_id = 1;
|
||||
cast.hit_target_entity_id_list.push_back(nodeMonster);
|
||||
client.send(cmd::SceneCastSkillCsReq, cast);
|
||||
check(client.await(cmd::SceneCastSkillScRsp, packet), "the node's fight answers");
|
||||
|
||||
proto::SceneCastSkillScRsp castRsp;
|
||||
check(parseBody(packet, castRsp), "the node's fight parses");
|
||||
check(castRsp.battle_info.has(), "a fight was started");
|
||||
if (castRsp.battle_info) {
|
||||
// The cycle pool is the tierce row's 45, not the floor's own 30.
|
||||
check(castRsp.battle_info->rounds_limit == 45, "with the floor-wide cycle pool");
|
||||
bool mazeBuff = false;
|
||||
for (const proto::BattleBuff& buff : castRsp.battle_info->buff_list) {
|
||||
if (buff.id == 3030146) mazeBuff = true;
|
||||
}
|
||||
check(mazeBuff, "and the maze buff of the floor it extends");
|
||||
|
||||
proto::PVEBattleResultCsReq result;
|
||||
result.battle_id = castRsp.battle_info->battle_id;
|
||||
result.stage_id = castRsp.battle_info->stage_id;
|
||||
result.end_status = proto::BattleEndStatus::BATTLE_END_WIN;
|
||||
result.stt.emplace().round_cnt = 5;
|
||||
client.send(cmd::PVEBattleResultCsReq, result);
|
||||
check(client.await(cmd::PVEBattleResultScRsp, packet), "the result answers");
|
||||
|
||||
net::Packet syncPacket;
|
||||
check(client.await(cmd::ChallengeTierceSyncNotify, syncPacket),
|
||||
"clearing a node reports it");
|
||||
}
|
||||
}
|
||||
|
||||
client.sendEmpty(cmd::StartNextChallengeTierceCsReq);
|
||||
check(client.await(cmd::StartNextChallengeTierceScRsp, packet), "the next node answers");
|
||||
proto::StartNextChallengeTierceScRsp nextRsp;
|
||||
check(parseBody(packet, nextRsp), "the next node parses");
|
||||
check(nextRsp.retcode == 0, "and was accepted");
|
||||
check(nextRsp.scene.has(), "carrying its arena too");
|
||||
check(nextRsp.challenge_tierce_info.has() &&
|
||||
nextRsp.challenge_tierce_info->stage_index == 1,
|
||||
"on the second node now");
|
||||
|
||||
client.sendEmpty(cmd::LeaveChallengeTierceCsReq);
|
||||
check(client.await(cmd::LeaveChallengeTierceScRsp, packet), "leaving answers");
|
||||
check(client.await(cmd::EnterSceneByServerScNotify, packet), "and puts the player back");
|
||||
|
||||
client.sendEmpty(cmd::GetChallengeTierceDataCsReq);
|
||||
check(client.await(cmd::GetChallengeTierceDataScRsp, packet), "the history answers");
|
||||
proto::GetChallengeTierceDataScRsp dataRsp;
|
||||
check(parseBody(packet, dataRsp), "the history parses");
|
||||
bool remembered = false;
|
||||
for (const proto::ChallengeTierceData& entry : dataRsp.challenge_info_list) {
|
||||
if (entry.challenge_id != 5213) continue;
|
||||
remembered = true;
|
||||
check(!entry.result_list.empty(), "with what the run did on it");
|
||||
}
|
||||
check(remembered, "and the floor is in it");
|
||||
}
|
||||
|
||||
// Anomaly Arbitration: a knight on its own arena -- team, start, fight, settle, retry,
|
||||
// leave -- then season ten's boss on hard, whose arena is newer than the scene dump.
|
||||
{
|
||||
client.sendEmpty(cmd::GetChallengePeakDataCsReq);
|
||||
check(client.await(cmd::GetChallengePeakDataScRsp, packet), "arbitration data answered");
|
||||
proto::GetChallengePeakDataScRsp peakData;
|
||||
check(parseBody(packet, peakData), "arbitration data parses");
|
||||
check(peakData.challenge_peak_groups.size() >= 10, "with every season in it");
|
||||
check(peakData.current_peak_group_id == 10, "and the newest on show");
|
||||
|
||||
proto::SetChallengePeakMobLineupAvatarCsReq teams;
|
||||
teams.peak_group_id = 9;
|
||||
proto::ChallengePeakLineup knightTeam;
|
||||
knightTeam.peak_id = 901;
|
||||
knightTeam.peak_avatar_id_list = {8001, 1001};
|
||||
teams.lineup_list.push_back(knightTeam);
|
||||
client.send(cmd::SetChallengePeakMobLineupAvatarCsReq, teams);
|
||||
check(client.await(cmd::SetChallengePeakMobLineupAvatarScRsp, packet),
|
||||
"setting a knight's team answers");
|
||||
net::Packet groupPacket;
|
||||
check(client.await(cmd::ChallengePeakGroupDataUpdateScNotify, groupPacket),
|
||||
"and re-sends the season");
|
||||
proto::ChallengePeakGroupDataUpdateScNotify groupUpdate;
|
||||
check(parseBody(groupPacket, groupUpdate), "the season parses");
|
||||
bool teamKept = false;
|
||||
if (groupUpdate.challenge_peak_group) {
|
||||
for (const proto::ChallengePeak& peak : groupUpdate.challenge_peak_group->peaks) {
|
||||
if (peak.peak_id == 901 && peak.peak_avatar_id_list.size() == 2) teamKept = true;
|
||||
}
|
||||
}
|
||||
check(teamKept, "with the team in the knight's slot");
|
||||
|
||||
// Sent without a team, the start falls back on the one just set.
|
||||
proto::StartChallengePeakCsReq start;
|
||||
start.peak_id = 901;
|
||||
client.send(cmd::StartChallengePeakCsReq, start);
|
||||
check(client.await(cmd::StartChallengePeakScRsp, packet), "starting a knight answers");
|
||||
proto::StartChallengePeakScRsp startRsp;
|
||||
check(parseBody(packet, startRsp), "the start parses");
|
||||
check(startRsp.retcode == 0, "and was accepted");
|
||||
|
||||
auto arenaOf = [&](const char* what, uint32_t& monster, size_t& monsters, size_t& actors,
|
||||
uint32_t& entryId) {
|
||||
net::Packet arenaPacket;
|
||||
check(client.await(cmd::EnterSceneByServerScNotify, arenaPacket), what);
|
||||
proto::EnterSceneByServerScNotify arena;
|
||||
check(parseBody(arenaPacket, arena), "the arena parses");
|
||||
monster = 0;
|
||||
monsters = actors = 0;
|
||||
entryId = arena.scene ? arena.scene->entry_id : 0;
|
||||
if (!arena.scene) return;
|
||||
for (const proto::SceneEntityGroupInfo& group : arena.scene->entity_group_list) {
|
||||
for (const proto::SceneEntityInfo& entity : group.entity_list) {
|
||||
if (entity.actor) ++actors;
|
||||
if (!entity.npc_monster) continue;
|
||||
++monsters;
|
||||
monster = entity.entity_id;
|
||||
}
|
||||
}
|
||||
};
|
||||
uint32_t knight = 0;
|
||||
size_t monsters = 0;
|
||||
size_t actors = 0;
|
||||
uint32_t entryId = 0;
|
||||
arenaOf("the arena went out before the response", knight, monsters, actors, entryId);
|
||||
check(entryId == 3014501, "on the season's own arena");
|
||||
check(monsters == 1, "with the knight and nothing else in it");
|
||||
check(actors == 2, "and the fight's team standing in it, not the squad");
|
||||
|
||||
auto fight = [&](uint32_t monster, uint32_t rounds, proto::SceneBattleInfo& battle) {
|
||||
proto::SceneCastSkillCsReq cast;
|
||||
cast.cast_entity_id = 1;
|
||||
cast.hit_target_entity_id_list.push_back(monster);
|
||||
client.send(cmd::SceneCastSkillCsReq, cast);
|
||||
check(client.await(cmd::SceneCastSkillScRsp, packet), "the fight answers");
|
||||
proto::SceneCastSkillScRsp castRsp;
|
||||
check(parseBody(packet, castRsp), "the fight parses");
|
||||
check(castRsp.battle_info.has(), "a fight was started");
|
||||
if (!castRsp.battle_info) return false;
|
||||
battle = *castRsp.battle_info;
|
||||
|
||||
proto::PVEBattleResultCsReq result;
|
||||
result.battle_id = battle.battle_id;
|
||||
result.stage_id = battle.stage_id;
|
||||
result.end_status = proto::BattleEndStatus::BATTLE_END_WIN;
|
||||
result.stt.emplace().round_cnt = rounds;
|
||||
client.send(cmd::PVEBattleResultCsReq, result);
|
||||
check(client.await(cmd::PVEBattleResultScRsp, packet), "the result answers");
|
||||
return true;
|
||||
};
|
||||
auto targetsIn = [](const proto::SceneBattleInfo& battle) {
|
||||
auto slot = battle.battle_target_info.find(5);
|
||||
return slot == battle.battle_target_info.end() ? size_t{0}
|
||||
: slot->second.battle_target_list.size();
|
||||
};
|
||||
|
||||
proto::SceneBattleInfo battle;
|
||||
if (knight != 0 && fight(knight, 3, battle)) {
|
||||
check(battle.stage_id == 30509011, "against the knight's own stage");
|
||||
check(battle.battle_avatar_list.size() == 2, "with the knight's team");
|
||||
check(targetsIn(battle) == 3, "and its three targets");
|
||||
|
||||
net::Packet settlePacket;
|
||||
check(client.await(cmd::ChallengePeakSettleScNotify, settlePacket),
|
||||
"winning settles the knight");
|
||||
proto::ChallengePeakSettleScNotify settle;
|
||||
check(parseBody(settlePacket, settle), "the settle parses");
|
||||
check(settle.is_win && settle.peak_id == 901, "as a win for that knight");
|
||||
// Four cycles or fewer, two or fewer, and nobody lost: three cycles meets two.
|
||||
check(settle.finished_target_list.size() == 2, "three cycles meets two targets of three");
|
||||
}
|
||||
|
||||
client.sendEmpty(cmd::GetCurChallengePeakCsReq);
|
||||
check(client.await(cmd::GetCurChallengePeakScRsp, packet), "the fight is queryable");
|
||||
proto::GetCurChallengePeakScRsp cur;
|
||||
check(parseBody(packet, cur), "the fight parses");
|
||||
check(cur.peak_id == 901, "and it is the knight");
|
||||
|
||||
client.sendEmpty(cmd::ReStartChallengePeakCsReq);
|
||||
check(client.await(cmd::ReStartChallengePeakScRsp, packet), "retrying answers");
|
||||
proto::ReStartChallengePeakScRsp restart;
|
||||
check(parseBody(packet, restart), "the retry parses");
|
||||
check(restart.retcode == 0, "and was accepted");
|
||||
arenaOf("the arena went out again", knight, monsters, actors, entryId);
|
||||
check(monsters == 1, "with the knight standing again");
|
||||
|
||||
client.sendEmpty(cmd::LeaveChallengePeakCsReq);
|
||||
check(client.await(cmd::LeaveChallengePeakScRsp, packet), "leaving answers");
|
||||
check(client.await(cmd::EnterSceneByServerScNotify, packet), "and puts the player back");
|
||||
|
||||
proto::SetChallengePeakBossHardModeCsReq hardMode;
|
||||
hardMode.peak_group_id = 10;
|
||||
hardMode.is_hard_mode = true;
|
||||
client.send(cmd::SetChallengePeakBossHardModeCsReq, hardMode);
|
||||
check(client.await(cmd::SetChallengePeakBossHardModeScRsp, packet), "hard mode answers");
|
||||
proto::SetChallengePeakBossHardModeScRsp hardRsp;
|
||||
check(parseBody(packet, hardRsp), "hard mode parses");
|
||||
check(hardRsp.is_hard_mode && hardRsp.peak_group_id == 10, "and is echoed");
|
||||
|
||||
proto::StartChallengePeakCsReq bossStart;
|
||||
bossStart.peak_id = 1004;
|
||||
bossStart.peak_avatar_id_list = {8001, 1001};
|
||||
client.send(cmd::StartChallengePeakCsReq, bossStart);
|
||||
check(client.await(cmd::StartChallengePeakScRsp, packet), "starting the boss answers");
|
||||
check(parseBody(packet, startRsp) && startRsp.retcode == 0, "and was accepted");
|
||||
uint32_t boss = 0;
|
||||
arenaOf("the boss arena went out", boss, monsters, actors, entryId);
|
||||
check(entryId == 3013501, "borrowing season one's arena");
|
||||
check(monsters == 1, "with the boss alone in it");
|
||||
|
||||
if (boss != 0 && fight(boss, 1, battle)) {
|
||||
check(battle.stage_id == 30510022, "against the boss's hard stage");
|
||||
check(targetsIn(battle) == 1, "judged by the hard target alone");
|
||||
|
||||
net::Packet settlePacket;
|
||||
check(client.await(cmd::ChallengePeakSettleScNotify, settlePacket),
|
||||
"winning settles the boss");
|
||||
proto::ChallengePeakSettleScNotify settle;
|
||||
check(parseBody(settlePacket, settle), "the settle parses");
|
||||
check(settle.is_win && settle.hard_mode_has_passed, "as a hard clear");
|
||||
}
|
||||
|
||||
client.sendEmpty(cmd::LeaveChallengePeakCsReq);
|
||||
check(client.await(cmd::LeaveChallengePeakScRsp, packet), "leaving the boss answers");
|
||||
check(client.await(cmd::EnterSceneByServerScNotify, packet), "and puts the player back");
|
||||
|
||||
client.sendEmpty(cmd::GetCurChallengePeakCsReq);
|
||||
check(client.await(cmd::GetCurChallengePeakScRsp, packet), "the run is queryable");
|
||||
proto::GetCurChallengePeakScRsp after;
|
||||
check(parseBody(packet, after), "the run parses");
|
||||
check(after.peak_id == 0, "and there is no longer one going");
|
||||
}
|
||||
|
||||
// Every way out of a fight answers. None of these did before, which left the
|
||||
// client sitting on the defeat screen or on a half-switched party.
|
||||
client.sendEmpty(cmd::QuitBattleCsReq);
|
||||
|
||||
+136
-1
@@ -18,6 +18,7 @@
|
||||
#include "game/roster.h"
|
||||
#include "game/scene.h"
|
||||
#include "game/srtools.h"
|
||||
#include "game/tierce.h"
|
||||
#include "net/packet.h"
|
||||
#include "tests/harness.h"
|
||||
|
||||
@@ -213,6 +214,27 @@ void testChallengeHistory() {
|
||||
check(rsp.max_level_list.size() == 3, "one max level per mode");
|
||||
check(rsp.serialize().size() + net::kPacketOverhead < net::kMaxKcpMessage,
|
||||
"the challenge history fits in one kcp message");
|
||||
|
||||
// Anomaly Arbitration. Production rows have no arena, so the beta dump fills it in,
|
||||
// and places season ten, which production does not have at all.
|
||||
check(tables.peakGroups().size() >= 10, "every arbitration season loaded");
|
||||
const data::PeakInfo* knight = tables.peak(101);
|
||||
check(knight != nullptr && knight->mapEntranceId == 3013501 && knight->eventId == 30501011,
|
||||
"a production knight gets its arena from the beta dump");
|
||||
check(knight != nullptr && knight->monsters.size() == 1 &&
|
||||
knight->monsters[0].configId == data::kPeakMarkerId,
|
||||
"and plants one monster over the arena's marker");
|
||||
const data::PeakInfo* boss = tables.peak(1004);
|
||||
check(boss != nullptr && boss->boss && boss->groupId == 10, "1004 is season ten's boss");
|
||||
check(boss != nullptr && boss->mapEntranceId == 3014601 && boss->mazeGroupId == 7,
|
||||
"on its own arena");
|
||||
check(boss != nullptr && boss->hardEventId == 30510022 && boss->hardTarget == 3007,
|
||||
"with a hard stage and target of its own");
|
||||
const data::BattleTargetInfo* turns = tables.battleTarget(3001);
|
||||
check(turns != nullptr && turns->param == 4 && !turns->countsDeaths,
|
||||
"target 3001 is four cycles or fewer");
|
||||
const data::BattleTargetInfo* deaths = tables.battleTarget(3000);
|
||||
check(deaths != nullptr && deaths->countsDeaths, "target 3000 counts avatars lost");
|
||||
}
|
||||
|
||||
void testSceneRes() {
|
||||
@@ -500,7 +522,6 @@ void testBattleBuild() {
|
||||
request.casterEntityId = 1;
|
||||
request.skillIndex = 0;
|
||||
request.monsterEntityIds = {monsterEntity};
|
||||
request.allowSrToolsOverride = false;
|
||||
|
||||
proto::SceneBattleInfo info = game::battle::create(player, request);
|
||||
check(info.battle_id != 0, "the fight got an id");
|
||||
@@ -565,6 +586,115 @@ void testBattleBuild() {
|
||||
check(!stillThere, "and can be switched off");
|
||||
}
|
||||
|
||||
// The third node 4.6 bolts onto a floor: its own arena and monster, everything else
|
||||
// borrowed from the floor it extends.
|
||||
void testTierceRun() {
|
||||
if (!data::SceneRes::get().loaded() || !data::Tables::get().loaded()) return;
|
||||
const data::Tables& tables = data::Tables::get();
|
||||
|
||||
const data::ChallengeTierceInfo* config = tables.challengeTierce(5213);
|
||||
check(config != nullptr, "the tierce table is loaded");
|
||||
if (config == nullptr) return;
|
||||
check(config->preChallengeId == 5212, "5213 extends Memory of Chaos floor 5212");
|
||||
check(config->roundLimit == 45, "with a cycle pool for the whole floor");
|
||||
check(config->mazeGroupId == 11, "its node is scene group 11");
|
||||
check(config->monsters.size() == 1, "holding one planted monster");
|
||||
check(config->targetIds.size() == 3, "and three star targets");
|
||||
check(tables.challengeTierceFor(5212) == config, "the floor finds it the other way too");
|
||||
|
||||
const data::ChallengeInfo* floor = tables.challenge(config->preChallengeId);
|
||||
check(floor != nullptr, "the floor it extends is loaded");
|
||||
if (floor == nullptr) return;
|
||||
|
||||
game::Player player = makeTestPlayer();
|
||||
std::vector<proto::ChallengeTierceStageLineupInfo> stages(3);
|
||||
for (uint32_t stage = 0; stage < 3; ++stage) {
|
||||
proto::AvatarIdentifier one;
|
||||
one.id = 8001 + stage;
|
||||
stages[stage].lineup.push_back(one);
|
||||
stages[stage].buff_id = 1000 + stage;
|
||||
}
|
||||
|
||||
proto::SceneInfo scene;
|
||||
uint32_t retcode = game::tierce::start(player, 5213, false, 0, stages, scene);
|
||||
check(retcode == 0, "the floor starts");
|
||||
if (retcode != 0) return;
|
||||
check(player.tierce().active, "the run is recorded on the player");
|
||||
check(player.tierce().stage == 0, "on its first node");
|
||||
check(player.tierce().roundsLeft == 45, "with the whole cycle pool");
|
||||
check(player.location().entryId == floor->mapEntranceId,
|
||||
"and the first node is the floor's own arena");
|
||||
|
||||
// The third node is the one the tierce row plants, not one of the floor's two.
|
||||
player.tierce().stage = 2;
|
||||
game::ChallengeArena storage;
|
||||
const game::ChallengeArena* arena = game::tierce::arena(player, storage);
|
||||
check(arena != nullptr && arena->mazeGroupId == config->mazeGroupId,
|
||||
"the last node fights in the tierce's own group");
|
||||
check(arena != nullptr && arena->monsters == &config->monsters,
|
||||
"with the tierce's own monster");
|
||||
player.tierce().stage = 0;
|
||||
|
||||
// What the node brings to a fight comes from both rows.
|
||||
game::BattleRequest request;
|
||||
game::tierce::prepareBattle(player, request);
|
||||
check(request.party.size() == 1, "the node fights with its own team");
|
||||
check(request.mazeBuffId == floor->mazeBuffId, "under the floor's maze buff");
|
||||
check(request.stageBuffId == 1000, "and the buff picked for this node");
|
||||
check(request.roundsLimit == 45, "with the cycles left in the pool");
|
||||
|
||||
// Starting again keeps the spot the first attempt left from.
|
||||
game::SceneLocation origin = player.tierce().origin;
|
||||
proto::SceneInfo again;
|
||||
check(game::tierce::start(player, 5213, false, 0, stages, again) == 0, "a retry starts");
|
||||
check(player.tierce().origin.entryId == origin.entryId, "and goes back to the same place");
|
||||
|
||||
// Every floor with a third node shows up in the history, under the tierce's id.
|
||||
proto::GetChallengeTierceDataScRsp history = game::tierce::history(player);
|
||||
bool found = false;
|
||||
for (const proto::ChallengeTierceData& entry : history.challenge_info_list) {
|
||||
if (entry.challenge_id != 5213) continue;
|
||||
found = true;
|
||||
check(entry.stage_info_list.size() == 3, "with a slot for each of its three nodes");
|
||||
}
|
||||
check(found, "the floor is in the history");
|
||||
}
|
||||
|
||||
// "auto" hands a calyx over to the srtools build and leaves every other fight on the
|
||||
// stage the client named. See gameplay.battle_source.
|
||||
void testBattleSourceSplit() {
|
||||
if (!data::Tables::get().loaded()) return;
|
||||
if (core::Config::get().gameplay.battleSource != "auto") return;
|
||||
|
||||
game::Player player = makeTestPlayer();
|
||||
game::Roster roster = player.roster();
|
||||
const game::BattleConfig& build = roster.data().battle;
|
||||
if (build.waves.empty() || build.stageId == 0) {
|
||||
std::printf("SKIP battle source split (the build on disk names no fight)\n");
|
||||
return;
|
||||
}
|
||||
const data::CocoonInfo* cocoon = data::Tables::get().cocoon(1001, player.worldLevel());
|
||||
if (cocoon == nullptr || cocoon->stageIds.empty()) {
|
||||
std::printf("SKIP battle source split (calyx 1001 is not in the tables)\n");
|
||||
return;
|
||||
}
|
||||
uint32_t calyxStage = cocoon->stageIds.front();
|
||||
check(calyxStage != build.stageId, "the calyx and the build name different stages");
|
||||
|
||||
game::BattleRequest calyx;
|
||||
calyx.cocoonId = 1001;
|
||||
calyx.wave = 1;
|
||||
calyx.stageIds.push_back(calyxStage);
|
||||
calyx.allowSrToolsOverride = true;
|
||||
proto::SceneBattleInfo run = game::battle::create(player, calyx);
|
||||
check(run.stage_id == build.stageId, "a calyx fights the srtools build");
|
||||
|
||||
game::BattleRequest shadow;
|
||||
shadow.stageIds.push_back(calyxStage);
|
||||
proto::SceneBattleInfo other = game::battle::create(player, shadow);
|
||||
check(other.stage_id == calyxStage, "every other fight keeps its own stage");
|
||||
}
|
||||
|
||||
// Writing an equip change back must not drop the parts of the build we do not
|
||||
// model -- the loadout presets are the user's, not ours.
|
||||
void testSrToolsRoundTrip() {
|
||||
@@ -672,6 +802,9 @@ void runGameTests() {
|
||||
std::string root = util::findRootDir("config/config.json");
|
||||
if (!root.empty()) util::setCurrentDir(root);
|
||||
core::Config::get().load("config/config.json");
|
||||
// Anything that saves goes to a scratch file: a test must never write the save a
|
||||
// real client is using.
|
||||
core::Config::get().paths.playerFile = "build/test-player.json";
|
||||
data::Tables::get().load(core::Config::get().paths.dataSources);
|
||||
data::SceneRes::get().load(core::Config::get().paths.sceneRes);
|
||||
data::SceneRes::get().loadAnchors(core::Config::get().paths.anchors);
|
||||
@@ -688,8 +821,10 @@ void runGameTests() {
|
||||
testRosterProtos();
|
||||
testSceneBuild();
|
||||
testChallengeRun();
|
||||
testTierceRun();
|
||||
testEveryScenePacketFits();
|
||||
testBattleBuild();
|
||||
testBattleSourceSplit();
|
||||
testPropInteraction();
|
||||
testSrToolsRoundTrip();
|
||||
}
|
||||
Reference in new issue
Block a user