|
|
|
@@ -0,0 +1,577 @@
|
|
|
|
|
#include "db.hpp"
|
|
|
|
|
#include "game_config.hpp"
|
|
|
|
|
#include "json.hpp"
|
|
|
|
|
#include "sqlite3.h"
|
|
|
|
|
#include <algorithm>
|
|
|
|
|
#include <cmath>
|
|
|
|
|
#include <cctype>
|
|
|
|
|
#include <ctime>
|
|
|
|
|
#include <stdexcept>
|
|
|
|
|
#include <vector>
|
|
|
|
|
#include <random>
|
|
|
|
|
#include <functional>
|
|
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
using J = nlohmann::json;
|
|
|
|
|
const J& config() { return gameplay::config(); }
|
|
|
|
|
const J& economy() { return config().at("economy"); }
|
|
|
|
|
// Statements and transactions are scoped: a disk/constraint failure never publishes half a command.
|
|
|
|
|
struct Statement {
|
|
|
|
|
sqlite3_stmt* p = nullptr;
|
|
|
|
|
Statement(sqlite3* db, const char* sql) {
|
|
|
|
|
if (sqlite3_prepare_v2(db, sql, -1, &p, nullptr) != SQLITE_OK) throw std::runtime_error("prepare");
|
|
|
|
|
}
|
|
|
|
|
~Statement() { sqlite3_finalize(p); }
|
|
|
|
|
void number(int i, int64_t n) { sqlite3_bind_int64(p, i, n); }
|
|
|
|
|
void text(int i, const std::string& s) { sqlite3_bind_text(p, i, s.c_str(), -1, SQLITE_TRANSIENT); }
|
|
|
|
|
int step() {
|
|
|
|
|
int rc = sqlite3_step(p);
|
|
|
|
|
if (rc != SQLITE_ROW && rc != SQLITE_DONE) throw std::runtime_error("step");
|
|
|
|
|
return rc;
|
|
|
|
|
}
|
|
|
|
|
std::string column(int i) { return reinterpret_cast<const char*>(sqlite3_column_text(p, i)); }
|
|
|
|
|
};
|
|
|
|
|
struct Transaction {
|
|
|
|
|
sqlite3* db;
|
|
|
|
|
bool committed = false;
|
|
|
|
|
explicit Transaction(sqlite3* d) : db(d) {
|
|
|
|
|
if (sqlite3_exec(db, "BEGIN IMMEDIATE", nullptr, nullptr, nullptr) != SQLITE_OK) throw std::runtime_error("begin");
|
|
|
|
|
}
|
|
|
|
|
void commit() {
|
|
|
|
|
if (sqlite3_exec(db, "COMMIT", nullptr, nullptr, nullptr) != SQLITE_OK) throw std::runtime_error("commit");
|
|
|
|
|
committed = true;
|
|
|
|
|
}
|
|
|
|
|
~Transaction() { if (!committed) sqlite3_exec(db, "ROLLBACK", nullptr, nullptr, nullptr); }
|
|
|
|
|
};
|
|
|
|
|
J zeros(int n) {J a=J::array();for(int i=0;i<n;i++)a.push_back(0);return a;}
|
|
|
|
|
J initial(int64_t now) {
|
|
|
|
|
const int pets=static_cast<int>(config()["pets"].size()),stages=static_cast<int>(config()["stages"].size());
|
|
|
|
|
J owned=zeros(pets);owned[0]=1;if(pets>1)owned[1]=1;
|
|
|
|
|
J formation=J::array();formation.push_back(0);if(pets>1)formation.push_back(1);while(static_cast<int>(formation.size())<6)formation.push_back(-1);
|
|
|
|
|
return J{{"schema",4},{"campLayout",{-1,-1,-1}},{"decorations",{0,0,0}},{"restPet",0},{"campInteractions",0},{"captureTutorialDone",false},{"captureCount",0},{"duplicateStreak",0},{"selectedStage",1},{"clearedStages",zeros(stages)},{"revision",0},{"coins",0},{"candy",0},{"balls",economy()["initialBalls"]},{"tickets",0},
|
|
|
|
|
{"level",1},{"shots",0},{"stages",owned},{"shards",zeros(pets)},
|
|
|
|
|
{"formation",formation},{"cleared",false},{"goalClaimed",false},
|
|
|
|
|
{"pendingShot",""},{"lastShot",""},{"pendingAim",0},{"lastCandy",0},{"lastCoins",0},
|
|
|
|
|
{"pendingBattle",""},{"lastBattle",""},{"ballRecoveryUnix",now}};
|
|
|
|
|
}
|
|
|
|
|
void migrate(J& s,int64_t now) {
|
|
|
|
|
if(s.at("schema")==1) {
|
|
|
|
|
s["schema"]=2;s["selectedStage"]=1;s["clearedStages"]={s.at("cleared").get<bool>()?1:0,0,0,0,0};
|
|
|
|
|
if(s.contains("battleData")) {s["battleData"]["stageId"]=1;s["battleData"]["reward"]={{"coins",80},{"candy",40},{"balls",3}};s["battleSnapshot"]["legacy"]=true;}
|
|
|
|
|
if(s.contains("lastBattleData")) {s["lastBattleData"]["stageId"]=1;s["lastBattleData"]["reward"]={{"coins",80},{"candy",40},{"balls",3}};}
|
|
|
|
|
}
|
|
|
|
|
if(s.at("schema")==2) {
|
|
|
|
|
if(s.at("stages").size()!=3||s.at("shards").size()!=3) throw std::runtime_error("invalid_old_save");
|
|
|
|
|
s["schema"]=3;s["captureTutorialDone"]=s["stages"][2].get<int>()>0;
|
|
|
|
|
s["captureCount"]=0;s["duplicateStreak"]=0;
|
|
|
|
|
// G2 introduced the two later chapter-one partners; the remaining roster is widened by 8->9.
|
|
|
|
|
for(int i=0;i<2;i++) {s["stages"].push_back(0);s["shards"].push_back(0);}
|
|
|
|
|
}
|
|
|
|
|
if(s.at("schema")==3) {s["schema"]=4;s["campLayout"]={-1,-1,-1};s["decorations"]={0,0,0};s["restPet"]=0;s["campInteractions"]=0;}
|
|
|
|
|
if(s.at("schema")==4) {s["schema"]=5;s["dailyDay"]=-1;s["dailyProgress"]={0,0,0};s["dailyClaimed"]={0,0,0};s["signinDays"]=0;s["signinClaimed"]={0,0,0,0,0,0,0};}
|
|
|
|
|
if(s.at("schema")==5) {s["schema"]=6;s["shopBought"]={0,0,0};}
|
|
|
|
|
if(s.at("schema")==6) {
|
|
|
|
|
s["schema"]=7;s["mails"]=J::array();
|
|
|
|
|
for(const auto& m:config()["mail"]["initial"]) s["mails"].push_back({{"mailId",m["mailId"]},{"title",m["title"]},{"body",m["body"]},{"reward",m["reward"]},{"sentAt",now},{"expiresAt",now+config()["mail"]["lifetimeSeconds"].get<int>()},{"read",false},{"claimed",false},{"expired",false}});
|
|
|
|
|
}
|
|
|
|
|
if(s.at("schema")==7) {s["schema"]=8;s["chests"]={0,0,0};s["chestDailyClaimed"]=0;}
|
|
|
|
|
if(s.at("schema")==8) {
|
|
|
|
|
// 8->9 only widens the roster and chapter list; no partner, stage clear or reward is granted.
|
|
|
|
|
const int pets=static_cast<int>(config()["pets"].size()),stages=static_cast<int>(config()["stages"].size());
|
|
|
|
|
if(static_cast<int>(s["stages"].size())>pets||static_cast<int>(s["shards"].size())>pets||static_cast<int>(s["clearedStages"].size())>stages) throw std::runtime_error("invalid_old_save");
|
|
|
|
|
while(static_cast<int>(s["stages"].size())<pets)s["stages"].push_back(0);
|
|
|
|
|
while(static_cast<int>(s["shards"].size())<pets)s["shards"].push_back(0);
|
|
|
|
|
while(static_cast<int>(s["clearedStages"].size())<stages)s["clearedStages"].push_back(0);
|
|
|
|
|
s["schema"]=9;
|
|
|
|
|
}
|
|
|
|
|
if(s.at("schema")!=9 || s.at("stages").size()!=config()["pets"].size() || s.at("shards").size()!=config()["pets"].size() || s.at("clearedStages").size()!=config()["stages"].size()) throw std::runtime_error("unsupported_save");
|
|
|
|
|
if(!s.at("mails").is_array()||s["mails"].size()>100) throw std::runtime_error("invalid_mail_save");
|
|
|
|
|
std::vector<std::string> ids;
|
|
|
|
|
for(const auto& m:s["mails"]) {
|
|
|
|
|
auto id=m.at("mailId").get<std::string>();
|
|
|
|
|
if(id.empty()||id.size()>64||std::find(ids.begin(),ids.end(),id)!=ids.end()||m.at("title").get<std::string>().empty()||m.at("body").get<std::string>().empty()||m.at("sentAt").get<int64_t>()<=0||m.at("expiresAt").get<int64_t>()<=m.at("sentAt").get<int64_t>()) throw std::runtime_error("invalid_mail_record");
|
|
|
|
|
ids.push_back(id);for(const char* k:{"read","claimed","expired"}) if(!m.at(k).is_boolean()) throw std::runtime_error("invalid_mail_status");
|
|
|
|
|
for(const char* k:{"coins","candy","balls","tickets"}) if(!m.at("reward").at(k).is_number_integer()||m["reward"][k].get<int>()<0||m["reward"][k].get<int>()>10000) throw std::runtime_error("invalid_mail_reward");
|
|
|
|
|
}
|
|
|
|
|
if(s.at("shopBought").size()!=3) throw std::runtime_error("invalid_shop_save");
|
|
|
|
|
for(int i=0;i<3;i++) if(s["shopBought"][i].get<int>()<0||s["shopBought"][i].get<int>()>config()["shop"]["goods"][i]["dailyLimit"].get<int>()) throw std::runtime_error("invalid_shop_count");
|
|
|
|
|
if(s.at("chests").size()!=3||s.at("chestDailyClaimed").get<int>()<0||s.at("chestDailyClaimed").get<int>()>1) throw std::runtime_error("invalid_chest_save");
|
|
|
|
|
for(int i=0;i<3;i++) if(s["chests"][i].get<int>()<0||s["chests"][i].get<int>()>100000) throw std::runtime_error("invalid_chest_count");
|
|
|
|
|
if(s.at("campLayout").size()!=3||s.at("decorations").size()!=3) throw std::runtime_error("invalid_camp_save");
|
|
|
|
|
bool used[3]={};
|
|
|
|
|
for(const auto& id:s["campLayout"]) {int i=id.get<int>();if(i < -1 || i>2 || (i>=0 && (used[i] || s["decorations"][i]!=1))) throw std::runtime_error("invalid_camp_layout");if(i>=0) used[i]=true;}
|
|
|
|
|
for(const auto& n:s["decorations"]) if(n!=0&&n!=1) throw std::runtime_error("invalid_camp_inventory");
|
|
|
|
|
int pet=s.at("restPet");if(pet<0||pet>=static_cast<int>(config()["pets"].size())||s["stages"][pet]==0||s.at("campInteractions").get<int>()<0) throw std::runtime_error("invalid_rest_pet");
|
|
|
|
|
if(s.at("dailyProgress").size()!=3||s.at("dailyClaimed").size()!=3||s.at("signinClaimed").size()!=7||s.at("dailyDay").get<int>()< -1||s.at("signinDays").get<int>()<0||s.at("signinDays").get<int>()>7) throw std::runtime_error("invalid_daily_save");
|
|
|
|
|
for(int i=0;i<3;i++) if(s["dailyProgress"][i].get<int>()<0||s["dailyProgress"][i].get<int>()>config()["daily"]["tasks"][i]["target"].get<int>()||(s["dailyClaimed"][i]!=0&&s["dailyClaimed"][i]!=1)) throw std::runtime_error("invalid_daily_progress");
|
|
|
|
|
for(int i=0;i<7;i++) if((s["signinClaimed"][i]!=0&&s["signinClaimed"][i]!=1)||(s["signinClaimed"][i]==1&&i>=s["signinDays"].get<int>())) throw std::runtime_error("invalid_signin_save");
|
|
|
|
|
}
|
|
|
|
|
void sync_calendar(J& s,int64_t now) {
|
|
|
|
|
int day=static_cast<int>((now+config()["daily"]["utcOffsetSeconds"].get<int>())/86400);
|
|
|
|
|
if(day>s["dailyDay"].get<int>()) {
|
|
|
|
|
s["dailyDay"]=day;s["dailyProgress"]={0,0,0};s["dailyClaimed"]={0,0,0};
|
|
|
|
|
s["shopBought"]={0,0,0};s["chestDailyClaimed"]=0;
|
|
|
|
|
s["signinDays"]=std::min(7,s["signinDays"].get<int>()+1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
void daily_event(J& s,int task) {s["dailyProgress"][task]=std::min(config()["daily"]["tasks"][task]["target"].get<int>(),s["dailyProgress"][task].get<int>()+1);}
|
|
|
|
|
void grant(J& s,const J& reward) {for(const char* key:{"coins","candy","balls","tickets"}) if(reward.contains(key)) s[key]=s[key].get<int>()+reward[key].get<int>();}
|
|
|
|
|
// Chest rewards may also carry a single partner's evolution shards.
|
|
|
|
|
void grant_chest(J& s,const J& reward) {
|
|
|
|
|
grant(s,reward);
|
|
|
|
|
int pet=reward.value("shardsPet",-1),shards=reward.value("shards",0);
|
|
|
|
|
if(shards>0&&pet>=0&&pet<static_cast<int>(config()["pets"].size())) s["shards"][pet]=s["shards"][pet].get<int>()+shards;
|
|
|
|
|
}
|
|
|
|
|
int first_clear_chest(int stageId) {
|
|
|
|
|
for(const auto& drop:config()["rewards"]["stageDrops"]) if(drop["stageId"].get<int>()==stageId) return drop["chest"].get<int>();
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
void sync_mail(J& s,int64_t now) {for(auto& m:s["mails"]) if(now>=m["expiresAt"].get<int64_t>()) m["expired"]=true;}
|
|
|
|
|
bool attachment(const J& m) {for(const char* k:{"coins","candy","balls","tickets"}) if(m["reward"][k].get<int>()>0) return true;return false;}
|
|
|
|
|
J* find_mail(J& s,const std::string& id) {for(auto& m:s["mails"]) if(m["mailId"]==id) return &m;return nullptr;}
|
|
|
|
|
std::string claim_mail(J& s,J* m) {
|
|
|
|
|
if(!m) return "not_found";
|
|
|
|
|
if(m->at("claimed").get<bool>()) return "already_claimed";
|
|
|
|
|
if(m->at("expired").get<bool>()) return "expired";
|
|
|
|
|
if(!attachment(*m)) return "no_attachment";
|
|
|
|
|
grant(s,m->at("reward"));(*m)["claimed"]=true;(*m)["read"]=true;return "claimed";
|
|
|
|
|
}
|
|
|
|
|
int unlocked(const J& s) {
|
|
|
|
|
int id=1;while(id<static_cast<int>(config()["stages"].size())&&s["clearedStages"][id-1]==1) id++;return id;
|
|
|
|
|
}
|
|
|
|
|
void recover(J& s, int64_t now) {
|
|
|
|
|
int cap=economy()["ballCap"],seconds=economy()["recoverySeconds"];
|
|
|
|
|
int balls = s.at("balls"); int64_t anchor = s.at("ballRecoveryUnix");
|
|
|
|
|
if (balls >= cap || now - anchor < seconds) return;
|
|
|
|
|
int n = static_cast<int>(std::min<int64_t>((now-anchor)/seconds, cap-balls));
|
|
|
|
|
s["balls"] = balls+n;
|
|
|
|
|
s["ballRecoveryUnix"] = balls+n >= cap ? now : anchor+n*seconds;
|
|
|
|
|
}
|
|
|
|
|
struct V { float x,y; };
|
|
|
|
|
J pinball(int aim) {
|
|
|
|
|
const auto& board=config().at("pinball");
|
|
|
|
|
constexpr float dt=1.f/60;
|
|
|
|
|
V p{.5f,.95f}, v{std::sin(aim * .0174532924f)*.95f,-.35f};
|
|
|
|
|
J path=J::array(); path.push_back({{"x",p.x},{"y",p.y}});
|
|
|
|
|
for(int tick=0;tick<600;tick++) {
|
|
|
|
|
v.y-=board["gravity"].get<float>()*dt; p.x+=v.x*dt; p.y+=v.y*dt;
|
|
|
|
|
if(p.x<.025f) {p.x=.025f;v.x=std::abs(v.x)*board["wallBounce"].get<float>();}
|
|
|
|
|
if(p.x>.975f) {p.x=.975f;v.x=-std::abs(v.x)*board["wallBounce"].get<float>();}
|
|
|
|
|
for(int row=0;row<4;row++) for(int col=0;col<5;col++) {
|
|
|
|
|
V peg{.12f+col*.19f+(row%2==0?0:.025f), .72f-row*.16f};
|
|
|
|
|
V d{p.x-peg.x,p.y-peg.y}; float len=d.x*d.x+d.y*d.y;
|
|
|
|
|
float radius=board["pegRadius"];
|
|
|
|
|
if(len>=radius*radius) continue;
|
|
|
|
|
V n = len<.000001f ? V{.4f,1} : d;
|
|
|
|
|
float norm=std::sqrt(n.x*n.x+n.y*n.y); n.x/=norm;n.y/=norm;
|
|
|
|
|
float separation=board["pegSeparation"];p={peg.x+n.x*separation,peg.y+n.y*separation};
|
|
|
|
|
float dot=v.x*n.x+v.y*n.y;
|
|
|
|
|
if(dot<0) v={(v.x-2*dot*n.x)*board["pegBounce"].get<float>(),(v.y-2*dot*n.y)*board["pegBounce"].get<float>()};
|
|
|
|
|
if(n.y>.8f && std::abs(v.x)<.06f) v.x=(aim<0?-1:1)*board["bumperSpeed"].get<float>();
|
|
|
|
|
}
|
|
|
|
|
if(p.y>.98f) {p.y=.98f;v.y=-std::abs(v.y);}
|
|
|
|
|
path.push_back({{"x",p.x},{"y",p.y}});
|
|
|
|
|
if(p.y<=.035f) break;
|
|
|
|
|
}
|
|
|
|
|
int bin=std::clamp(static_cast<int>(p.x*5),0,4);
|
|
|
|
|
return J{{"path",path},{"bin",bin},{"candy",board["candy"][bin]},{"coins",board["coins"][bin]}};
|
|
|
|
|
}
|
|
|
|
|
int lane_position(int lane) {return lane==0?0:lane==1?-1:1;}
|
|
|
|
|
J battle_lanes(const J& s,int stageId) {
|
|
|
|
|
struct Hero {int pet,slot,tier,skills=0;float hp,max,attack,cd=0,skill=0,shield=0,shieldTime=0,damage=0,healing=0,absorbed=0;};
|
|
|
|
|
struct Enemy {int index,type,lane;float hp,z=11,cd=0,slowTime=0,slow=1;};
|
|
|
|
|
const auto& stage=gameplay::stage(stageId);const auto& pets=config()["pets"];const auto& types=config()["enemies"];
|
|
|
|
|
std::vector<Hero> heroes;std::vector<Enemy> enemies;
|
|
|
|
|
for(int slot=0;slot<stage["lanes"].get<int>()*2;slot++) {
|
|
|
|
|
int pet=s["formation"][slot];if(pet<0)continue;const auto& p=pets[pet];int tier=s["stages"][pet];
|
|
|
|
|
float growth=(1+economy()["levelGrowth"].get<float>()*(s["level"].get<int>()-1))*(tier==2?economy()["evolveGrowth"].get<float>():1);
|
|
|
|
|
float hp=p["hp"].get<float>()*growth;heroes.push_back({pet,slot,tier,0,hp,hp,p["attack"].get<float>()*growth});
|
|
|
|
|
}
|
|
|
|
|
int spawned=0,durability=stage["durability"],defeated=0;float time=0;bool won=false;int leaks[3]={};constexpr float dt=.05f;
|
|
|
|
|
auto z=[](const Hero& h){return h.slot%2==0?0.f:-2.f;};
|
|
|
|
|
auto damage=[&](float raw,int source,int target,float defense) {return stage["elementsEnabled"].get<bool>()?std::max(1.f,std::floor(raw*gameplay::element_multiplier(source,target)-defense)):raw;};
|
|
|
|
|
auto targets=[&](const Hero& h,float range) {
|
|
|
|
|
std::vector<size_t> ids;for(size_t i=0;i<enemies.size();i++)if(enemies[i].hp>0&&enemies[i].lane==h.slot/2&&enemies[i].z-z(h)<=range)ids.push_back(i);
|
|
|
|
|
std::sort(ids.begin(),ids.end(),[&](size_t a,size_t b){return enemies[a].z!=enemies[b].z?enemies[a].z<enemies[b].z:enemies[a].index<enemies[b].index;});return ids;
|
|
|
|
|
};
|
|
|
|
|
auto hit=[&](Hero& h,Enemy& e,float raw) {
|
|
|
|
|
float amount=damage(raw,pets[h.pet]["element"],stage["element"],types[e.type]["defense"]);h.damage+=std::min(e.hp,amount);e.hp-=amount;if(e.hp<=0)defeated++;
|
|
|
|
|
};
|
|
|
|
|
for(int tick=0;tick<2401;tick++) {
|
|
|
|
|
time+=dt;
|
|
|
|
|
for(auto& e:enemies){e.slowTime=std::max(0.f,e.slowTime-dt);if(e.slowTime<=0)e.slow=1;}
|
|
|
|
|
if(spawned<9&&time>=spawned/3*stage["waveGap"].get<float>()+spawned%3*stage["spawnGap"].get<float>()) {
|
|
|
|
|
int type=stage["enemyIds"][spawned];const auto& d=types[type];enemies.push_back({spawned,type,stage["enemyLanes"][spawned],(d["hp"].get<float>()+spawned*d["hpStep"].get<float>())*stage["hpScale"].get<float>()});spawned++;
|
|
|
|
|
}
|
|
|
|
|
for(auto& h:heroes) {
|
|
|
|
|
h.shieldTime=std::max(0.f,h.shieldTime-dt);if(h.shieldTime<=0)h.shield=0;if(h.hp<=0)continue;h.cd-=dt;h.skill-=dt;
|
|
|
|
|
const auto& p=pets[h.pet];const auto& a=p["ability"];
|
|
|
|
|
if(h.skill<=0) {
|
|
|
|
|
std::string kind=a["kind"];float value=a[h.tier==2?"t2":"t1"];bool cast=false;
|
|
|
|
|
if(kind=="shield") {h.shield=std::max(h.shield,value);h.shieldTime=a[h.tier==2?"t2Duration":"duration"];cast=true;}
|
|
|
|
|
else if(kind=="heal") {
|
|
|
|
|
Hero* target=nullptr;int radius=a[h.tier==2?"t2Radius":"radius"];
|
|
|
|
|
for(auto& other:heroes)if(other.hp>0&&other.hp<other.max&&std::abs(lane_position(other.slot/2)-lane_position(h.slot/2))<=radius&&std::abs(z(other)-z(h))<=a["range"].get<float>()&&(!target||other.hp/other.max<target->hp/target->max))target=&other;
|
|
|
|
|
if(target){float amount=std::min(value,target->max-target->hp);target->hp+=amount;h.healing+=amount;cast=true;}
|
|
|
|
|
} else {
|
|
|
|
|
auto ids=targets(h,a["range"]);if(!ids.empty()) {
|
|
|
|
|
if(kind=="slow") {auto& e=enemies[ids[0]];float factor=types[e.type]["boss"].get<bool>()?1-(1-value)*a["bossControlScale"].get<float>():value;e.slow=std::min(e.slow,factor);e.slowTime=std::max(e.slowTime,a[h.tier==2?"t2Duration":"duration"].get<float>());}
|
|
|
|
|
else for(size_t i=0;i<std::min(ids.size(),static_cast<size_t>(a[h.tier==2?"t2Targets":"targets"].get<int>()));i++)hit(h,enemies[ids[i]],h.attack*value);
|
|
|
|
|
cast=true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if(cast){h.skill=a["cooldown"];h.skills++;}
|
|
|
|
|
}
|
|
|
|
|
auto ids=targets(h,p["range"]);if(!ids.empty()&&h.cd<=0){hit(h,enemies[ids[0]],h.attack);h.cd=p["cooldown"];}
|
|
|
|
|
}
|
|
|
|
|
for(auto& e:enemies) {
|
|
|
|
|
if(e.hp<=0)continue;const auto& d=types[e.type];Hero* blocker=nullptr;
|
|
|
|
|
for(auto& h:heroes)if(h.hp>0&&h.slot/2==e.lane&&(!blocker||h.slot<blocker->slot))blocker=&h;
|
|
|
|
|
float stop=blocker?z(*blocker)+1:-4;
|
|
|
|
|
if(e.z>stop)e.z=std::max(stop,e.z-d["speed"].get<float>()*e.slow*dt);
|
|
|
|
|
else if(blocker) {
|
|
|
|
|
e.cd-=dt;if(e.cd<=0){float amount=damage(d["attack"].get<float>()*stage["attackScale"].get<float>(),stage["element"],pets[blocker->pet]["element"],0);float absorbed=std::min(blocker->shield,amount);blocker->shield-=absorbed;blocker->absorbed+=absorbed;blocker->hp=std::max(0.f,blocker->hp-(amount-absorbed));e.cd=d["cooldown"];}
|
|
|
|
|
} else {durability-=d["leak"].get<int>();leaks[e.lane]++;e.hp=0;}
|
|
|
|
|
}
|
|
|
|
|
if(durability<=0||time>=stage["timeout"].get<float>())break;
|
|
|
|
|
if(spawned==9&&std::all_of(enemies.begin(),enemies.end(),[](const Enemy& e){return e.hp<=0;})){won=true;break;}
|
|
|
|
|
}
|
|
|
|
|
J stats=J::array();for(const auto& h:heroes)stats.push_back({{"pet",h.pet},{"slot",h.slot},{"damage",h.damage},{"healing",h.healing},{"absorbed",h.absorbed},{"skills",h.skills}});
|
|
|
|
|
return J{{"won",won},{"duration",time},{"durability",std::max(0,durability)},{"defeated",defeated},{"stageId",stageId},{"reward",stage["reward"]},{"configVersion",config()["version"]},{"stats",stats},{"leaks",{leaks[0],leaks[1],leaks[2]}}};
|
|
|
|
|
}
|
|
|
|
|
// Old frozen results are already saved; only new battle.begin uses the current rule version.
|
|
|
|
|
J battle(const J& s,int stageId) {
|
|
|
|
|
if(config().value("battleRules",0)==2)return battle_lanes(s,stageId);
|
|
|
|
|
struct Hero {int pet,slot;float hp,max,attack,cd=0,heal=0;};
|
|
|
|
|
struct Enemy {int index,type;float hp,z=11,cd=0;};
|
|
|
|
|
const auto& stage=gameplay::stage(stageId);const auto& pets=config().at("pets");const auto& types=config().at("enemies");
|
|
|
|
|
std::vector<Hero> heroes; std::vector<Enemy> enemies;
|
|
|
|
|
for(int slot=0;slot<2;slot++) {
|
|
|
|
|
int pet=s["formation"][slot];if(pet<0) continue;
|
|
|
|
|
const auto& p=pets[pet];float g=(1+economy()["levelGrowth"].get<float>()*(s["level"].get<int>()-1))*(s["stages"][pet]==2?economy()["evolveGrowth"].get<float>():1);
|
|
|
|
|
float hp=p["hp"].get<float>()*g;heroes.push_back({pet,slot,hp,hp,p["attack"].get<float>()*g});
|
|
|
|
|
}
|
|
|
|
|
int spawned=0,durability=stage["durability"],defeated=0;float time=0;bool won=false;
|
|
|
|
|
constexpr float dt=.05f;
|
|
|
|
|
auto damage=[&](float raw,int attackElement,int defenseElement,float defense) {
|
|
|
|
|
if(!stage["elementsEnabled"].get<bool>()) return raw;
|
|
|
|
|
return std::max(1.f,std::floor(raw*gameplay::element_multiplier(attackElement,defenseElement)-defense));
|
|
|
|
|
};
|
|
|
|
|
for(int tick=0;tick<2401;tick++) {
|
|
|
|
|
time+=dt;
|
|
|
|
|
if(spawned<9 && time>=spawned/3*stage["waveGap"].get<float>()+spawned%3*stage["spawnGap"].get<float>()) {
|
|
|
|
|
int type=stage["enemyIds"][spawned];const auto& e=types[type];
|
|
|
|
|
enemies.push_back({spawned,type,(e["hp"].get<float>()+spawned*e["hpStep"].get<float>())*stage["hpScale"].get<float>()});spawned++;
|
|
|
|
|
}
|
|
|
|
|
for(auto& h:heroes) {
|
|
|
|
|
if(h.hp<=0) continue;h.cd-=dt;h.heal-=dt;const auto& p=pets[h.pet];
|
|
|
|
|
if(p["heal"].get<float>()>0 && h.heal<=0) {
|
|
|
|
|
Hero* target=nullptr;
|
|
|
|
|
for(auto& other:heroes) if(other.hp>0 && other.hp<other.max && (!target || other.hp/other.max<target->hp/target->max)) target=&other;
|
|
|
|
|
if(target) {target->hp=std::min(target->max,target->hp+p["heal"].get<float>());h.heal=p["healCooldown"];}
|
|
|
|
|
}
|
|
|
|
|
Enemy* nearest=nullptr;float z=h.slot==0?0.f:-2.f;
|
|
|
|
|
for(auto& e:enemies) if(e.hp>0 && e.z-z<=p["range"].get<float>() && (!nearest||e.z<nearest->z)) nearest=&e;
|
|
|
|
|
if(nearest && h.cd<=0) {
|
|
|
|
|
nearest->hp-=damage(h.attack,p["element"],stage["element"],types[nearest->type]["defense"]);
|
|
|
|
|
h.cd=p["cooldown"];if(nearest->hp<=0) defeated++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for(auto& e:enemies) {
|
|
|
|
|
if(e.hp<=0) continue;const auto& type=types[e.type];
|
|
|
|
|
Hero* blocker=nullptr;
|
|
|
|
|
for(auto& h:heroes) if(h.hp>0 && (!blocker||h.slot<blocker->slot)) blocker=&h;
|
|
|
|
|
float stop=blocker?(blocker->slot==0?1.f:-1.f):-4.f;
|
|
|
|
|
if(e.z>stop) e.z=std::max(stop,e.z-type["speed"].get<float>()*dt);
|
|
|
|
|
else if(blocker) {
|
|
|
|
|
e.cd-=dt;if(e.cd<=0) {blocker->hp=std::max(0.f,blocker->hp-damage(type["attack"].get<float>()*stage["attackScale"].get<float>(),stage["element"],pets[blocker->pet]["element"],0));e.cd=type["cooldown"];}
|
|
|
|
|
} else {durability-=type["leak"].get<int>();e.hp=0;}
|
|
|
|
|
}
|
|
|
|
|
if(durability<=0||time>=stage["timeout"].get<float>()) break;
|
|
|
|
|
if(spawned==9&&std::all_of(enemies.begin(),enemies.end(),[](const Enemy& e){return e.hp<=0;})) {won=true;break;}
|
|
|
|
|
}
|
|
|
|
|
return J{{"won",won},{"duration",time},{"durability",std::max(0,durability)},{"defeated",defeated},{"stageId",stageId},{"reward",stage["reward"]},{"configVersion",config()["version"]}};
|
|
|
|
|
}
|
|
|
|
|
bool identifier(const std::string& id) {
|
|
|
|
|
return id.size()>=16 && id.size()<=64 && std::all_of(id.begin(),id.end(),[](unsigned char c){return std::isalnum(c)||c=='-'||c=='_';});
|
|
|
|
|
}
|
|
|
|
|
int integer(const J& j,const char* field) {
|
|
|
|
|
if(!j.contains(field)||!j[field].is_number_integer()) throw std::invalid_argument("invalid_input");
|
|
|
|
|
int64_t n=j[field].get<int64_t>();
|
|
|
|
|
if(n < -1000000000LL || n > 1000000000LL) throw std::invalid_argument("invalid_input");
|
|
|
|
|
return static_cast<int>(n);
|
|
|
|
|
}
|
|
|
|
|
J capture_info(const J& s) {
|
|
|
|
|
const auto& pool=config().at("capture");int sum=0,missingSum=0;bool tutorial=!s.at("captureTutorialDone").get<bool>();
|
|
|
|
|
for(size_t i=0;i<pool["weights"].size();i++) {sum+=pool["weights"][i].get<int>();if(s["stages"][i]==0) missingSum+=pool["weights"][i].get<int>();}
|
|
|
|
|
bool pity=missingSum>0 && s["duplicateStreak"].get<int>()>=pool["duplicateLimit"].get<int>();
|
|
|
|
|
J entries=J::array();
|
|
|
|
|
for(size_t i=0;i<pool["weights"].size();i++) {
|
|
|
|
|
double base=pool["weights"][i].get<double>()/sum;
|
|
|
|
|
double chance=tutorial?(i==pool["tutorialPet"].get<size_t>()?1.:0.):pity?(s["stages"][i]==0?pool["weights"][i].get<double>()/missingSum:0.):base;
|
|
|
|
|
entries.push_back({{"pet",i},{"baseProbability",base},{"probability",chance}});
|
|
|
|
|
}
|
|
|
|
|
return J{{"tutorial",tutorial},{"completed",missingSum==0},{"pityReady",pity},{"count",s["captureCount"]},{"duplicateStreak",s["duplicateStreak"]},{"entries",entries}};
|
|
|
|
|
}
|
|
|
|
|
J capture(J& s,const std::function<uint32_t(uint32_t)>& random) {
|
|
|
|
|
const auto& pool=config().at("capture");auto info=capture_info(s);
|
|
|
|
|
bool tutorial=info["tutorial"],pity=!tutorial&&info["pityReady"].get<bool>();
|
|
|
|
|
int pet=pool["tutorialPet"];
|
|
|
|
|
if(!tutorial) {
|
|
|
|
|
uint32_t total=0;for(size_t i=0;i<pool["weights"].size();i++) if(!pity||s["stages"][i]==0) total+=pool["weights"][i].get<uint32_t>();
|
|
|
|
|
uint32_t roll;
|
|
|
|
|
if(random) roll=random(total);
|
|
|
|
|
else {std::random_device source;roll=std::uniform_int_distribution<uint32_t>(0,total-1)(source);}
|
|
|
|
|
if(roll>=total) throw std::runtime_error("invalid_random_source");
|
|
|
|
|
for(size_t i=0;i<pool["weights"].size();i++) if(!pity||s["stages"][i]==0) {
|
|
|
|
|
uint32_t weight=pool["weights"][i];if(roll<weight) {pet=static_cast<int>(i);break;}roll-=weight;
|
|
|
|
|
}
|
|
|
|
|
s["captureCount"]=s["captureCount"].get<int>()+1;
|
|
|
|
|
} else s["captureTutorialDone"]=true;
|
|
|
|
|
bool duplicate=s["stages"][pet].get<int>()>0;
|
|
|
|
|
s["tickets"]=s["tickets"].get<int>()-1;
|
|
|
|
|
if(duplicate) s["shards"][pet]=s["shards"][pet].get<int>()+economy()["duplicateShards"].get<int>();else s["stages"][pet]=1;
|
|
|
|
|
if(!tutorial) {
|
|
|
|
|
bool complete=std::all_of(s["stages"].begin(),s["stages"].end(),[](const J& value){return value.get<int>()>0;});
|
|
|
|
|
s["duplicateStreak"]=complete||!duplicate?0:std::min(pool["duplicateLimit"].get<int>(),s["duplicateStreak"].get<int>()+1);
|
|
|
|
|
}
|
|
|
|
|
return J{{"pet",pet},{"duplicate",duplicate},{"tutorial",tutorial},{"pity",pity},{"shards",duplicate?economy()["duplicateShards"].get<int>():0},{"poolVersion",config()["version"]}};
|
|
|
|
|
}
|
|
|
|
|
// Every expected refusal becomes a durable operation result, including stale revisions.
|
|
|
|
|
std::string apply(J& s,const J& c,int64_t now,J& result,const std::function<uint32_t(uint32_t)>& random) {
|
|
|
|
|
const auto action=c.at("action").get<std::string>();
|
|
|
|
|
if(integer(c,"revision")!=s["revision"].get<int>()) return "stale_revision";
|
|
|
|
|
if(action=="shot.begin") {
|
|
|
|
|
int aim=integer(c,"aim");if(aim < -config()["pinball"]["maxAim"].get<int>() || aim > config()["pinball"]["maxAim"].get<int>()) return "invalid_aim";
|
|
|
|
|
if(s["pendingShot"]!=""||s["pendingBattle"]!="") return "pending_activity";
|
|
|
|
|
if(s["balls"].get<int>()<1) return "insufficient_balls";
|
|
|
|
|
int balls=s["balls"];s["balls"]=balls-1;
|
|
|
|
|
if(balls>=economy()["ballCap"].get<int>()) s["ballRecoveryUnix"]=now;
|
|
|
|
|
s["pendingShot"]=c["requestId"];s["pendingAim"]=aim;
|
|
|
|
|
s["shotData"]=pinball(aim);
|
|
|
|
|
s["shotReadyAt"]=now+static_cast<int64_t>(std::ceil((s["shotData"]["path"].size()-1)/60.0));
|
|
|
|
|
} else if(action=="shot.finish") {
|
|
|
|
|
if(!c.contains("activityId")||c["activityId"]!=s["pendingShot"]||s["pendingShot"]=="") return "activity_missing";
|
|
|
|
|
if(now<s["shotReadyAt"].get<int64_t>()) return "activity_not_ready";
|
|
|
|
|
const auto shot=s.at("shotData");
|
|
|
|
|
s["coins"]=s["coins"].get<int>()+shot["coins"].get<int>();s["candy"]=s["candy"].get<int>()+shot["candy"].get<int>();
|
|
|
|
|
int shots=s["shots"].get<int>()+1;s["shots"]=shots;
|
|
|
|
|
if(shots==1||shots%economy()["ticketEvery"].get<int>()==0) s["tickets"]=s["tickets"].get<int>()+1;
|
|
|
|
|
s["lastCandy"]=shot["candy"];s["lastCoins"]=shot["coins"];s["lastShot"]=s["pendingShot"];s["pendingShot"]="";
|
|
|
|
|
s.erase("shotData");s.erase("shotReadyAt");
|
|
|
|
|
daily_event(s,0);
|
|
|
|
|
} else if(action=="battle.begin") {
|
|
|
|
|
if(s["pendingShot"]!=""||s["pendingBattle"]!="") return "pending_activity";
|
|
|
|
|
int stageId=c.contains("stageId")?integer(c,"stageId"):s["selectedStage"].get<int>();
|
|
|
|
|
if(stageId==0) stageId=s["selectedStage"];
|
|
|
|
|
if(stageId<1||stageId>static_cast<int>(config()["stages"].size())) return "invalid_stage";
|
|
|
|
|
if(stageId>unlocked(s)) return "stage_locked";
|
|
|
|
|
bool deployed=false;for(int slot=0;slot<gameplay::stage(stageId)["lanes"].get<int>()*2;slot++)if(s["formation"][slot]!=-1)deployed=true;
|
|
|
|
|
if(!deployed)return "empty_formation";
|
|
|
|
|
s["selectedStage"]=stageId;
|
|
|
|
|
s["pendingBattle"]=c["requestId"];s["battleData"]=battle(s,stageId);
|
|
|
|
|
// The client supports at most 2x playback. A forged victory or elapsed time is never accepted.
|
|
|
|
|
s["battleReadyAt"]=now+static_cast<int64_t>(std::ceil(s["battleData"]["duration"].get<double>()/2));
|
|
|
|
|
s["battleSnapshot"]={{"level",s["level"]},{"stages",s["stages"]},{"formation",s["formation"]},{"stageId",stageId},{"config",config()},{"legacy",false}};
|
|
|
|
|
} else if(action=="battle.finish"||action=="battle.abandon") {
|
|
|
|
|
if(!c.contains("activityId")||c["activityId"]!=s["pendingBattle"]||s["pendingBattle"]=="") return "activity_missing";
|
|
|
|
|
bool abandon=action=="battle.abandon";
|
|
|
|
|
if(!abandon&&now<s["battleReadyAt"].get<int64_t>()) return "activity_not_ready";
|
|
|
|
|
result=s["battleData"];if(abandon) result["won"]=false;
|
|
|
|
|
int stageId=result["stageId"];bool first=result["won"].get<bool>()&&s["clearedStages"][stageId-1]==0;result["firstClear"]=first;
|
|
|
|
|
if(first) {
|
|
|
|
|
s["clearedStages"][stageId-1]=1;if(stageId==1) s["cleared"]=true;
|
|
|
|
|
for(const char* key:{"coins","candy","balls"}) s[key]=s[key].get<int>()+result["reward"][key].get<int>();
|
|
|
|
|
int chest=first_clear_chest(stageId);
|
|
|
|
|
if(chest>=0) {s["chests"][chest]=s["chests"][chest].get<int>()+1;result["chest"]=chest;}
|
|
|
|
|
}
|
|
|
|
|
result["id"]=s["pendingBattle"];s["lastBattleData"]=result;
|
|
|
|
|
s["lastBattle"]=s["pendingBattle"];s["pendingBattle"]="";
|
|
|
|
|
s.erase("battleData");s.erase("battleReadyAt");s.erase("battleSnapshot");
|
|
|
|
|
if(!abandon) daily_event(s,1);
|
|
|
|
|
} else {
|
|
|
|
|
if(s["pendingBattle"]!=""||s["pendingShot"]!="") return "pending_activity";
|
|
|
|
|
if(action=="stage.select") {
|
|
|
|
|
int id=integer(c,"stageId");if(id<1||id>static_cast<int>(config()["stages"].size())) return "invalid_stage";
|
|
|
|
|
if(id>unlocked(s)) return "stage_locked";s["selectedStage"]=id;
|
|
|
|
|
} else if(action=="camp.place"||action=="camp.store") {
|
|
|
|
|
const auto& camp=config().at("camp");
|
|
|
|
|
if(s["clearedStages"][camp["buildUnlockStage"].get<int>()-1]!=1) return "camp_build_locked";
|
|
|
|
|
int slot=integer(c,"slot");if(slot<0||slot>=camp["slots"].get<int>()) return "invalid_camp_slot";
|
|
|
|
|
if(action=="camp.store") {
|
|
|
|
|
if(s["campLayout"][slot]==-1) return "camp_slot_empty";
|
|
|
|
|
result={{"item",s["campLayout"][slot]},{"slot",slot},{"chargedCoins",0}};s["campLayout"][slot]=-1;
|
|
|
|
|
} else {
|
|
|
|
|
int item=integer(c,"item");if(item<0||item>=static_cast<int>(camp["decorations"].size())) return "invalid_decoration";
|
|
|
|
|
int target=s["campLayout"][slot];if(target>=0&&target!=item) return "camp_slot_occupied";
|
|
|
|
|
int cost=s["decorations"][item]==1?0:camp["decorations"][item]["coins"].get<int>();
|
|
|
|
|
if(s["coins"].get<int>()<cost) return "insufficient_coins";
|
|
|
|
|
s["coins"]=s["coins"].get<int>()-cost;s["decorations"][item]=1;
|
|
|
|
|
for(auto& old:s["campLayout"]) if(old==item) old=-1;
|
|
|
|
|
s["campLayout"][slot]=item;result={{"item",item},{"slot",slot},{"chargedCoins",cost}};
|
|
|
|
|
}
|
|
|
|
|
result["id"]=c["requestId"];
|
|
|
|
|
} else if(action=="camp.interact") {
|
|
|
|
|
if(s["clearedStages"][config()["camp"]["restUnlockStage"].get<int>()-1]!=1) return "camp_rest_locked";
|
|
|
|
|
int pet=integer(c,"pet");if(pet<0||pet>=static_cast<int>(config()["pets"].size())||s["stages"][pet]==0) return "invalid_pet";
|
|
|
|
|
s["restPet"]=pet;s["campInteractions"]=std::min(s["campInteractions"].get<int>(),999999999)+1;
|
|
|
|
|
result={{"id",c["requestId"]},{"pet",pet}};
|
|
|
|
|
daily_event(s,2);
|
|
|
|
|
} else if(action=="mail.read"||action=="mail.claim") {
|
|
|
|
|
auto id=c.at("mailId").get<std::string>();if(id.empty()||id.size()>64) return "invalid_input";
|
|
|
|
|
auto m=find_mail(s,id);if(!m) return "mail_not_found";
|
|
|
|
|
if(action=="mail.read") {(*m)["read"]=true;result={{"id",c["requestId"]},{"mailId",id}};}
|
|
|
|
|
else {
|
|
|
|
|
auto status=claim_mail(s,m);if(status!="claimed") return "mail_"+status;
|
|
|
|
|
result={{"id",c["requestId"]},{"mailId",id},{"reward",m->at("reward")},{"claimedCount",1}};
|
|
|
|
|
}
|
|
|
|
|
} else if(action=="mail.claimAll") {
|
|
|
|
|
const auto& ids=c.at("mailIds");if(!ids.is_array()||ids.empty()||ids.size()>20) return "invalid_input";
|
|
|
|
|
std::vector<std::string> unique;
|
|
|
|
|
for(const auto& value:ids) {auto id=value.get<std::string>();if(id.empty()||id.size()>64||std::find(unique.begin(),unique.end(),id)!=unique.end()) return "invalid_input";unique.push_back(id);}
|
|
|
|
|
J total={{"coins",0},{"candy",0},{"balls",0},{"tickets",0}},entries=J::array();int count=0;
|
|
|
|
|
for(const auto& id:unique) {
|
|
|
|
|
auto m=find_mail(s,id);auto status=claim_mail(s,m);entries.push_back({{"mailId",id},{"status",status}});
|
|
|
|
|
if(status=="claimed") {count++;for(const char* k:{"coins","candy","balls","tickets"}) total[k]=total[k].get<int>()+m->at("reward")[k].get<int>();}
|
|
|
|
|
}
|
|
|
|
|
result={{"id",c["requestId"]},{"reward",total},{"claimedCount",count},{"entries",entries}};
|
|
|
|
|
} else if(action=="shop.buy") {
|
|
|
|
|
if(integer(c,"day")!=s["dailyDay"].get<int>()) return "shop_expired";
|
|
|
|
|
int item=integer(c,"item");if(item<0||item>=3) return "invalid_goods";
|
|
|
|
|
const auto& goods=config()["shop"]["goods"][item];int unlock=goods["unlockStage"];
|
|
|
|
|
if(unlock>0&&s["clearedStages"][unlock-1]!=1) return "goods_locked";
|
|
|
|
|
if(s["shopBought"][item].get<int>()>=goods["dailyLimit"].get<int>()) return "shop_limit";
|
|
|
|
|
int cost=goods["coins"];if(s["coins"].get<int>()<cost) return "insufficient_coins";
|
|
|
|
|
s["coins"]=s["coins"].get<int>()-cost;grant(s,goods["reward"]);s["shopBought"][item]=s["shopBought"][item].get<int>()+1;
|
|
|
|
|
result={{"id",c["requestId"]},{"item",item},{"day",s["dailyDay"]},{"chargedCoins",cost},{"reward",goods["reward"]}};
|
|
|
|
|
} else if(action=="daily.claim") {
|
|
|
|
|
if(integer(c,"day")!=s["dailyDay"].get<int>()) return "daily_expired";
|
|
|
|
|
int item=integer(c,"item");if(item<0||item>=3) return "invalid_task";
|
|
|
|
|
if(s["dailyClaimed"][item]==1) return "reward_claimed";
|
|
|
|
|
if(s["dailyProgress"][item].get<int>()<config()["daily"]["tasks"][item]["target"].get<int>()) return "task_incomplete";
|
|
|
|
|
auto reward=config()["daily"]["tasks"][item]["reward"];grant(s,reward);s["dailyClaimed"][item]=1;
|
|
|
|
|
result={{"id",c["requestId"]},{"item",item},{"day",s["dailyDay"]},{"reward",reward}};
|
|
|
|
|
} else if(action=="signin.claim") {
|
|
|
|
|
int item=integer(c,"item");if(item<0||item>=7) return "invalid_signin";
|
|
|
|
|
if(item>=s["signinDays"].get<int>()) return "signin_locked";
|
|
|
|
|
if(s["signinClaimed"][item]==1) return "reward_claimed";
|
|
|
|
|
auto reward=config()["daily"]["signin"][item];grant(s,reward);s["signinClaimed"][item]=1;
|
|
|
|
|
result={{"id",c["requestId"]},{"item",item},{"reward",reward}};
|
|
|
|
|
} else if(action=="chest.claimDaily") {
|
|
|
|
|
const auto& rewards=config()["rewards"];int chest=rewards["dailyChest"];
|
|
|
|
|
if(s["chestDailyClaimed"].get<int>()==1) return "chest_claimed";
|
|
|
|
|
s["chestDailyClaimed"]=1;s["chests"][chest]=s["chests"][chest].get<int>()+1;
|
|
|
|
|
result={{"id",c["requestId"]},{"chest",chest},{"day",s["dailyDay"]},{"count",1}};
|
|
|
|
|
} else if(action=="chest.open") {
|
|
|
|
|
const auto& rewards=config()["rewards"];
|
|
|
|
|
int chest=integer(c,"chest");if(chest<0||chest>=static_cast<int>(rewards["chests"].size())) return "invalid_chest";
|
|
|
|
|
if(s["chests"][chest].get<int>()<1) return "chest_missing";
|
|
|
|
|
const auto& def=rewards["chests"][chest];std::string kind=def["kind"];
|
|
|
|
|
J reward=def["contents"];
|
|
|
|
|
if(kind=="choice") {
|
|
|
|
|
if(!c.contains("choice")) return "invalid_choice";
|
|
|
|
|
int choice=integer(c,"choice");if(choice<0||choice>=static_cast<int>(def["choices"].size())) return "invalid_choice";
|
|
|
|
|
reward=def["choices"][choice]["reward"];
|
|
|
|
|
}
|
|
|
|
|
s["chests"][chest]=s["chests"][chest].get<int>()-1;grant_chest(s,reward);
|
|
|
|
|
result={{"id",c["requestId"]},{"chest",chest},{"kind",kind},{"remaining",s["chests"][chest]},{"chestReward",reward}};
|
|
|
|
|
if(kind=="choice") result["choice"]=integer(c,"choice");
|
|
|
|
|
} else if(action=="capture") {
|
|
|
|
|
if(s["tickets"].get<int>()<1) return "insufficient_tickets";
|
|
|
|
|
result=capture(s,random);result["id"]=c["requestId"];s["lastCaptureData"]=result;
|
|
|
|
|
} else if(action=="train") {
|
|
|
|
|
int level=s["level"];if(level>=economy()["levelCap"].get<int>()) return "level_cap";
|
|
|
|
|
if(s["candy"].get<int>()<economy()["trainCost"].get<int>()*level) return "insufficient_candy";
|
|
|
|
|
s["candy"]=s["candy"].get<int>()-economy()["trainCost"].get<int>()*level;s["level"]=level+1;
|
|
|
|
|
} else if(action=="evolve") {
|
|
|
|
|
int pet=integer(c,"pet");if(pet<0||pet>=static_cast<int>(config()["pets"].size())) return "invalid_pet";
|
|
|
|
|
if(s["stages"][pet]!=1||s["level"].get<int>()<economy()["evolveLevel"].get<int>()||s["shards"][pet].get<int>()<economy()["evolveShards"].get<int>()||s["coins"].get<int>()<economy()["evolveCoins"].get<int>()) return "evolve_requirements";
|
|
|
|
|
s["shards"][pet]=s["shards"][pet].get<int>()-economy()["evolveShards"].get<int>();s["coins"]=s["coins"].get<int>()-economy()["evolveCoins"].get<int>();s["stages"][pet]=2;
|
|
|
|
|
} else if(action=="deploy") {
|
|
|
|
|
int pet=integer(c,"pet"),slot=integer(c,"slot");
|
|
|
|
|
if(pet<0||pet>=static_cast<int>(config()["pets"].size())||slot<0||slot>=gameplay::stage(s["selectedStage"])["lanes"].get<int>()*2||s["stages"][pet]==0) return "invalid_formation";
|
|
|
|
|
int old=-1;for(int i=0;i<6;i++) if(s["formation"][i]==pet) old=i;
|
|
|
|
|
if(old>=0) s["formation"][old]=s["formation"][slot];s["formation"][slot]=pet;
|
|
|
|
|
} else if(action=="undeploy") {
|
|
|
|
|
int slot=integer(c,"slot");if(slot<0||slot>=6||s["formation"][slot]==-1)return "invalid_formation";s["formation"][slot]=-1;
|
|
|
|
|
} else if(action=="autoDeploy") {
|
|
|
|
|
s["formation"]={-1,-1,-1,-1,-1,-1};int lanes=gameplay::stage(s["selectedStage"])["lanes"];
|
|
|
|
|
const auto& pets=config()["pets"];std::vector<int> used(pets.size(),0);
|
|
|
|
|
// Front line: closest-range owned partners first; back line: longest-range owned partners first.
|
|
|
|
|
auto fill=[&](int offset,bool front){
|
|
|
|
|
for(int lane=0;lane<lanes;lane++) {
|
|
|
|
|
int best=-1;float bestRange=front?1e9f:-1.f;
|
|
|
|
|
for(size_t pet=0;pet<pets.size();pet++) {
|
|
|
|
|
if(s["stages"][pet].get<int>()==0||used[pet])continue;
|
|
|
|
|
float range=pets[pet]["range"].get<float>();
|
|
|
|
|
if(front?(range<bestRange):(range>bestRange)){bestRange=range;best=static_cast<int>(pet);}
|
|
|
|
|
}
|
|
|
|
|
if(best>=0){s["formation"][lane*2+offset]=best;used[best]=1;}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
fill(0,true);fill(1,false);
|
|
|
|
|
}
|
|
|
|
|
else if(action=="goal.claim") {
|
|
|
|
|
if(!s["cleared"].get<bool>()) return "goal_incomplete";if(s["goalClaimed"].get<bool>()) return "goal_claimed";
|
|
|
|
|
s["goalClaimed"]=true;s["balls"]=s["balls"].get<int>()+economy()["goalBalls"].get<int>();
|
|
|
|
|
result={{"id",c["requestId"]},{"reward",{{"coins",0},{"candy",0},{"balls",economy()["goalBalls"]}}}};
|
|
|
|
|
} else return "unknown_action";
|
|
|
|
|
}
|
|
|
|
|
return "";
|
|
|
|
|
}
|
|
|
|
|
J response(const J& s,int64_t now) {
|
|
|
|
|
J visible=s;for(const char* k:{"shotData","battleData","battleSnapshot","shotReadyAt","battleReadyAt","lastBattleData","lastCaptureData"}) visible.erase(k);
|
|
|
|
|
J r{{"ok",true},{"state",visible},{"serverTime",now},{"configVersion",config()["version"]},{"config",config()}};
|
|
|
|
|
r["capture"]=capture_info(s);
|
|
|
|
|
r["daily"]={{"day",s["dailyDay"]},{"resetAt",(s["dailyDay"].get<int64_t>()+1)*86400-config()["daily"]["utcOffsetSeconds"].get<int>()}};
|
|
|
|
|
r["rewards"]={{"dailyClaimed",s["chestDailyClaimed"]}};
|
|
|
|
|
if(s.contains("lastCaptureData")) r["lastCaptureResult"]=s["lastCaptureData"];
|
|
|
|
|
if(s.contains("lastBattleData")) r["lastBattleResult"]=s["lastBattleData"];
|
|
|
|
|
if(s["pendingShot"]!="") {r["shot"]=s["shotData"];r["shot"]["id"]=s["pendingShot"];r["shot"]["readyAt"]=s["shotReadyAt"];}
|
|
|
|
|
if(s["pendingBattle"]!="") {
|
|
|
|
|
r["battle"]={{"id",s["pendingBattle"]},{"readyAt",s["battleReadyAt"]},{"snapshot",s["battleSnapshot"]}};
|
|
|
|
|
}
|
|
|
|
|
return r;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
db::GameReply db::Database::game_request(const std::string& token,const std::string& command,int64_t now,const std::function<uint32_t(uint32_t)>& random) {
|
|
|
|
|
std::lock_guard<std::recursive_mutex> guard(mutex_);
|
|
|
|
|
auto pid=pid_for_token(token);if(!pid) return {401,"{\"ok\":false,\"error\":\"unauthorized\"}"};
|
|
|
|
|
if(now<=0) now=static_cast<int64_t>(std::time(nullptr));
|
|
|
|
|
J c;std::string id;
|
|
|
|
|
try {
|
|
|
|
|
if(!command.empty()) {
|
|
|
|
|
if(command.size()>8192) throw std::invalid_argument("size");
|
|
|
|
|
c=J::parse(command);id=c.at("requestId").get<std::string>();
|
|
|
|
|
if(!identifier(id)||!c.at("action").is_string()) throw std::invalid_argument("id");
|
|
|
|
|
integer(c,"revision");
|
|
|
|
|
}
|
|
|
|
|
} catch(...) {return {400,"{\"ok\":false,\"error\":\"invalid_input\"}"};}
|
|
|
|
|
try {
|
|
|
|
|
Transaction tx(db_);J s;
|
|
|
|
|
{Statement q(db_,"SELECT state FROM game_states WHERE pid=?");q.number(1,*pid);if(q.step()==SQLITE_ROW) s=J::parse(q.column(0));else s=initial(now);}
|
|
|
|
|
auto previous=s;migrate(s,now);recover(s,now);sync_calendar(s,now);sync_mail(s,now);
|
|
|
|
|
J result=J::object(),r;std::string error;
|
|
|
|
|
bool replayed=false;
|
|
|
|
|
if(!command.empty()) {
|
|
|
|
|
Statement q(db_,"SELECT request,response FROM game_operations WHERE pid=? AND request_id=?");q.number(1,*pid);q.text(2,id);
|
|
|
|
|
if(q.step()==SQLITE_ROW) {
|
|
|
|
|
if(q.column(0)!=c.dump()) return {409,"{\"ok\":false,\"error\":\"request_id_conflict\"}"};
|
|
|
|
|
auto old=J::parse(q.column(1));result=old["result"];error=old.value("error","");replayed=true;
|
|
|
|
|
} else {
|
|
|
|
|
auto before=s;
|
|
|
|
|
try {error=apply(s,c,now,result,random);} catch(const J::exception&) {error="invalid_input";} catch(const std::invalid_argument&) {error="invalid_input";}
|
|
|
|
|
if(!error.empty()) s=before;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if(s!=previous) s["revision"]=previous["revision"].get<int>()+1;
|
|
|
|
|
r=response(s,now);r["result"]=result;r["ok"]=error.empty();r["error"]=error;r["replayed"]=replayed;r["action"]=command.empty()?"":c["action"].get<std::string>();
|
|
|
|
|
{Statement q(db_,"INSERT INTO game_states(pid,state) VALUES(?,?) ON CONFLICT(pid) DO UPDATE SET state=excluded.state");q.number(1,*pid);q.text(2,s.dump());q.step();}
|
|
|
|
|
if(!command.empty()&&!replayed) {
|
|
|
|
|
Statement q(db_,"INSERT INTO game_operations(pid,request_id,request,response) VALUES(?,?,?,?)");
|
|
|
|
|
q.number(1,*pid);q.text(2,id);q.text(3,c.dump());q.text(4,r.dump());q.step();
|
|
|
|
|
}
|
|
|
|
|
tx.commit();return {200,r.dump()};
|
|
|
|
|
} catch(...) {return {500,"{\"ok\":false,\"error\":\"storage_unavailable\"}"};}
|
|
|
|
|
}
|