Add rank, friends, fishing, and camp treasure with T2 studio models and skill presentation.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
#include "combat_slice.hpp"
|
||||
#include <cmath>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
using namespace combat_slice;
|
||||
using J=nlohmann::json;
|
||||
static int assertions=0;
|
||||
static void check(bool value,const char* message){++assertions;if(!value)throw std::runtime_error(message);}
|
||||
static int count(const J& events,const std::string& kind,int source=-1){int n=0;for(const auto& e:events)if(e["kind"]==kind&&(source<0||e.value("source",-1)==source))++n;return n;}
|
||||
static Unit actor(int id,int team,int z=0){Unit u;u.id=id;u.team=team;u.z=z;u.hp=u.maxHp=100000;return u;}
|
||||
static Skill attack(int windup=2,bool projectile=false){Skill s;s.id="test.basic";s.release=windup;s.recovery=2;s.cooldown=10;s.range=10000;s.value=12000;s.projectile=projectile;return s;}
|
||||
static void tests(){
|
||||
{
|
||||
auto a=actor(0,0),b=actor(100,1,1600);a.basic=attack(2,true);Battle sim({a,b});
|
||||
sim.step();check(sim.unit(100).hp==100000,"cast start has no damage");sim.step();sim.step();
|
||||
check(count(sim.events(),"projectile")==1&&sim.unit(100).hp==100000,"release launches without early damage");sim.step();check(sim.unit(100).hp==100000,"flight is pending");sim.step();
|
||||
check(sim.unit(100).hp==88000&&count(sim.events(),"hit")==1,"one arrival causes exactly one damage");
|
||||
auto trace=sim.trace();for(const auto& e:trace["events"])check(e["seq"].get<int>()<static_cast<int>(trace["events"].size()),"sequence bounded");
|
||||
}
|
||||
{
|
||||
auto a=actor(0,0),b=actor(100,1,1000);a.basic=attack();a.ability=attack(0);a.ability.id="test.shield";a.ability.ability=true;a.ability.action=Action::Shield;a.ability.duration=3;a.ability.value=20000;
|
||||
b.basic=attack(0);Battle sim({a,b});sim.step();
|
||||
check(sim.unit(0).shield==8000&&sim.unit(0).hp==100000,"zero windup shield absorbs same-tick hit");
|
||||
check(count(sim.events(),"cast_start",0)==1&&count(sim.events(),"shield")==1,"ability excludes concurrent basic");
|
||||
sim.step();sim.step();check(count(sim.events(),"cast_start",0)==2,"channel opens only at recovery end");sim.step();check(sim.unit(0).shield==0&&count(sim.events(),"shield_expired")==1,"shield expires on its absolute tick");
|
||||
}
|
||||
{
|
||||
auto a=actor(0,0),b=actor(100,1,1000);a.basic=attack(4);b.basic=attack(0);b.basic.value=100000;
|
||||
Battle sim({a,b});sim.run();check(count(sim.events(),"release",0)==0&&count(sim.events(),"cancel",0)==1,"death before release cancels once");check(count(sim.events(),"death")==1,"death is emitted once");
|
||||
}
|
||||
{
|
||||
auto a=actor(0,0),b=actor(100,1,1600);a.basic=attack(0,true);a.basic.value=100000;b.basic=attack(0);b.basic.value=100000;
|
||||
Battle sim({a,b});sim.run();check(sim.unit(0).hp==0&&sim.unit(100).hp==0&&count(sim.events(),"hit",0)==1,"released projectile survives caster death");
|
||||
}
|
||||
{
|
||||
auto a=actor(0,0),b=actor(100,1,1600),c=actor(1,0);a.basic=attack(0,true);c.basic=attack(1);c.basic.value=100000;
|
||||
Battle sim({a,b,c});sim.run();check(count(sim.events(),"expired",0)==1&&count(sim.events(),"hit",0)==0,"target lost never retargets or damages twice");
|
||||
}
|
||||
{
|
||||
auto a=actor(0,0),b=actor(100,1,500);a.basic=attack(0);a.basic.cooldown=3;
|
||||
Battle sim({a,b});for(int i=0;i<7;i++)sim.step();check(count(sim.events(),"cast_start",0)==3,"cooldown boundary uses absolute integer tick");
|
||||
auto once=sim.events().size();sim.run();sim.step();check(sim.finished()&&sim.events().size()>=once&&count(sim.events(),"battle_end")==1,"finished battle is idempotent");
|
||||
}
|
||||
{
|
||||
auto a=actor(0,0),b=actor(100,1,500);a.basic=attack(0);a.element=1;b.element=2;b.defense=1000;Settings cfg;cfg.elements=true;
|
||||
Battle sim({a,b},cfg);sim.step();check(sim.unit(100).hp==86000,"element multiplier and defense round in fixed units");
|
||||
auto x=actor(0,0),y=actor(100,1,500);x.basic=attack(0);x.basic.value=999;Battle raw({x,y});raw.step();check(raw.unit(100).hp==99001,"elements off keeps fractional raw damage");
|
||||
}
|
||||
{
|
||||
auto a=actor(0,0),b=actor(100,1,1000);a.basic=attack(0,true);a.basic.value=100000;b.hp=b.maxHp=5000;Battle first({a,b});Battle second({b,a});
|
||||
check(first.run()==second.run(),"input ordering does not change deterministic trace");
|
||||
bool rejected=false;try{Battle bad({a,a});}catch(const std::invalid_argument&){rejected=true;}check(rejected,"duplicate entity ids rejected");
|
||||
}
|
||||
{
|
||||
auto a=actor(0,0),b=actor(100,1,1000);a.ability=attack(0);a.ability.action=Action::Shield;a.ability.ability=true;a.ability.value=10000;a.ability.duration=6;a.ability.cooldown=3;
|
||||
Battle sim({a,b});for(int i=0;i<4;i++)sim.step();check(sim.unit(0).shield==10000&&sim.unit(0).shieldExpiry==9,"shield refresh replaces expiry and never adds values");
|
||||
b.speed=100;Battle moving({a,b});moving.step();check(moving.unit(100).z==1000,"enemy stops at blocker");
|
||||
a.basic=attack(0,true);a.ability={};b.z=4000;Settings limit;limit.projectileLifetime=1;Battle expiring({a,b},limit);expiring.step();check(count(expiring.events(),"expired")==1,"projectile lifetime bounded");
|
||||
}
|
||||
}
|
||||
static J read(const char* path){std::ifstream in(path,std::ios::binary);if(!in)throw std::runtime_error(std::string("read ")+path);J j;in>>j;return j;}
|
||||
static int ticks(double seconds){return static_cast<int>(std::ceil(seconds*20-1e-8));}
|
||||
static int64_t fixed(double value){return std::llround(value*1000);}
|
||||
static J sample(const J& gameplay,const J& proposal,const J& scenario){
|
||||
std::vector<Unit> units;const int tier=scenario.at("tier");
|
||||
auto resolve=[&](const J& binding,const J& p,bool ability){
|
||||
J t;for(const auto& v:proposal["templates"])if(v["id"]==binding["template"])t=v;
|
||||
if(t.is_null())throw std::runtime_error("missing template");
|
||||
if(t["actions"].size()!=1)throw std::runtime_error("slice supports one action");
|
||||
auto action=t["actions"][0];if(action["type"]!="damage"&&action.value("statusId","")!="shield_main")throw std::runtime_error("unsupported slice action");
|
||||
auto timing=binding.value("timing",t["timing"]);auto data=ability?p["ability"]:p;Skill s;s.id=binding["id"];s.ability=ability;s.projectile=t["delivery"]=="projectile";s.release=timing["releaseTicks"];s.recovery=timing["recoveryTicks"];
|
||||
s.cooldown=ticks(data["cooldown"]);s.range=static_cast<int>(fixed(data["range"]));s.value=fixed(p["attack"]);
|
||||
if(ability){s.targets=data[tier==2?"t2Targets":"targets"];double value=data[tier==2?"t2":"t1"];s.value=data["kind"]=="shield"?fixed(value):fixed(p["attack"].get<double>()*value);s.duration=ticks(data[tier==2?"t2Duration":"duration"]);}
|
||||
if(action["type"]=="apply_status")s.action=Action::Shield;
|
||||
return s;
|
||||
};
|
||||
for(int pet=0;pet<2;pet++){
|
||||
auto p=gameplay["pets"][pet];auto u=actor(pet,0,pet==0?0:-2000);u.art=pet;u.hp=u.maxHp=fixed(p["hp"]);u.element=p["element"];
|
||||
for(const auto& binding:proposal["skills"])if(binding["actorKind"]=="pet"&&binding["actorId"]==pet){bool ability=binding["slot"]=="ability";(ability?u.ability:u.basic)=resolve(binding,p,ability);}
|
||||
units.push_back(u);
|
||||
}
|
||||
int type=scenario["enemyType"],id=100;auto d=gameplay["enemies"][type];
|
||||
for(int z:scenario["enemyZ"]){auto u=actor(id++,1,z);u.art=type;u.speed=static_cast<int>(std::llround(d["speed"].get<double>()*50));u.hp=u.maxHp=fixed(d["hp"].get<double>()*scenario["enemyHpScale"].get<double>());u.defense=fixed(d["defense"]);u.element=0;
|
||||
for(const auto& binding:proposal["skills"])if(binding["actorKind"]=="enemy"&&binding["actorId"]==type){J p=d;p["range"]=1;u.basic=resolve(binding,p,false);}units.push_back(u);}
|
||||
Settings settings;settings.maxTicks=scenario["maxTicks"];settings.elements=scenario["elementsEnabled"];settings.projectileSpeed=proposal["projectile"]["speedMmPerTick"];settings.projectileLifetime=proposal["projectile"]["maxLifetimeTicks"];
|
||||
settings.advantage=static_cast<int>(std::llround(gameplay["elements"]["advantage"].get<double>()*10000));settings.disadvantage=static_cast<int>(std::llround(gameplay["elements"]["disadvantage"].get<double>()*10000));
|
||||
Battle battle(units,settings);auto result=battle.run();result["scenario"]=scenario;result["frozenGameplay"]=gameplay;result["skillDefinitions"]=proposal;
|
||||
check(count(result["events"],"projectile",1)>0&&count(result["events"],"hit",1)>0,"sample fire launch and impact");
|
||||
check(count(result["events"],"shield",0)>0&&count(result["events"],"hit",0)>0,"sample shield and rock melee");
|
||||
bool absorbed=false;for(const auto& e:result["events"])if(e.value("absorbed",int64_t(0))>0)absorbed=true;check(absorbed,"sample shield absorbs an enemy attack");return result;
|
||||
}
|
||||
int main(int argc,char** argv){try{tests();if(argc==5){auto trace=sample(read(argv[1]),read(argv[2]),read(argv[3]));std::ofstream out(argv[4],std::ios::binary);out<<trace.dump(2);check(out.good(),"trace exported");std::cout<<"trace ticks="<<trace["lastTick"]<<" events="<<trace["events"].size()<<" won="<<trace["won"]<<"\n";}else if(argc!=1)throw std::runtime_error("usage: combat_slice_tests [gameplay proposal scenario output]");std::cout<<"PASS "<<assertions<<" skill-slice assertions\n";return 0;}catch(const std::exception& e){std::cerr<<"FAIL "<<e.what()<<"\n";return 1;}}
|
||||
Reference in New Issue
Block a user