Add formal enemies, remaining formal partners and synthesized UI SFX
- UI SFX v3: procedurally synthesized one-shots (Tools/synth_ui_sfx.py) wired to buttons, rewards, errors and red dots via UiSfx, gated by the SFX setting. - Five formal enemies (Hornlet, Dashlet, Shellhorn, HornCaptain, GreatHorn): Hunyuan 50k -> 2000-tri XPbr, DGXA TokenRig, Idle/Walk/Attack, nested per type in PrototypeArena; PrototypeBattle.Enemy gains presentation-only Moving/Strikes. - Formal partners for all ten pets in the arena and portraits, including new VineSprite, ThunderLeopard and TideWhale. - Pipeline fixes: reject local TokenRig fallback, drop loose mesh islands, configurable Unity run_script timeout, companion static stage. - Also includes concurrent work from another tool in this working tree (FlameLion, enemy combat FX, challenge mode, home explorer, server updates). - Resources registered in Doc/ResDesc_*.md; verified on isolated :18081. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+103
-15
@@ -27,7 +27,7 @@ struct Fixture {
|
||||
check(db.store_token(token,*pid,1,4102444800LL),"token created");read();
|
||||
}
|
||||
J read() {
|
||||
auto r=db.game_request(token,"",now);check(r.status==200,"read succeeds");auto j=J::parse(r.body);state=j["state"];return j;
|
||||
auto r=db.game_request(token,"",now);if(r.status!=200) std::cerr<<"READ "<<r.status<<" "<<r.body<<"\n";check(r.status==200,"read succeeds");auto j=J::parse(r.body);state=j["state"];return j;
|
||||
}
|
||||
J command(const std::string& action,J extra=J::object()) {
|
||||
extra["action"]=action;extra["revision"]=state["revision"];
|
||||
@@ -159,13 +159,13 @@ int main(int argc,char** argv) {
|
||||
auto finish=f.command("battle.finish",{{"activityId",start["requestId"]}});auto result=f.send(finish);
|
||||
check(result["result"]["won"],"configured stage wins at recommended level");
|
||||
parity["entries"].push_back({{"snapshot",b["battle"]["snapshot"]},{"result",result["result"]}});
|
||||
check(result["result"]["firstClear"]&&f.state["clearedStages"][id-1]==1,"per-stage clear recorded");
|
||||
check(result["result"]["firstClear"]&&f.state["clearedStages"][id-1]==1&&f.state["storyRankAt"]==f.now,"per-stage clear recorded at the confirmation time");
|
||||
for(const char* key:{"coins","candy","balls"}) check(f.state[key].get<int>()==before[key].get<int>()+cfg["stages"][id-1]["reward"][key].get<int>(),"configured first-clear reward exact");
|
||||
before=f.state;f.send(finish);check(f.state==before,"per-stage settlement replay once only");
|
||||
// A fresh repeated battle still receives no first-clear reward.
|
||||
start=f.command("battle.begin");b=f.send(start);f.now=b["battle"]["readyAt"].get<int64_t>();before=f.state;
|
||||
result=f.send(f.command("battle.finish",{{"activityId",start["requestId"]}}));
|
||||
check(result["result"]["won"]&&!result["result"]["firstClear"].get<bool>()&&f.state["coins"]==before["coins"]&&f.state["candy"]==before["candy"]&&f.state["balls"]==before["balls"],"repeat victory grants nothing");
|
||||
check(result["result"]["won"]&&!result["result"]["firstClear"].get<bool>()&&f.state["coins"]==before["coins"]&&f.state["candy"]==before["candy"]&&f.state["balls"]==before["balls"]&&f.state["storyRankAt"]==before["storyRankAt"],"repeat victory grants nothing and keeps the rank time");
|
||||
}
|
||||
db::Database reopened(f.path);check(J::parse(reopened.game_request(f.token,"",f.now).body)["state"]==f.state,"all stage progress survives reconnect");
|
||||
for(int id:{1,5,10,15,20}) for(int level:{1,5,20}) for(int party=0;party<6;party++) {
|
||||
@@ -271,7 +271,7 @@ int main(int argc,char** argv) {
|
||||
old["schema"]=2;old["stages"]={1,2,1};old["shards"]={12,25,30};old["formation"]={2,1,-1,-1,-1,-1};
|
||||
old["clearedStages"]={1,1,0,0,0};old["selectedStage"]=2;old["level"]=5;old["coins"]=123;old["candy"]=456;old["tickets"]=7;
|
||||
old.erase("captureTutorialDone");old.erase("captureCount");old.erase("duplicateStreak");f.replace(old);
|
||||
check(f.state["schema"]==9&&f.state["stages"][0]==1&&f.state["stages"][1]==2&&f.state["stages"][2]==1&&f.state["stages"].size()==10&&f.state["shards"][0]==12&&f.state["shards"][1]==25&&f.state["shards"][2]==30&&f.state["shards"].size()==10&&f.state["formation"]==J({2,1,-1,-1,-1,-1}),"G2 migration preserves owned evolutions, shards and formation");
|
||||
check(f.state["schema"]==11&&f.state["stages"][0]==1&&f.state["stages"][1]==2&&f.state["stages"][2]==1&&f.state["stages"].size()==10&&f.state["shards"][0]==12&&f.state["shards"][1]==25&&f.state["shards"][2]==30&&f.state["shards"].size()==10&&f.state["formation"]==J({2,1,-1,-1,-1,-1}),"G2 migration preserves owned evolutions, shards and formation");
|
||||
check(f.state["captureTutorialDone"]&&f.state["captureCount"]==0&&f.state["selectedStage"]==2&&f.state["clearedStages"][0]==1&&f.state["clearedStages"][1]==1&&f.state["clearedStages"].size()==20&&f.state["coins"]==123&&f.state["tickets"]==7,"G2 migration preserves progress and suppresses extra tutorial reward");
|
||||
auto r=f.send(f.command("capture"),[](uint32_t n){check(n==10,"migrated ordinary pool size");return 3u;});
|
||||
check(r["result"]["pet"]==3&&!r["result"]["tutorial"].get<bool>()&&f.state["captureCount"]==1,"migrated player enters ordinary pool");
|
||||
@@ -296,7 +296,7 @@ int main(int argc,char** argv) {
|
||||
{
|
||||
Fixture f((root/"migration.db").string());
|
||||
auto old=f.state;old["schema"]=1;old["stages"].get_ref<J::array_t&>().resize(3);old["shards"].get_ref<J::array_t&>().resize(3);old["cleared"]=true;old["goalClaimed"]=true;old["coins"]=777;old["candy"]=888;old.erase("selectedStage");old.erase("clearedStages");
|
||||
f.replace(old);check(f.state["schema"]==9&&f.state["clearedStages"][0]==1&&f.state["clearedStages"].size()==20&&f.state["coins"]==777&&f.state["candy"]==888&&f.state["goalClaimed"],"G1 completed save migrates without reward or reset");
|
||||
f.replace(old);check(f.state["schema"]==11&&f.state["clearedStages"][0]==1&&f.state["clearedStages"].size()==20&&f.state["coins"]==777&&f.state["candy"]==888&&f.state["goalClaimed"],"G1 completed save migrates without reward or reset");
|
||||
auto next=f.state;next["cleared"]=false;next["clearedStages"]=cleared(static_cast<int>(f.read()["config"]["stages"].size()),0);f.replace(next);
|
||||
auto start=f.command("battle.begin");auto b=f.send(start);old=f.stored();old["schema"]=1;old["stages"].get_ref<J::array_t&>().resize(3);old["shards"].get_ref<J::array_t&>().resize(3);old["battleSnapshot"]["stages"].get_ref<J::array_t&>().resize(3);old.erase("selectedStage");old.erase("clearedStages");
|
||||
freeze_classic(old,2);
|
||||
@@ -312,7 +312,7 @@ int main(int argc,char** argv) {
|
||||
}
|
||||
{
|
||||
Fixture f((root/"camp.db").string());auto cfg=f.read()["config"];
|
||||
check(f.state["schema"]==9&&f.state["campLayout"]==J({-1,-1,-1})&&f.state["decorations"]==J({0,0,0})&&f.state["chests"]==J({0,0,0}),"fresh camp is empty, no free purchases");
|
||||
check(f.state["schema"]==11&&f.state["campLayout"]==J({-1,-1,-1})&&f.state["decorations"]==J({0,0,0})&&f.state["chests"]==J({0,0,0}),"fresh camp is empty, no free purchases");
|
||||
auto before=f.state;
|
||||
check(f.send(f.command("camp.place",{{"item",0},{"slot",0}}))["error"]=="camp_build_locked","construction gated by actual clear");
|
||||
check(f.send(f.command("camp.interact",{{"pet",0}}))["error"]=="camp_rest_locked"&&f.state==before,"rest gated without rewards");
|
||||
@@ -353,13 +353,13 @@ int main(int argc,char** argv) {
|
||||
f.sql("DROP TRIGGER fail_camp");check(f.send(buy)["ok"]&&f.state["coins"]==640,"original purchase succeeds once after repair");
|
||||
auto old=f.stored();old["schema"]=3;old.erase("campLayout");old.erase("decorations");old.erase("restPet");old.erase("campInteractions");
|
||||
old["captureCount"]=12;old["duplicateStreak"]=3;old["tickets"]=9;old["shards"][0]=77;f.replace(old);
|
||||
check(f.state["schema"]==9&&f.state["coins"]==640&&f.state["tickets"]==9&&f.state["shards"][0]==77&&f.state["stages"].size()==10&&f.state["captureCount"]==12&&f.state["duplicateStreak"]==3&&f.state["campLayout"]==J({-1,-1,-1}),"schema3 migration preserves collection wallet and initializes empty camp");
|
||||
check(f.state["schema"]==11&&f.state["coins"]==640&&f.state["tickets"]==9&&f.state["shards"][0]==77&&f.state["stages"].size()==10&&f.state["captureCount"]==12&&f.state["duplicateStreak"]==3&&f.state["campLayout"]==J({-1,-1,-1}),"schema3 migration preserves collection wallet and initializes empty camp");
|
||||
auto beforeRead=f.state;f.read();check(f.state==beforeRead,"camp reads grant no extra production");
|
||||
f.now+=600;f.read();check(f.state["balls"]==21,"camp uses existing single recovery source");
|
||||
}
|
||||
{
|
||||
Fixture f((root/"daily.db").string());auto cfg=f.read()["config"];int day=f.state["dailyDay"];int64_t midnight=(static_cast<int64_t>(day)+1)*86400-28800;
|
||||
check(f.state["schema"]==9&&f.state["signinDays"]==1&&f.state["dailyProgress"]==J({0,0,0})&&f.state["stages"].size()==10,"first server day unlocks day1 without fabricated events");
|
||||
check(f.state["schema"]==11&&f.state["signinDays"]==1&&f.state["dailyProgress"]==J({0,0,0})&&f.state["stages"].size()==10,"first server day unlocks day1 without fabricated events");
|
||||
auto before=f.state;f.read();check(f.state==before,"opening pages/reconnecting grants no task progress or login day");
|
||||
check(f.send(f.command("daily.claim",{{"item",0},{"day",day},{"progress",999}}))["error"]=="task_incomplete","client cannot fabricate task progress");
|
||||
check(f.send(f.command("signin.claim",{{"item",6},{"loginDays",7}}))["error"]=="signin_locked","client cannot unlock future sign-in");
|
||||
@@ -408,12 +408,12 @@ int main(int argc,char** argv) {
|
||||
for(int i=0;i<16;i++)retries.emplace_back([&]{auto r=f.db.game_request(f.token,sign.dump(),f.now);if(r.status==200&&J::parse(r.body)["ok"])ok++;});for(auto& t:retries)t.join();f.read();check(ok==16&&f.state["balls"]==35,"16 sign-in retries award ten once");
|
||||
auto start=f.command("shot.begin",{{"aim",0}});auto launched=f.send(start);auto old=f.stored();old["schema"]=4;old["shots"]=100;old["coins"]=777;old["campInteractions"]=12;old["decorations"]={1,0,0};old["campLayout"]={-1,0,-1};
|
||||
for(const char* key:{"dailyDay","dailyProgress","dailyClaimed","signinDays","signinClaimed"})old.erase(key);f.replace(old);
|
||||
check(f.state["schema"]==9&&f.state["coins"]==777&&f.state["campLayout"]==J({-1,0,-1})&&f.state["campInteractions"]==12&&f.state["shots"]==100&&f.state["dailyProgress"]==J({0,0,0})&&f.state["signinDays"]==1&&f.state["signinClaimed"]==J({0,0,0,0,0,0,0})&&f.state["pendingShot"]==start["requestId"],"schema4 migration preserves wallet camp and pending shot; no retroactive daily progress or sign-in award");
|
||||
check(f.state["schema"]==11&&f.state["coins"]==777&&f.state["campLayout"]==J({-1,0,-1})&&f.state["campInteractions"]==12&&f.state["shots"]==100&&f.state["dailyProgress"]==J({0,0,0})&&f.state["signinDays"]==1&&f.state["signinClaimed"]==J({0,0,0,0,0,0,0})&&f.state["pendingShot"]==start["requestId"],"schema4 migration preserves wallet camp and pending shot; no retroactive daily progress or sign-in award");
|
||||
f.now=launched["shot"]["readyAt"].get<int64_t>();f.send(f.command("shot.finish",{{"activityId",start["requestId"]}}));check(f.state["dailyProgress"][0]==1,"pre-migration pending shot counts once when actually settled");
|
||||
}
|
||||
{
|
||||
Fixture f((root/"shop.db").string());auto cfg=f.read()["config"];int day=f.state["dailyDay"];
|
||||
check(f.state["schema"]==9&&f.state["shopBought"]==J({0,0,0})&&f.state["balls"]==20&&f.state["chests"]==J({0,0,0}),"fresh shop stock has no automatic supply award");
|
||||
check(f.state["schema"]==11&&f.state["shopBought"]==J({0,0,0})&&f.state["balls"]==20&&f.state["chests"]==J({0,0,0}),"fresh shop stock has no automatic supply award");
|
||||
auto before=f.state;f.read();check(f.state==before,"opening shop does not consume stock or grant inventory");
|
||||
auto free=f.command("shop.buy",{{"item",0},{"day",day},{"balls",999}});auto r=f.send(free);
|
||||
check(r["ok"]&&r["result"]["chargedCoins"]==0&&r["result"]["reward"]["balls"]==5&&f.state["balls"]==25&&f.state["shopBought"]==J({1,0,0}),"free supply grants configured five once");
|
||||
@@ -457,7 +457,7 @@ int main(int argc,char** argv) {
|
||||
auto shot=f.command("shot.begin",{{"aim",0}});auto launched=f.send(shot);before=f.state;check(f.send(f.command("shop.buy",{{"item",1},{"day",day}}))["error"]=="pending_activity"&&f.state==before,"pending activity blocks purchase");
|
||||
f.now=launched["shot"]["readyAt"].get<int64_t>();f.send(f.command("shot.finish",{{"activityId",shot["requestId"]}}));
|
||||
auto old=f.stored();old["schema"]=5;old.erase("shopBought");old["dailyClaimed"]={1,0,0};old["dailyProgress"][0]=3;old["decorations"]={1,0,0};old["campLayout"]={-1,0,-1};int coins=f.state["coins"],tickets=f.state["tickets"];f.replace(old);
|
||||
check(f.state["schema"]==9&&f.state["coins"]==coins&&f.state["tickets"]==tickets&&f.state["dailyClaimed"]==J({1,0,0})&&f.state["shopBought"]==J({0,0,0})&&f.state["campLayout"]==J({-1,0,-1}),"schema5 migration preserves wallets daily and decorations, initializes only shop limits");
|
||||
check(f.state["schema"]==11&&f.state["coins"]==coins&&f.state["tickets"]==tickets&&f.state["dailyClaimed"]==J({1,0,0})&&f.state["shopBought"]==J({0,0,0})&&f.state["campLayout"]==J({-1,0,-1}),"schema5 migration preserves wallets daily and decorations, initializes only shop limits");
|
||||
}
|
||||
{
|
||||
Fixture f((root/"shop_pending.db").string());auto begin=f.command("battle.begin");auto fight=f.send(begin);auto old=f.stored();freeze_classic(old,3);old["schema"]=5;old.erase("shopBought");old["battleSnapshot"]["config"].erase("shop");f.replace(old);
|
||||
@@ -468,7 +468,7 @@ int main(int argc,char** argv) {
|
||||
}
|
||||
{
|
||||
Fixture f((root/"mail_single.db").string());
|
||||
check(f.state["schema"]==9&&f.state["mails"].size()==2&&f.state["balls"]==20&&f.state["candy"]==0&&f.state["chests"]==J({0,0,0}),"schema7 adds letters, no automatic wallet award");
|
||||
check(f.state["schema"]==11&&f.state["mails"].size()==2&&f.state["balls"]==20&&f.state["candy"]==0&&f.state["chests"]==J({0,0,0}),"schema7 adds letters, no automatic wallet award");
|
||||
auto id=f.state["mails"][0]["mailId"],notice=f.state["mails"][1]["mailId"];auto reward=f.state["mails"][0]["reward"];
|
||||
auto before=f.state;auto read=f.command("mail.read",{{"mailId",id}});auto r=f.send(read);
|
||||
check(r["ok"]&&f.state["mails"][0]["read"]&&!f.state["mails"][0]["claimed"]&&f.state["balls"]==before["balls"],"reading never grants attachment");
|
||||
@@ -523,7 +523,7 @@ int main(int argc,char** argv) {
|
||||
{
|
||||
Fixture f((root/"mail_migration.db").string());auto begin=f.command("battle.begin");auto fight=f.send(begin);auto old=f.stored();
|
||||
freeze_classic(old,4);old["schema"]=6;old.erase("mails");old["coins"]=777;old["candy"]=888;old["signinClaimed"]={1,0,0,0,0,0,0};old["shopBought"]={1,0,0};old["battleSnapshot"]["config"].erase("mail");f.replace(old);
|
||||
auto migrated=f.read();check(f.state["schema"]==9&&f.state["coins"]==777&&f.state["candy"]==888&&f.state["balls"]==20&&f.state["mails"].size()==2&&f.state["shopBought"]==J({1,0,0})&&f.state["signinClaimed"]==J({1,0,0,0,0,0,0}),"schema6 migration adds mail only, preserves wallet and daily stock");
|
||||
auto migrated=f.read();check(f.state["schema"]==11&&f.state["coins"]==777&&f.state["candy"]==888&&f.state["balls"]==20&&f.state["mails"].size()==2&&f.state["shopBought"]==J({1,0,0})&&f.state["signinClaimed"]==J({1,0,0,0,0,0,0}),"schema6 migration adds mail only, preserves wallet and daily stock");
|
||||
check(f.state["pendingBattle"]==begin["requestId"]&&!migrated["battle"]["snapshot"]["config"].contains("mail"),"migration preserves pending battle and pre-mail frozen config");
|
||||
auto read=f.command("mail.read",{{"mailId",f.state["mails"][0]["mailId"]}});check(f.send(read)["error"]=="pending_activity","mail operations preserve pending activity exclusion");
|
||||
f.now=fight["battle"]["readyAt"].get<int64_t>();auto end=f.send(f.command("battle.finish",{{"activityId",begin["requestId"]}}));
|
||||
@@ -535,7 +535,7 @@ int main(int argc,char** argv) {
|
||||
}
|
||||
{
|
||||
Fixture f((root/"chest.db").string());auto cfg=f.read()["config"];int day=f.state["dailyDay"];
|
||||
check(f.state["schema"]==9&&f.state["chests"]==J({0,0,0})&&f.state["chestDailyClaimed"]==0&&f.state["balls"]==20&&f.state["stages"].size()==10,"fresh chest inventory is empty and grants nothing");
|
||||
check(f.state["schema"]==11&&f.state["chests"]==J({0,0,0})&&f.state["chestDailyClaimed"]==0&&f.state["balls"]==20&&f.state["stages"].size()==10,"fresh chest inventory is empty and grants nothing");
|
||||
auto before=f.state;f.read();check(f.state==before,"opening the chest page grants no chest");
|
||||
check(f.send(f.command("chest.open",{{"chest",0}}))["error"]=="chest_missing"&&f.state==before,"opening a missing chest is refused");
|
||||
check(f.send(f.command("chest.open",{{"chest",3}}))["error"]=="invalid_chest","unknown chest refused");
|
||||
@@ -593,7 +593,95 @@ int main(int argc,char** argv) {
|
||||
check(ok==16&&f.state["chests"]==J({1,0,0})&&f.state["chestDailyClaimed"]==1,"16 concurrent daily chest retries add one box");
|
||||
auto old=f.stored();old["schema"]=7;old.erase("chests");old.erase("chestDailyClaimed");
|
||||
old["coins"]=777;old["candy"]=888;old["mails"]=old["mails"];f.replace(old);
|
||||
check(f.state["schema"]==9&&f.state["coins"]==777&&f.state["candy"]==888&&f.state["chests"]==J({0,0,0})&&f.state["chestDailyClaimed"]==0&&f.state["mails"].size()==2&&f.state["stages"].size()==10&&f.state["clearedStages"].size()==20,"schema7 migration preserves wallet and mail, initializes empty chests without free rewards");
|
||||
check(f.state["schema"]==11&&f.state["coins"]==777&&f.state["candy"]==888&&f.state["chests"]==J({0,0,0})&&f.state["chestDailyClaimed"]==0&&f.state["mails"].size()==2&&f.state["stages"].size()==10&&f.state["clearedStages"].size()==20,"schema7 migration preserves wallet and mail, initializes empty chests without free rewards");
|
||||
}
|
||||
{
|
||||
Fixture f((root/"challenge.db").string());auto cfg=f.read()["config"];
|
||||
check(cfg["challenges"].size()==5&&f.state["schema"]==11&&f.state["challengeBadges"]==J({0,0,0,0,0})&&f.state["challengeBestMs"]==J({0,0,0,0,0})&&f.state["storyRankAt"]==0&&f.state["challengeRankAt"]==J({0,0,0,0,0}),"fresh save has five locked element trials and no badges");
|
||||
auto locked=f.command("challenge.begin",{{"stageId",1}});auto before=f.state;
|
||||
check(f.send(locked)["error"]=="challenge_locked"&&f.state==before,"element trial stays closed before 1-1");
|
||||
auto s=f.stored();s["schema"]=9;s.erase("challengeBadges");s.erase("challengeBestMs");s["shards"][0]=7;s["coins"]=640;f.replace(s);
|
||||
check(f.state["schema"]==11&&f.state["challengeBadges"]==J({0,0,0,0,0})&&f.state["challengeBestMs"]==J({0,0,0,0,0})&&f.state["storyRankAt"]==0&&f.state["challengeRankAt"]==J({0,0,0,0,0})&&f.state["shards"][0]==7&&f.state["coins"]==640,"schema9 migration adds empty trials without shards or coins");
|
||||
s=f.stored();s["clearedStages"][0]=1;f.replace(s);before=f.state;auto story=before["formation"];
|
||||
check(f.send(f.command("challenge.begin",{{"stageId",2}}))["error"]=="need_element"&&f.state==before,"grass trial explains itself by refusing until a grass partner is owned");
|
||||
auto start=f.command("challenge.begin",{{"stageId",1}});auto began=f.send(start);
|
||||
check(began["ok"]&&began["battle"]["snapshot"]["challengeId"]==1&&began["battle"]["snapshot"]["formation"]==J({1,-1,-1,-1,-1,-1})&&f.state["formation"]==story&&f.state["pendingBattle"]==start["requestId"],"fire trial deploys only the owned fire partner and leaves the story formation");
|
||||
check(f.send(f.command("battle.begin"))["error"]=="pending_activity","a running trial blocks a story battle");
|
||||
auto abandon=f.command("battle.abandon",{{"activityId",start["requestId"]}});
|
||||
check(f.send(abandon)["result"]["won"]==false&&f.state["challengeBadges"][0]==0&&f.state["shards"][1]==0&&f.state["dailyProgress"][1]==0&&f.state["challengeRankAt"][0]==0,"leaving a trial grants nothing and does not count as a story adventure");
|
||||
start=f.command("challenge.begin",{{"stageId",1}});began=f.send(start);f.now=began["battle"]["readyAt"].get<int64_t>();
|
||||
auto end=f.send(f.command("battle.finish",{{"activityId",start["requestId"]}}));
|
||||
check(end["result"]["won"]&&end["result"]["firstClear"]&&end["result"]["shards"]==10&&end["result"]["pet"]==1&&f.state["shards"][1]==10&&f.state["challengeBadges"][0]==1&&f.state["challengeBestMs"][0].get<int>()>0&&f.state["challengeRankAt"][0]==f.now&&f.state["coins"]==640&&f.state["dailyProgress"][1]==0,"first fire clear pays directed shards once and records the time");
|
||||
int best=f.state["challengeBestMs"][0];int64_t bestAt=f.state["challengeRankAt"][0];
|
||||
start=f.command("challenge.begin",{{"stageId",1}});began=f.send(start);f.now=began["battle"]["readyAt"].get<int64_t>();
|
||||
end=f.send(f.command("battle.finish",{{"activityId",start["requestId"]}}));
|
||||
check(end["result"]["won"]&&!end["result"]["firstClear"].get<bool>()&&!end["result"]["improved"].get<bool>()&&f.state["shards"][1]==10&&f.state["challengeBestMs"][0]==best&&f.state["challengeRankAt"][0]==bestAt,"a repeat clear keeps the badge, the shards and the record");
|
||||
start=f.command("challenge.begin",{{"stageId",3}});began=f.send(start);
|
||||
check(began["ok"]&&began["battle"]["snapshot"]["formation"][0]==0,"rock trial accepts the owned rock partner");
|
||||
f.now=began["battle"]["readyAt"].get<int64_t>();
|
||||
end=f.send(f.command("battle.finish",{{"activityId",start["requestId"]}}));
|
||||
check(end["result"]["won"]&&end["result"]["pet"]==0&&f.state["shards"][0]==17,"rock trial is winnable at level 1 and adds shards to the existing seven");
|
||||
}
|
||||
{
|
||||
Fixture f((root/"rank.db").string());
|
||||
int coins=f.state["coins"];
|
||||
check(f.state["schema"]==11&&f.state["storyRankAt"]==0&&coins==20,"fresh account is absent from every board");
|
||||
auto before=f.state;
|
||||
check(f.send(f.command("rank.list",{{"stageId",9}}))["error"]=="invalid_board"&&f.state==before,"unknown board changes nothing");
|
||||
check(f.send(f.command("rank.list",{{"stageId",-1}}))["error"]=="invalid_board"&&f.state==before,"negative board changes nothing");
|
||||
auto list=f.command("rank.list",{{"stageId",0}});
|
||||
auto empty=f.send(list);
|
||||
check(empty["ok"]&&empty["rank"]["total"]==0&&empty["rank"]["selfPlace"]==0&&empty["rank"]["entries"].empty()&&f.state["coins"]==coins&&f.state["revision"]==before["revision"],"an empty board is a read and does not move the wallet or revision");
|
||||
auto replay=f.send(list);
|
||||
check(replay["replayed"]&&replay["rank"]==empty["rank"],"the same rank request keeps the board it first confirmed");
|
||||
auto start=f.command("battle.begin");check(f.send(start)["ok"],"battle starts before a rank read");
|
||||
auto during=f.send(f.command("rank.list",{{"stageId",1}}));
|
||||
check(during["ok"]&&during["rank"]["total"]==0&&f.state["pendingBattle"]==start["requestId"]&&f.state["coins"]==coins,"reading a board does not interrupt or settle a battle");
|
||||
check(f.send(f.command("battle.abandon",{{"activityId",start["requestId"]}}))["ok"],"abandon after a rank read");
|
||||
auto migrated=f.stored();migrated["schema"]=10;migrated.erase("storyRankAt");migrated.erase("challengeRankAt");
|
||||
migrated["clearedStages"][0]=1;migrated["challengeBestMs"][0]=4321;migrated["coins"]=640;f.replace(migrated);
|
||||
check(f.state["schema"]==11&&f.state["storyRankAt"]==f.now&&f.state["challengeRankAt"][0]==f.now&&f.state["challengeRankAt"][1]==0&&f.state["coins"]==640&&f.state["shards"][0]==0,"schema10 migration stamps existing scores without paying rewards");
|
||||
auto paint=[&](int clears,int64_t at,int best,int64_t bestAt){auto s=f.stored();for(int i=0;i<static_cast<int>(s["clearedStages"].size());i++) s["clearedStages"][i]=i<clears?1:0;s["storyRankAt"]=at;s["challengeBestMs"][0]=best;s["challengeRankAt"][0]=best>0?bestAt:0;return s;};
|
||||
auto write=[&](int64_t pid,J state){
|
||||
std::string data=state.dump(),escaped;for(char c:data){escaped+=c;if(c=='\'') escaped+='\'';}
|
||||
f.sql("UPDATE game_states SET state='"+escaped+"' WHERE pid="+std::to_string(pid));
|
||||
};
|
||||
auto add=[&](const std::string& name,J state){
|
||||
auto pid=f.db.create_user(name,"hash","salt");check(pid.has_value(),"rank player");
|
||||
check(f.db.store_token("token-"+name,*pid,1,4102444800LL),"rank token");
|
||||
check(f.db.game_request("token-"+name,"",f.now).status==200,"rank player save");
|
||||
write(*pid,state);return *pid;
|
||||
};
|
||||
write(1,paint(2,f.now-80,5000,f.now-10));
|
||||
add("leader",paint(3,f.now,0,0));
|
||||
add("early",paint(2,f.now-100,4000,f.now));
|
||||
add("late",paint(2,f.now-50,5000,f.now-10));
|
||||
add("untimed",paint(2,0,0,0));
|
||||
add("absent",paint(0,0,0,0));
|
||||
f.read();
|
||||
auto story=f.send(f.command("rank.list",{{"stageId",0}}));
|
||||
auto rows=story["rank"]["entries"];
|
||||
check(story["ok"]&&story["rank"]["total"]==5&&story["rank"]["selfPlace"]==3&&rows.size()==5&&rows[0]["name"]=="leader"&&rows[1]["name"]=="early"&&rows[2]["name"]=="tester"&&rows[3]["name"]=="late"&&rows[4]["name"]=="untimed"&&f.state["coins"]==640,"story board orders confirmed clears, then time, then player id");
|
||||
auto trial=f.send(f.command("rank.list",{{"stageId",1}}));rows=trial["rank"]["entries"];
|
||||
check(trial["ok"]&&trial["rank"]["total"]==3&&trial["rank"]["selfPlace"]==2&&rows[0]["name"]=="early"&&rows[0]["score"]==4000&&rows[1]["name"]=="tester"&&rows[2]["name"]=="late","trial board orders the shorter confirmed time first");
|
||||
f.sql("UPDATE game_states SET state='{' WHERE pid=(SELECT pid FROM users WHERE username='absent')");
|
||||
auto kept=f.send(f.command("rank.list",{{"stageId",0}}));
|
||||
check(kept["ok"]&&kept["rank"]["total"]==5,"a broken save is skipped and the confirmed board remains");
|
||||
}
|
||||
{
|
||||
Fixture f((root/"rankcap.db").string());
|
||||
for(int i=0;i<22;i++) {
|
||||
std::string name="cap"+std::to_string(i),token="cap-"+name;
|
||||
auto pid=f.db.create_user(name,"hash","salt");check(pid.has_value(),"cap player");
|
||||
check(f.db.store_token(token,*pid,1,4102444800LL),"cap token");
|
||||
check(f.db.game_request(token,"",f.now).status==200,"cap save");
|
||||
auto s=f.stored();int score=i<20?20-i:1;
|
||||
s["clearedStages"]=cleared_prefix(static_cast<int>(s["clearedStages"].size()),score);s["storyRankAt"]=f.now-score*10-i;
|
||||
std::string data=s.dump(),escaped;for(char c:data){escaped+=c;if(c=='\'') escaped+='\'';}
|
||||
f.sql("UPDATE game_states SET state='"+escaped+"' WHERE pid="+std::to_string(*pid));
|
||||
}
|
||||
auto board=f.send(f.command("rank.list",{{"stageId",0}}));
|
||||
check(board["ok"]&&board["rank"]["total"]==22&&board["rank"]["entries"].size()==20&&board["rank"]["entries"][0]["score"]==20&&board["rank"]["selfPlace"]==0&&f.state["coins"]==20,"the page keeps the top 20 and leaves an unranked player off the board");
|
||||
}
|
||||
std::filesystem::remove_all(root);
|
||||
if(argc>1) {std::ofstream output(argv[1],std::ios::binary);output<<parity.dump(2);check(output.good(),"parity vectors exported");}
|
||||
|
||||
Reference in New Issue
Block a user