迁移独立 gameFramework 核心与 V1 存档规则

This commit is contained in:
2026-11-13 21:32:44 +08:00
parent a513c2f1d3
commit 55262351ab
22 changed files with 52156 additions and 0 deletions
+5
View File
@@ -9,6 +9,11 @@
/Docs/Production/Licenses/** -text
/Docs/QA/** -text
# Keep whitespace from upstream originals and license texts unchanged.
/SourceAssets/** -whitespace
/gameFramework/third_party/** -whitespace
/Docs/Production/Licenses/** -whitespace
*.uasset binary
*.umap binary
*.blend binary
+132
View File
@@ -0,0 +1,132 @@
{
"equipment": [
{
"id": "coil",
"slot": 0,
"mass": 6,
"power": 6,
"damage": 47,
"magazineSize": 12,
"reloadSeconds": 1.8,
"fireRate": 1.7,
"speedFactor": 1,
"guided": 0
},
{
"id": "composite_guard",
"slot": 3,
"mass": 2,
"power": 2,
"damage": 65,
"fireRate": 1,
"speedFactor": 1,
"guided": 0
},
{
"id": "efficient",
"slot": 2,
"mass": 1,
"power": 2,
"damage": 65,
"fireRate": 1,
"speedFactor": 1,
"guided": 0
},
{
"id": "guided",
"slot": 1,
"mass": 2,
"power": 3,
"damage": 65,
"fireRate": 1,
"speedFactor": 1,
"guided": 1
},
{
"id": "pulse",
"slot": 1,
"mass": 3,
"power": 4,
"damage": 65,
"fireRate": 1,
"speedFactor": 1,
"guided": 0
},
{
"id": "rapid",
"slot": 0,
"mass": 4,
"power": 3,
"damage": 15,
"magazineSize": 36,
"reloadSeconds": 2,
"fireRate": 7,
"speedFactor": 1,
"guided": 0
},
{
"id": "reactive_guard",
"slot": 3,
"mass": 4,
"power": 4,
"damage": 65,
"fireRate": 1,
"speedFactor": 1,
"guided": 0
},
{
"id": "siege",
"slot": 0,
"mass": 9,
"power": 4,
"damage": 90,
"magazineSize": 6,
"reloadSeconds": 2.6,
"fireRate": 1,
"speedFactor": 1,
"guided": 0
},
{
"id": "vector",
"slot": 2,
"mass": 2,
"power": 4,
"damage": 65,
"fireRate": 1,
"speedFactor": 1.2,
"guided": 0
}
],
"chassis": [
{
"id": "chassis_0",
"speed": 8,
"boostSpeed": 24,
"boostDuration": 0.48,
"lift": 5.5,
"armor": 110,
"massCapacity": 14,
"powerCapacity": 16
},
{
"id": "chassis_1",
"speed": 6,
"boostSpeed": 18,
"boostDuration": 0.35,
"lift": 0,
"armor": 180,
"massCapacity": 20,
"powerCapacity": 19
},
{
"id": "chassis_2",
"speed": 4.2,
"boostSpeed": 12,
"boostDuration": 0.25,
"lift": 0,
"armor": 300,
"massCapacity": 28,
"powerCapacity": 23
}
]
}
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2013-2025 Niels Lohmann
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
File diff suppressed because it is too large Load Diff
Binary file not shown.
+16
View File
@@ -0,0 +1,16 @@
param([string]$Configuration='Release',[switch]$SkipTests)
$ErrorActionPreference='Stop'
$projectRoot=Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
$cmakeCommand=Get-Command cmake -ErrorAction SilentlyContinue
if($cmakeCommand){$cmakeExecutable=$cmakeCommand.Source}else{$cmakeExecutable='C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe'}
if(-not(Test-Path -LiteralPath $cmakeExecutable)){throw 'CMake not available'}
$buildDirectory=Join-Path $projectRoot 'Builds\gameFramework\vs2022-x64'
& $cmakeExecutable -S (Join-Path $projectRoot 'gameFramework') -B $buildDirectory -G 'Visual Studio 17 2022' -A x64
if($LASTEXITCODE -ne 0){throw 'Core configuration failed'}
& $cmakeExecutable --build $buildDirectory --config $Configuration --parallel 2
if($LASTEXITCODE -ne 0){throw 'Core compilation failed'}
if(-not $SkipTests){
$ctestExecutable=Join-Path (Split-Path $cmakeExecutable) 'ctest.exe'
& $ctestExecutable --test-dir $buildDirectory -C $Configuration --output-on-failure
if($LASTEXITCODE -ne 0){throw 'Core behavior tests failed'}
}
+60
View File
@@ -0,0 +1,60 @@
"""Copy project-owned interchange sources and pin the portable JSON dependency."""
from pathlib import Path
import hashlib, json, re, shutil, urllib.request, subprocess, tarfile
ROOT = Path(__file__).resolve().parents[2]
UNITY = Path('D:/vibeCoding/unityRouge')
def write(path, text):
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(text, encoding='utf-8', newline='\n')
def main():
originals = ROOT/'SourceAssets/ThirdParty/nlohmann/json/3.12.0'
originals.mkdir(parents=True, exist_ok=True)
if not all((originals/name).exists() for name in ('json.hpp','LICENSE.MIT')):
archive=originals/'json.tar.xz'
url='https://github.com/nlohmann/json/releases/download/v3.12.0/json.tar.xz'
if not archive.exists():
subprocess.run(['curl.exe','--fail','--location','--retry','1','--connect-timeout','5','--max-time','45','--output',str(archive),url],check=True)
assert hashlib.sha256(archive.read_bytes()).hexdigest()=='42f6e95cad6ec532fd372391373363b62a14af6d771056dbfc86160e6dfff7aa'
with tarfile.open(archive) as package:
for name,tail in [('json.hpp','single_include/nlohmann/json.hpp'),('LICENSE.MIT','LICENSE.MIT')]:
matches=[m for m in package.getmembers() if m.isfile() and (m.name.endswith('/'+tail) or m.name==tail)]
member=min(matches,key=lambda m:len(m.name))
(originals/name).write_bytes(package.extractfile(member).read())
digest = hashlib.sha256((originals/'json.hpp').read_bytes()).hexdigest()
assert digest == 'aaf127c04cb31c406e5b04a63f1ae89369fccde6d8fa7cdda1ed4f32dfc5de63'
for name in ['json.hpp','LICENSE.MIT']:
target=ROOT/'gameFramework/third_party/nlohmann'/name
target.parent.mkdir(parents=True, exist_ok=True); shutil.copy2(originals/name,target)
entries=[]
for category, folder in [('equipment','Equipment'),('chassis','Mechs')]:
for path in sorted((UNITY/'Assets/Game/Data'/folder).glob('*.asset')):
data={}
for line in path.read_text(encoding='utf-8-sig').splitlines():
m=re.match(r'^ (\w+): (.+)$',line)
if not m: continue
key,value=m.groups()
if key in ('id','slot','mass','power','damage','magazineSize','reloadSeconds','fireRate','speedFactor','guided','speed','boostSpeed','boostDuration','lift','armor','massCapacity','powerCapacity'):
try:value=float(value) if '.' in value else int(value)
except ValueError:pass
data[key]=value
if 'id' in data: entries.append((category,data))
catalog={'equipment':[d for c,d in entries if c=='equipment'], 'chassis':[d for c,d in entries if c=='chassis']}
assert len(catalog['equipment'])==9 and len(catalog['chassis'])==3
write(ROOT/'SourceAssets/Data/Definitions.json',json.dumps(catalog,ensure_ascii=False,indent=2)+'\n')
write(ROOT/'gameFramework/src/DefaultCatalog.inc','R"GFDEFS('+json.dumps(catalog,ensure_ascii=False,separators=(',',':'))+')GFDEFS"\n')
records=[]
selections=[('SourceAssets/Blender','SourceAssets/Blender'),('Assets/Game/Art/Models','SourceAssets/Exports/Unity'),('Assets/Game/UI/Icons/Equipment','SourceAssets/UI/Icons/Equipment'),('Assets/Game/UI/Illustrations','SourceAssets/UI/Illustrations'),('Assets/Game/Localization/Tables','SourceAssets/Localization/Tables'),('SourceAssets/ThirdParty/Adobe/NotoSansSC','SourceAssets/ThirdParty/Adobe/NotoSansSC'),('Docs/Production/Licenses/Adobe/NotoSansSC','Docs/Production/Licenses/Adobe/NotoSansSC')]
for source,target in selections:
for path in (UNITY/source).rglob('*'):
if not path.is_file() or path.suffix not in ('.blend','.fbx','.svg','.json','.ttf','.txt'): continue
dest=ROOT/target/path.relative_to(UNITY/source)
dest.parent.mkdir(parents=True,exist_ok=True);shutil.copy2(path,dest)
records.append({'source':str(path.relative_to(UNITY)).replace('\\','/'),'destination':str(dest.relative_to(ROOT)).replace('\\','/'),'sha256':hashlib.sha256(dest.read_bytes()).hexdigest()})
records.append({'author':'Niels Lohmann','version':'3.12.0','license':'MIT','source':'https://github.com/nlohmann/json/releases/tag/v3.12.0','destination':str(originals.relative_to(ROOT)).replace('\\','/'),'sha256':digest})
write(ROOT/'Docs/Production/AssetSources.json',json.dumps({'date':'2026-11-13','unityBaseline':'463d898','sources':records},ensure_ascii=False,indent=2)+'\n')
print(f'Prepared {len(records)} source records, 9 equipment definitions, 3 chassis; JSON dependency hash verified')
if __name__=='__main__': main()
+20
View File
@@ -0,0 +1,20 @@
"""Portable core boundary check. Run independently of any engine tooling."""
import json
import re
import sys
from pathlib import Path
root=Path(__file__).resolve().parents[2]
core=root/"gameFramework"
forbidden=re.compile(r'\b(?:CoreMinimal|UObject|UCLASS|USTRUCT|UPROPERTY|FString|FVector|TArray|TMap|UnrealBuildTool)\b|\.generated\.h|\.uproject|\.Build\.cs|Engine/Source')
failures=[]
checked=[]
for path in core.rglob("*"):
if path.is_file() and path.suffix in (".cpp",".hpp",".h",".txt",".inc") and "third_party" not in path.parts:
checked.append(str(path.relative_to(root)).replace("\\","/"))
for index,line in enumerate(path.read_text(encoding="utf-8").splitlines(),1):
if forbidden.search(line):failures.append({"file":str(path),"line":index})
report={"passed":not failures,"filesChecked":checked,"violations":failures,"method":"Source and public boundary scan; independent build additionally required"}
dest=root/"Docs/QA/UE-Migration/core-boundary.json";dest.parent.mkdir(parents=True,exist_ok=True);dest.write_text(json.dumps(report,indent=2)+"\n",encoding="utf-8")
print(f"Boundary: {len(checked)} files, {len(failures)} violations")
sys.exit(bool(failures))
+22
View File
@@ -0,0 +1,22 @@
param([string]$CMake='')
$ErrorActionPreference='Stop'
$root=(Resolve-Path (Join-Path $PSScriptRoot '../..')).Path
if(-not $CMake){$CMake='C:/Program Files/Microsoft Visual Studio/2022/Community/Common7/IDE/CommonExtensions/Microsoft/CMake/CMake/bin/cmake.exe'}
$proofRoot=Join-Path $root 'Builds/IndependentCoreProof'
New-Item -ItemType Directory -Force $proofRoot | Out-Null
# Only the portable source tree is present. No Source/, .uproject, UHT or UBT is copied.
Copy-Item -LiteralPath (Join-Path $root 'gameFramework') -Destination $proofRoot -Recurse -Force
$removed=@()
Get-ChildItem Env: | Where-Object {$_.Name -match '^(UE_|UNREAL|UBT)'} | ForEach-Object {$removed+=$_.Name;Remove-Item -LiteralPath ('Env:'+$_.Name)}
& $CMake -S (Join-Path $proofRoot 'gameFramework') -B (Join-Path $proofRoot 'build') -G 'Visual Studio 17 2022' -A x64
if($LASTEXITCODE -ne 0){throw 'Independent configure failed'}
& $CMake --build (Join-Path $proofRoot 'build') --config Release --parallel 2
if($LASTEXITCODE -ne 0){throw 'Independent compilation failed'}
& (Join-Path (Split-Path $CMake) 'ctest.exe') --test-dir (Join-Path $proofRoot 'build') -C Release --output-on-failure
if($LASTEXITCODE -ne 0){throw 'Independent CTest failed'}
& (Join-Path $proofRoot 'build/Release/gameFrameworkTests.exe') (Join-Path $root 'Docs/QA/UE-Migration/independent-tests.json')
if($LASTEXITCODE -ne 0){throw 'Independent behavioral tests failed'}
$dependencyFiles=Get-ChildItem (Join-Path $proofRoot 'build') -Recurse -Filter 'CL.read.1.tlog'
$engineDependencies=@($dependencyFiles | Select-String -Pattern 'EPIC GAMES','UNREALENGINE','ENGINE\SOURCE' -SimpleMatch)
if($engineDependencies.Count){throw 'Unexpected engine dependency in compiler trace'}
[ordered]@{passed=$true;sourceTree=(Join-Path $proofRoot 'gameFramework');configuration='VS2022 x64 C++20 /MD Release';removedEngineEnvironmentNames=$removed;compilerDependencyTraces=$dependencyFiles.Count;engineDependencies=0;scope='Copied core-only tree, engine variables removed, CMake/MSVC/CTest only. UE remains installed on this workstation; no claim of a second clean machine.'} | ConvertTo-Json -Depth 5 | Set-Content -Encoding utf8 (Join-Path $root 'Docs/QA/UE-Migration/independent-build.json')
+23
View File
@@ -0,0 +1,23 @@
cmake_minimum_required(VERSION 3.24)
project(gameFramework VERSION 1.0 LANGUAGES CXX)
add_library(gameFramework STATIC src/Catalog.cpp src/Grid.cpp src/Profile.cpp src/Session.cpp src/Combat.cpp src/Encounter.cpp)
add_library(irons::gameFramework ALIAS gameFramework)
target_compile_features(gameFramework PUBLIC cxx_std_20)
target_include_directories(gameFramework PUBLIC include PRIVATE third_party)
set_target_properties(gameFramework PROPERTIES CXX_EXTENSIONS OFF MSVC_RUNTIME_LIBRARY MultiThreadedDLL)
if(MSVC)
target_compile_options(gameFramework PRIVATE /W4 /utf-8 /GR- /EHsc)
else()
target_compile_options(gameFramework PRIVATE -Wall -Wextra -Wpedantic)
endif()
option(GAMEFRAMEWORK_TESTS "Build engine-independent behavior tests" ON)
if(GAMEFRAMEWORK_TESTS)
enable_testing()
add_executable(gameFrameworkTests tests/BehaviorTests.cpp)
target_link_libraries(gameFrameworkTests PRIVATE irons::gameFramework)
set_target_properties(gameFrameworkTests PROPERTIES MSVC_RUNTIME_LIBRARY MultiThreadedDLL)
if(MSVC)
target_compile_options(gameFrameworkTests PRIVATE /utf-8 /EHsc /GR-)
endif()
add_test(NAME gameFrameworkBehavior COMMAND gameFrameworkTests)
endif()
+19
View File
@@ -0,0 +1,19 @@
# gameFramework
IRONSPORE 的独立标准 C++20 规则库。公共接口位于 `include/irons/gameFramework/Game.hpp`;库与测试均不需要 UE、UBT、UHT、蓝图或引擎头文件。
通用构建:
```sh
cmake -S gameFramework -B build-core -DCMAKE_BUILD_TYPE=Release
cmake --build build-core --config Release
ctest --test-dir build-core -C Release --output-on-failure
```
本项目 Windows 工具入口为 `Tools/Framework/Build.ps1`。UE 以 VS2022 x64、C++20、Release `/MD`、关闭 RTTI 的配置链接其产物;核心内部处理异常,不向宿主抛出。标准库结构跨模块使用,因此宿主需使用匹配的编译器、运行库和迭代器调试设置。
`Session` 是规则权威。UI 读取 const 快照,通过命令完成修改。`IProfileStorage` 与 `IIdGenerator` 由宿主提供;模拟时间、感知距离和视线等事实显式输入。核心不读取系统路径、时钟、引擎全局状态或 Actor。
需要持久化的命令先复制候选状态、校验、原子提交,再发布状态和领域事件。普通战斗 Tick 不逐帧写盘。V1 JSON、唯一 UID、预占、待整理区、主动舱以及机毁的两阶段结算均属于核心规则。正式进度和训练进度由宿主隔离。
默认目录数据从来源 Unity 基线的普通装备定义生成,保留 9 个装备 ID 和 3 个底盘。第三方 JSON 依赖为 nlohmann/json 3.12.0,许可见 `third_party/nlohmann/LICENSE.MIT`。其锁定来源与哈希在项目素材台账中登记。
@@ -0,0 +1,200 @@
#pragma once
#include <array>
#include <cstdint>
#include <functional>
#include <string>
#include <string_view>
#include <vector>
namespace irons::gameFramework {
enum class Code { Ok, Invalid, Missing, NoSpace, Locked, Budget, Funds, Duplicate, SaveFailed, Corrupt, Unsupported };
struct Result {
Code code = Code::Ok;
std::string detail;
explicit operator bool() const noexcept { return code == Code::Ok; }
static Result fail(Code code, std::string detail = {}) { return {code, std::move(detail)}; }
};
enum class Hold { Cargo, SafeCase, Warehouse, Inbox };
struct Item {
std::string uid, itemId;
int count=1, x=0, y=0;
bool rotated=false, borrowed=false;
};
struct Equipment {
std::string id;
int slot=0, magazineSize=36;
float mass=2, power=2, damage=15, reloadSeconds=2, fireRate=6, speedFactor=1;
bool guided=false;
};
struct Chassis {
std::string id;
float speed=6, boostSpeed=18, boostDuration=.35f, lift=0, armor=150, massCapacity=20, powerCapacity=18;
};
struct Recipe { std::string blueprint, item; int alloy=0, cells=0; };
struct Catalog {
std::vector<Equipment> equipment;
std::vector<Chassis> chassis;
const Equipment* part(std::string_view id) const noexcept;
const Equipment* part(int slot,int index) const noexcept;
int index(std::string_view id) const noexcept;
bool fits(int chassisIndex,const std::array<int,4>& parts) const noexcept;
static Catalog defaults();
static Result decode(std::string_view json,Catalog& out) noexcept;
};
struct Run {
std::string id;
bool active=false;
int seed=0, stage=0, branch=0, biome=0, chassis=1;
std::array<int,4> equipment{0,0,0,0};
float armor=0, energy=100;
bool podUsed=false, objectiveDone=false, routeCompleted=false;
int upgrades=0;
float elapsed=0, escortArmor=250;
std::vector<Item> cargo, safeCase, installed;
bool destructionPending=false;
std::vector<std::string> searched;
};
struct Profile {
int version=1;
std::string format="grid-warehouse";
int warehouseWidth=10, warehouseHeight=24;
std::vector<Item> warehouse, recoveryInbox;
int sorties=0, extractions=0, completed=0;
std::vector<std::string> unlocked, receipts;
Run run;
};
struct Prepared { std::string uid; Hold hold=Hold::Cargo; int count=1,x=0,y=0; bool rotated=false; };
struct Preparation { int chassis=1; std::array<std::string,4> selected; std::array<int,4> equipment{0,0,0,0}; std::vector<Prepared> items; };
struct IIdGenerator { virtual ~IIdGenerator()=default; virtual std::string next()=0; };
struct IProfileStorage {
virtual ~IProfileStorage()=default;
// Missing is distinct from corrupt/unreadable. The driver handles backup recovery.
virtual Result load(std::string& bytes)=0;
virtual Result commitAtomically(std::string_view bytes)=0;
};
struct DomainEvent { std::string key,runId,itemId; int amount=0; };
struct Combat;
struct EnemyState { std::string id; int type=0; float health=95,cooldown=0,age=0; bool terminal=false; };
struct EnemyDecision { bool fire=false,fly=false; float damage=0,moveSpeed=0; };
struct RangeState {int gates=0,hits=0;float started=0,seconds=0;};
struct FieldState {float extraction=-1,podProgress=0;bool ready=false;};
struct Grid {
static constexpr int Cargo=4, SafeCase=2, Pod=2;
static bool material(std::string_view id) noexcept;
static bool blueprint(std::string_view id) noexcept;
static int slot(std::string_view id) noexcept;
static std::string id(int slot,int index);
static bool known(std::string_view id) noexcept;
static Recipe recipe(std::string_view id);
static std::pair<int,int> size(std::string_view id,bool rotated=false) noexcept;
static int area(const Item& item) noexcept;
static int occupied(const std::vector<Item>& items) noexcept;
static bool fits(const std::vector<Item>& items,int width,int height,const Item& item,int x,int y,bool rotated) noexcept;
static bool place(std::vector<Item>& items,int width,int height,Item item,int x,int y,bool rotated);
static bool autoPlace(std::vector<Item>& items,int width,int height,Item item,bool preferred=false,bool allowOtherOrientation=true);
static bool podFits(const std::vector<Item>& items);
};
Result validate(const Profile& profile) noexcept;
std::string encode(const Profile& profile);
Result decode(std::string_view json,Profile& out) noexcept;
int total(const std::vector<Item>& items,std::string_view id) noexcept;
float radialDamage(float baseDamage,float distanceMeters,float radiusMeters)noexcept;
const Item* find(const std::vector<Item>& items,std::string_view uid) noexcept;
class Session {
Catalog catalog_;
IProfileStorage& storage_;
IIdGenerator& ids_;
Profile profile_;
Preparation preparation_;
Result last_;
std::vector<DomainEvent> events_;
std::vector<EnemyState> enemies_;
RangeState range_;
FieldState field_;
Result transact(const std::function<Result(Profile&)>& change,std::string_view event={},bool allowPending=false) noexcept;
Item make(std::string_view id,int count=1);
bool deposit(Profile& profile,Item item);
bool consume(Profile& profile,std::string_view id,int count);
void reconcile();
public:
Session(IProfileStorage& storage,IIdGenerator& ids,Catalog catalog=Catalog::defaults());
const Profile& state() const noexcept {return profile_;}
const Preparation& draft() const noexcept {return preparation_;}
const Catalog& catalog() const noexcept {return catalog_;}
const Result& lastResult() const noexcept {return last_;}
std::vector<DomainEvent> takeEvents();
Result load() noexcept;
Result save() noexcept;
Result resetDraft() noexcept;
Result selectChassis(int index) noexcept;
Result equip(std::string_view uid) noexcept;
Result borrow(int slot) noexcept;
Result prepare(std::string_view uid,Hold hold,bool rotated=false,int x=-1,int y=-1,int quantity=0) noexcept;
Result unprepare(std::string_view uid) noexcept;
bool reserved(std::string_view uid) const noexcept;
int available(std::string_view id) const noexcept;
Result moveWarehouse(std::string_view uid,int x,int y,bool rotated,int quantity=0) noexcept;
Result mergeWarehouse(std::string_view source,std::string_view target,int quantity) noexcept;
Result sortWarehouse() noexcept;
Result claim(std::string_view uid={}) noexcept;
Result discard(std::string_view uid,bool inbox=false) noexcept;
Result recycle(std::string_view uid) noexcept;
Result research(std::string_view blueprint) noexcept;
Result manufacture(std::string_view blueprint) noexcept;
Result begin(int seed,bool trial=false) noexcept;
Result addLoot(std::string_view id,int count,std::string_view sourceKey={},std::string_view offeredPart={}) noexcept;
Result moveItem(std::string_view uid,Hold from,Hold to,int x=-1,int y=-1,bool rotated=false,int quantity=0) noexcept;
Result mergeItem(std::string_view source,Hold from,std::string_view target,Hold to,int quantity) noexcept;
Result swapItems(std::string_view source,Hold from,std::string_view target,Hold to,bool rotated) noexcept;
Result uninstall(int slot,Hold to,int x,int y,bool rotated=false) noexcept;
Result install(std::string_view uid,Hold from) noexcept;
Result recoverPod(const std::vector<std::string>& selected) noexcept;
Result finish(bool destroyed) noexcept;
Result objective() noexcept;
Result nextStage(int branch) noexcept;
Result completeRoute() noexcept;
Result upgrade(int type) noexcept;
Result seedAcceptanceStock() noexcept;
void vitals(float armor,float energy,float elapsed,float escortArmor) noexcept;
void tick(float dt,Combat& combat,bool triggerReleased) noexcept;
bool fire(Combat& combat,bool secondary=false) noexcept;
bool boost(Combat& combat) noexcept;
bool guard(Combat& combat) noexcept;
bool reload(Combat& combat) noexcept;
void takeDamage(Combat& combat,float amount) noexcept;
float moveSpeed()const noexcept;
float shotDamage(bool secondary=false)const noexcept;
float lift(float seconds) noexcept;
void registerEnemy(std::string id,int type,bool terminal=false);
const EnemyState* enemy(std::string_view id)const noexcept;
bool damageEnemy(std::string_view id,float damage)noexcept;
EnemyDecision stepEnemy(std::string_view id,float dt,float distanceMeters,bool lineOfSight)noexcept;
void clearEnemies()noexcept;
void damageEscort(float damage)noexcept;
const RangeState& range()const noexcept{return range_;}
void rangeHit()noexcept;
void rangeGate(int gate)noexcept;
void rangeReset()noexcept;
const FieldState& field()const noexcept{return field_;}
void resetField()noexcept;
Result callExtraction()noexcept;
Result boardExtraction()noexcept;
Result advanceRecovery(float dt,bool held,bool stable,const std::vector<std::string>& selected)noexcept;
float blastRadius(bool secondary=false)const noexcept;
};
struct Combat {
int rounds=0;
float reload=0,cooldown=0,secondaryCooldown=0,guard=0,boost=0,boostCooldown=0;
bool freshTrigger=true;
std::string primaryId;
void apply(const Catalog& catalog,const Run& run) noexcept;
void tick(float dt,const Catalog& catalog,Run& run,bool triggerReleased) noexcept;
bool fire(const Catalog& catalog,Run& run,bool secondary=false) noexcept;
bool startReload(const Catalog& catalog,const Run& run) noexcept;
bool startBoost(const Catalog& catalog,Run& run) noexcept;
bool startGuard(Run& run) noexcept;
float damage(const Catalog& catalog,const Run& run,float incoming) const noexcept;
};
}
+39
View File
@@ -0,0 +1,39 @@
#include "irons/gameFramework/Game.hpp"
#include "nlohmann/json.hpp"
#include <cmath>
namespace irons::gameFramework {
const Equipment* Catalog::part(std::string_view id)const noexcept{for(const auto& p:equipment)if(p.id==id)return &p;return nullptr;}
const Equipment* Catalog::part(int slot,int index)const noexcept{if(index<0)return nullptr;try{return part(Grid::id(slot,index));}catch(...){return nullptr;}}
int Catalog::index(std::string_view id)const noexcept{int s=Grid::slot(id);if(s<0)return -1;for(int i=0;i<3;i++){auto p=part(s,i);if(p&&p->id==id)return i;}return -1;}
bool Catalog::fits(int index,const std::array<int,4>& parts)const noexcept{
if(index<0||index>=static_cast<int>(chassis.size()))return false;
float mass=0,power=0;for(int s=0;s<4;s++){auto p=part(s,parts[s]);if(!p&&parts[s]!=-1)return false;if(p){mass+=p->mass;power+=p->power;}}
return mass<=chassis[index].massCapacity&&power<=chassis[index].powerCapacity;
}
Result Catalog::decode(std::string_view text,Catalog& out)noexcept{
try{
auto json=nlohmann::json::parse(text);Catalog c;
for(const auto& j:json.at("equipment")){
Equipment e;e.id=j.at("id");e.slot=j.at("slot");
e.mass=j.value("mass",2.f);e.power=j.value("power",2.f);e.damage=j.value("damage",15.f);
e.magazineSize=j.value("magazineSize",36);e.reloadSeconds=j.value("reloadSeconds",2.f);
e.fireRate=j.value("fireRate",6.f);e.speedFactor=j.value("speedFactor",1.f);e.guided=j.value("guided",0)!=0;
if(Grid::slot(e.id)!=e.slot||e.magazineSize<1||e.reloadSeconds<=0||e.fireRate<=0||c.part(e.id))return Result::fail(Code::Invalid);
c.equipment.push_back(e);
}
for(const auto& j:json.at("chassis")){
Chassis f;f.id=j.at("id");f.speed=j.value("speed",6.f);f.boostSpeed=j.value("boostSpeed",18.f);
f.boostDuration=j.value("boostDuration",.35f);f.lift=j.value("lift",0.f);f.armor=j.value("armor",150.f);
f.massCapacity=j.value("massCapacity",20.f);f.powerCapacity=j.value("powerCapacity",18.f);c.chassis.push_back(f);
}
if(c.equipment.size()!=9||c.chassis.size()!=3)return Result::fail(Code::Invalid);
out=std::move(c);return {};
}catch(...){return Result::fail(Code::Invalid,"catalog");}
}
Catalog Catalog::defaults(){
static const char* definitions=
#include "DefaultCatalog.inc"
;Catalog c;auto result=decode(definitions,c);if(!result)throw std::runtime_error("Invalid compiled catalog");return c;
}
}
+29
View File
@@ -0,0 +1,29 @@
#include "irons/gameFramework/Game.hpp"
#include <algorithm>
#include <cmath>
namespace irons::gameFramework {
void Combat::apply(const Catalog& c,const Run& run)noexcept{
auto p=c.part(0,run.equipment[0]);std::string id=p?p->id:"";
if(id!=primaryId){primaryId=id;rounds=p?p->magazineSize:0;reload=0;cooldown=0;freshTrigger=true;}
}
void Combat::tick(float dt,const Catalog& c,Run& r,bool released)noexcept{
if(!std::isfinite(dt)||dt<0)return;apply(c,r);
cooldown=std::max(0.f,cooldown-dt);secondaryCooldown=std::max(0.f,secondaryCooldown-dt);
guard=std::max(0.f,guard-dt);boost=std::max(0.f,boost-dt);boostCooldown=std::max(0.f,boostCooldown-dt);
if(reload>0){reload=std::max(0.f,reload-dt);if(reload==0){auto p=c.part(0,r.equipment[0]);rounds=p?p->magazineSize:0;}}
if(released)freshTrigger=false;
if(boost==0&&guard==0)r.energy=std::min(100.f,r.energy+18.f*dt);
r.elapsed+=dt;
}
bool Combat::fire(const Catalog& c,Run& r,bool secondary)noexcept{
if(!r.active||r.destructionPending||r.armor<=0)return false;
if(secondary){if(!c.part(1,r.equipment[1])||secondaryCooldown>0||r.energy<15)return false;r.energy-=15;secondaryCooldown=2.5f;return true;}
auto p=c.part(0,r.equipment[0]);if(!p||freshTrigger||cooldown>0||reload>0||rounds<=0)return false;
rounds--;cooldown=1.f/p->fireRate;if(!rounds)reload=p->reloadSeconds;return true;
}
bool Combat::startReload(const Catalog& c,const Run& r)noexcept{auto p=c.part(0,r.equipment[0]);if(!r.active||r.destructionPending||r.armor<=0||!p||rounds>=p->magazineSize||reload>0)return false;reload=p->reloadSeconds;return true;}
bool Combat::startBoost(const Catalog& c,Run& r)noexcept{if(!r.active||r.destructionPending||r.armor<=0||!c.part(2,r.equipment[2])||r.energy<18||boostCooldown>0)return false;r.energy-=18;boost=c.chassis[r.chassis].boostDuration;boostCooldown=.65f;return true;}
bool Combat::startGuard(Run& r)noexcept{if(!r.active||r.destructionPending||r.armor<=0||r.equipment[3]<0||r.energy<20)return false;r.energy-=20;guard=2.5f;return true;}
float Combat::damage(const Catalog& c,const Run& r,float incoming)const noexcept{if(!std::isfinite(incoming)||incoming<0)return 0;auto p=c.part(3,r.equipment[3]);return incoming*(p&&p->id=="reactive_guard"?.72f:1.f)*(guard>0?.25f:1.f);}
}
+1
View File
@@ -0,0 +1 @@
R"GFDEFS({"equipment":[{"id":"coil","slot":0,"mass":6,"power":6,"damage":47,"magazineSize":12,"reloadSeconds":1.8,"fireRate":1.7,"speedFactor":1,"guided":0},{"id":"composite_guard","slot":3,"mass":2,"power":2,"damage":65,"fireRate":1,"speedFactor":1,"guided":0},{"id":"efficient","slot":2,"mass":1,"power":2,"damage":65,"fireRate":1,"speedFactor":1,"guided":0},{"id":"guided","slot":1,"mass":2,"power":3,"damage":65,"fireRate":1,"speedFactor":1,"guided":1},{"id":"pulse","slot":1,"mass":3,"power":4,"damage":65,"fireRate":1,"speedFactor":1,"guided":0},{"id":"rapid","slot":0,"mass":4,"power":3,"damage":15,"magazineSize":36,"reloadSeconds":2,"fireRate":7,"speedFactor":1,"guided":0},{"id":"reactive_guard","slot":3,"mass":4,"power":4,"damage":65,"fireRate":1,"speedFactor":1,"guided":0},{"id":"siege","slot":0,"mass":9,"power":4,"damage":90,"magazineSize":6,"reloadSeconds":2.6,"fireRate":1,"speedFactor":1,"guided":0},{"id":"vector","slot":2,"mass":2,"power":4,"damage":65,"fireRate":1,"speedFactor":1.2,"guided":0}],"chassis":[{"id":"chassis_0","speed":8,"boostSpeed":24,"boostDuration":0.48,"lift":5.5,"armor":110,"massCapacity":14,"powerCapacity":16},{"id":"chassis_1","speed":6,"boostSpeed":18,"boostDuration":0.35,"lift":0,"armor":180,"massCapacity":20,"powerCapacity":19},{"id":"chassis_2","speed":4.2,"boostSpeed":12,"boostDuration":0.25,"lift":0,"armor":300,"massCapacity":28,"powerCapacity":23}]})GFDEFS"
+48
View File
@@ -0,0 +1,48 @@
#include "irons/gameFramework/Game.hpp"
#include <algorithm>
#include <cmath>
namespace irons::gameFramework {
void Session::tick(float dt,Combat& combat,bool released)noexcept{if(profile_.run.active&&!profile_.run.destructionPending){combat.tick(dt,catalog_,profile_.run,released);if(std::isfinite(dt)&&dt>=0&&field_.extraction>=0&&!field_.ready){field_.extraction=std::max(0.f,field_.extraction-dt);if(field_.extraction==0){field_.ready=true;events_.push_back({"extraction.ready",profile_.run.id,{},{}});}}}}
bool Session::fire(Combat& combat,bool secondary)noexcept{return combat.fire(catalog_,profile_.run,secondary);}
bool Session::boost(Combat& combat)noexcept{return combat.startBoost(catalog_,profile_.run);}
bool Session::guard(Combat& combat)noexcept{return combat.startGuard(profile_.run);}
bool Session::reload(Combat& combat)noexcept{return combat.startReload(catalog_,profile_.run);}
void Session::takeDamage(Combat& combat,float damage)noexcept{if(profile_.run.active&&!profile_.run.destructionPending)profile_.run.armor=std::max(0.f,profile_.run.armor-combat.damage(catalog_,profile_.run,damage));}
float Session::moveSpeed()const noexcept{auto& r=profile_.run;auto p=catalog_.part(2,r.equipment[2]);return catalog_.chassis[r.chassis].speed*(p?p->speedFactor:1.f);}
float Session::shotDamage(bool secondary)const noexcept{auto& r=profile_.run;auto p=catalog_.part(secondary?1:0,r.equipment[secondary?1:0]);return p?p->damage*(1.f+.12f*r.upgrades):0.f;}
float Session::lift(float dt)noexcept{auto& r=profile_.run;if(!r.active||r.destructionPending||dt<0||!std::isfinite(dt)||r.energy<=0)return 0;float lift=catalog_.chassis[r.chassis].lift;if(lift>0)r.energy=std::max(0.f,r.energy-23.f*dt);return lift;}
void Session::registerEnemy(std::string id,int type,bool terminal){if(enemy(id))return;enemies_.push_back({std::move(id),type,type==3?900.f:type==2?180.f:type==1?70.f:95.f,1.f,0,terminal});}
const EnemyState* Session::enemy(std::string_view id)const noexcept{for(const auto& e:enemies_)if(e.id==id)return &e;return nullptr;}
bool Session::damageEnemy(std::string_view id,float amount)noexcept{
if(!std::isfinite(amount)||amount<0||!profile_.run.active)return false;
for(auto& e:enemies_)if(e.id==id){if(e.type>=4){rangeHit();return false;}if(e.health<=0)return false;e.health=std::max(0.f,e.health-amount);if(e.health==0){events_.push_back({"enemy.destroyed",profile_.run.id,e.id,e.type});if(e.terminal)completeRoute();return true;}return false;}return false;
}
EnemyDecision Session::stepEnemy(std::string_view id,float dt,float distance,bool visible)noexcept{
if(!std::isfinite(dt)||dt<0||!profile_.run.active||profile_.run.destructionPending)return {};
for(auto& e:enemies_)if(e.id==id){if(e.type>=4)return {};if(e.health<=0){if(e.terminal&&!profile_.run.routeCompleted)completeRoute();return {};}
e.age+=dt;e.cooldown=std::max(0.f,e.cooldown-dt);EnemyDecision decision;decision.fly=e.type==1;
decision.moveSpeed=distance>10?(e.type==3?1.4f:e.type==0?2.5f:1.2f):0;
if(distance<36&&visible&&e.cooldown==0){decision.fire=true;decision.damage=e.type==3?20.f:e.type==2?14.f:9.f;e.cooldown=e.type==3?.8f:e.type==1?1.8f:1.4f;}return decision;
}return {};
}
void Session::clearEnemies()noexcept{enemies_.clear();}
void Session::damageEscort(float amount)noexcept{if(profile_.run.active&&std::isfinite(amount)&&amount>0)profile_.run.escortArmor=std::max(0.f,profile_.run.escortArmor-amount);}
void Session::rangeHit()noexcept{if(profile_.run.active)range_.hits++;}
void Session::rangeGate(int gate)noexcept{if(!profile_.run.active)return;if(gate==0){range_.gates=1;range_.started=profile_.run.elapsed;range_.seconds=0;}else if(gate==range_.gates&&gate<=2){range_.gates++;if(gate==2)range_.seconds=profile_.run.elapsed-range_.started;}}
void Session::rangeReset()noexcept{range_={};}
void Session::resetField()noexcept{field_={};}
Result Session::callExtraction()noexcept{if(!profile_.run.active||profile_.run.destructionPending||profile_.run.armor<=0)return Result::fail(Code::Locked);if(field_.extraction>=0)return Result::fail(Code::Duplicate);field_.extraction=15;return {};}
Result Session::boardExtraction()noexcept{if(!field_.ready)return Result::fail(Code::Locked);return finish(false);}
Result Session::advanceRecovery(float dt,bool held,bool stable,const std::vector<std::string>& picked)noexcept{
if(!std::isfinite(dt)||dt<0)return Result::fail(Code::Invalid);
if(!held){field_.podProgress=0;return {};}
if(!profile_.run.active||profile_.run.destructionPending||profile_.run.podUsed){field_.podProgress=0;return Result::fail(Code::Locked);}
if(!stable){bool Interrupted=field_.podProgress>0;field_.podProgress=0;return Interrupted?Result::fail(Code::Invalid,"interrupted"):Result{};}
if(picked.empty())return Result::fail(Code::Invalid);
field_.podProgress=std::min(3.f,field_.podProgress+dt);if(field_.podProgress<3)return {};
auto result=recoverPod(picked);field_.podProgress=0;return result;
}
float Session::blastRadius(bool secondary)const noexcept{auto& r=profile_.run;auto p=catalog_.part(secondary?1:0,r.equipment[secondary?1:0]);if(!p)return 0;return p->id=="siege"?5.f:p->id=="guided"?6.f:p->id=="pulse"?9.f:0.f;}
float radialDamage(float base,float distance,float radius)noexcept{if(!std::isfinite(base)||!std::isfinite(distance)||!std::isfinite(radius)||base<0||distance<0||radius<=0||distance>=radius)return 0;return base*(1-.45f*distance/radius);}
}
+64
View File
@@ -0,0 +1,64 @@
#include "irons/gameFramework/Game.hpp"
#include <algorithm>
#include <utility>
namespace irons::gameFramework {
namespace { const std::vector<std::vector<std::string>> slots={{"rapid","coil","siege"},{"guided","pulse"},{"efficient","vector"},{"composite_guard","reactive_guard"}}; }
bool Grid::material(std::string_view id) noexcept {return id=="alloy"||id=="cells";}
bool Grid::blueprint(std::string_view id) noexcept {return id.starts_with("blueprint_")&&id.size()>10;}
int Grid::slot(std::string_view id) noexcept {for(int s=0;s<4;s++)for(const auto& key:slots[s])if(key==id)return s;return -1;}
std::string Grid::id(int s,int i) {return s>=0&&s<4&&i>=0&&i<static_cast<int>(slots[s].size())?slots[s][i]:"";}
bool Grid::known(std::string_view id) noexcept {return material(id)||blueprint(id)||slot(id)>=0;}
Recipe Grid::recipe(std::string_view id) {
if(id=="blueprint_coil_capacitor")return {std::string(id),"coil",4,2};
if(id=="blueprint_boost_efficient")return {std::string(id),"vector",3,2};
if(id=="blueprint_reactive_guard")return {std::string(id),"reactive_guard",4,1};
return {};
}
std::pair<int,int> Grid::size(std::string_view id,bool rotated) noexcept {
int w=1,h=1;
if(id=="rapid")h=3;
else if(id=="coil"){w=2;h=3;}
else if(id=="siege"){w=2;h=4;}
else if(id=="pulse"||id=="reactive_guard"||id=="vector"){w=2;h=2;}
else if(id=="guided"||id=="efficient"||id=="composite_guard")h=2;
return rotated?std::pair{h,w}:std::pair{w,h};
}
int Grid::area(const Item& item) noexcept {auto [w,h]=size(item.itemId);return w*h;}
int Grid::occupied(const std::vector<Item>& items) noexcept {int n=0;for(const auto& i:items)n+=area(i);return n;}
bool Grid::fits(const std::vector<Item>& items,int width,int height,const Item& item,int x,int y,bool rotated) noexcept {
auto [w,h]=size(item.itemId,rotated);
if(x<0||y<0||x>width-w||y>height-h)return false;
for(const auto& other:items){
if(&other==&item)continue;
auto [ow,oh]=size(other.itemId,other.rotated);
if(x<other.x+ow&&x+w>other.x&&y<other.y+oh&&y+h>other.y)return false;
}
return true;
}
bool Grid::place(std::vector<Item>& items,int width,int height,Item item,int x,int y,bool rotated) {
if(find(items,item.uid)||!fits(items,width,height,item,x,y,rotated))return false;
item.x=x;item.y=y;item.rotated=rotated;items.push_back(std::move(item));return true;
}
bool Grid::autoPlace(std::vector<Item>& items,int width,int height,Item item,bool preferred,bool allowOtherOrientation){
for(int turn=0;turn<(allowOtherOrientation?2:1);turn++)
for(int y=0;y<height;y++)for(int x=0;x<width;x++)
if(place(items,width,height,item,x,y,turn==0?preferred:!preferred))return true;
return false;
}
bool Grid::podFits(const std::vector<Item>& items){
if(items.empty()||items.size()>4)return false;
std::vector<Item> packed;
std::function<bool(std::size_t)> pack=[&](std::size_t index){
if(index==items.size())return true;
for(int t=0;t<2;t++)for(int y=0;y<2;y++)for(int x=0;x<2;x++){
if(!place(packed,2,2,items[index],x,y,t==1))continue;
if(pack(index+1))return true;packed.pop_back();
}
return false;
};
return pack(0);
}
int total(const std::vector<Item>& items,std::string_view id) noexcept {int n=0;for(const auto& i:items)if(i.itemId==id)n+=i.count;return n;}
const Item* find(const std::vector<Item>& items,std::string_view uid) noexcept {for(const auto& i:items)if(i.uid==uid)return &i;return nullptr;}
}
+38
View File
@@ -0,0 +1,38 @@
#include "irons/gameFramework/Game.hpp"
#include "nlohmann/json.hpp"
#include <cmath>
#include <set>
namespace irons::gameFramework {
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(Item,uid,itemId,count,x,y,rotated,borrowed)
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(Run,id,active,seed,stage,branch,biome,chassis,equipment,armor,energy,podUsed,objectiveDone,routeCompleted,upgrades,elapsed,escortArmor,cargo,safeCase,installed,destructionPending,searched)
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(Profile,version,format,warehouseWidth,warehouseHeight,warehouse,recoveryInbox,sorties,extractions,completed,unlocked,receipts,run)
Result validate(const Profile& p)noexcept{
try{
if(p.version!=1)return Result::fail(Code::Unsupported);
if(p.format!="grid-warehouse"||p.warehouseWidth<1||p.warehouseWidth>64||p.warehouseHeight<1||p.warehouseHeight>200||p.sorties<0||p.extractions<0||p.completed<0)return Result::fail(Code::Corrupt);
const auto& r=p.run;
if(r.chassis<0||r.chassis>2||r.stage<0||r.stage>4||!std::isfinite(r.armor)||!std::isfinite(r.energy)||!std::isfinite(r.elapsed)||!std::isfinite(r.escortArmor)||r.armor<0||r.energy<0||r.energy>100||r.elapsed<0)return Result::fail(Code::Corrupt);
std::set<std::string> ids;
auto check=[&](const Item& i){return !i.uid.empty()&&ids.insert(i.uid).second&&Grid::known(i.itemId)&&i.count>0&&i.count<=(Grid::material(i.itemId)?10:1);};
for(const auto& i:p.warehouse)if(!check(i)||i.borrowed||!Grid::fits(p.warehouse,p.warehouseWidth,p.warehouseHeight,i,i.x,i.y,i.rotated))return Result::fail(Code::Corrupt);
for(const auto& i:p.recoveryInbox)if(!check(i)||i.borrowed)return Result::fail(Code::Corrupt);
for(const auto& i:r.cargo)if(!check(i)||!Grid::fits(r.cargo,4,4,i,i.x,i.y,i.rotated))return Result::fail(Code::Corrupt);
for(const auto& i:r.safeCase)if(!check(i)||!Grid::fits(r.safeCase,2,2,i,i.x,i.y,i.rotated))return Result::fail(Code::Corrupt);
std::set<int> slots;
for(const auto& i:r.installed){int slot=Grid::slot(i.itemId);if(!check(i)||slot<0||!slots.insert(slot).second||Grid::id(slot,r.equipment[slot])!=i.itemId)return Result::fail(Code::Corrupt);}
if(r.active){if(r.id.empty())return Result::fail(Code::Corrupt);for(int s=0;s<4;s++)if(!slots.contains(s)&&r.equipment[s]!=-1)return Result::fail(Code::Corrupt);}
else if(r.destructionPending||!r.cargo.empty()||!r.safeCase.empty()||!r.installed.empty())return Result::fail(Code::Corrupt);
if(std::set<std::string>(p.receipts.begin(),p.receipts.end()).size()!=p.receipts.size()||std::set<std::string>(p.unlocked.begin(),p.unlocked.end()).size()!=p.unlocked.size())return Result::fail(Code::Corrupt);
return {};
}catch(...){return Result::fail(Code::Corrupt);}
}
std::string encode(const Profile& profile){return nlohmann::json(profile).dump(2);}
Result decode(std::string_view text,Profile& out)noexcept{
try{
const auto j=nlohmann::json::parse(text);
if(!j.is_object()||!j.contains("format")||!j.contains("warehouseWidth")||!j.contains("recoveryInbox")||!j.contains("version"))return Result::fail(Code::Corrupt);
Profile candidate=j.get<Profile>();auto result=validate(candidate);if(result)out=std::move(candidate);return result;
}catch(...){return Result::fail(Code::Corrupt);}
}
}
+223
View File
@@ -0,0 +1,223 @@
#include "irons/gameFramework/Game.hpp"
#include <algorithm>
#include <cmath>
#include <set>
#include <stdexcept>
namespace irons::gameFramework {
namespace {
bool live(const Run& r){return r.active&&!r.destructionPending&&r.armor>0;}
bool contains(const std::vector<std::string>& v,std::string_view key){return std::find(v.begin(),v.end(),key)!=v.end();}
std::vector<Item>& contents(Run& r,Hold h){return h==Hold::Cargo?r.cargo:r.safeCase;}
int capacity(Hold h){return h==Hold::Cargo?4:2;}
bool validHold(Hold h){return h==Hold::Cargo||h==Hold::SafeCase;}
Item* edit(std::vector<Item>& v,std::string_view uid){for(auto& i:v)if(i.uid==uid)return &i;return nullptr;}
void removeUid(std::vector<Item>& v,std::string_view uid){std::erase_if(v,[&](const Item& i){return i.uid==uid;});}
Result base(const Profile& p){return p.run.active?Result::fail(Code::Locked):Result{};}
}
Session::Session(IProfileStorage& storage,IIdGenerator& ids,Catalog catalog):catalog_(std::move(catalog)),storage_(storage),ids_(ids){}
Item Session::make(std::string_view id,int count){Item i;i.uid=ids_.next();i.itemId=id;i.count=count;return i;}
Result Session::load()noexcept{
try{
std::string bytes;auto result=storage_.load(bytes);
if(result.code==Code::Missing){
profile_=Profile{};for(int s=0;s<4;s++)if(!Grid::autoPlace(profile_.warehouse,10,24,make(Grid::id(s,0))))return last_=Result::fail(Code::NoSpace);
result=storage_.commitAtomically(encode(profile_));
}else if(result){Profile candidate;result=decode(bytes,candidate);if(result)profile_=std::move(candidate);}
last_=result;if(result){preparation_=Preparation{};reconcile();}return last_;
}catch(...){return last_=Result::fail(Code::Corrupt);}
}
Result Session::transact(const std::function<Result(Profile&)>& change,std::string_view event,bool allowPending)noexcept{
try{
if(profile_.run.destructionPending&&!allowPending)return last_=Result::fail(Code::Locked);
Profile candidate=profile_;auto result=change(candidate);if(!result)return last_=result;
result=validate(candidate);if(!result)return last_=result;
result=storage_.commitAtomically(encode(candidate));if(!result)return last_=Result::fail(Code::SaveFailed,result.detail);
profile_=std::move(candidate);if(!event.empty())events_.push_back({std::string(event),profile_.run.id,{},{}});
reconcile();return last_={};
}catch(...){return last_=Result::fail(Code::Invalid,"transaction");}
}
Result Session::save()noexcept{return transact([](Profile&){return Result{};});}
std::vector<DomainEvent> Session::takeEvents(){auto events=std::move(events_);events_.clear();return events;}
void Session::reconcile(){
if(profile_.run.active)return;
std::erase_if(preparation_.items,[&](const Prepared& p){auto i=find(profile_.warehouse,p.uid);return !i||i->count<p.count;});
for(int s=0;s<4;s++){
auto& selected=preparation_.selected[s];if(selected=="@borrowed")continue;
const Item* item=find(profile_.warehouse,selected);
if(item&&Grid::slot(item->itemId)==s){preparation_.equipment[s]=catalog_.index(item->itemId);continue;}
selected.clear();preparation_.equipment[s]=0;
for(const auto& i:profile_.warehouse)if(i.itemId==Grid::id(s,0)&&!reserved(i.uid)){selected=i.uid;break;}
if(selected.empty())selected="@borrowed";
}
}
bool Session::reserved(std::string_view uid)const noexcept{
for(const auto& s:preparation_.selected)if(s==uid)return true;
for(const auto& p:preparation_.items)if(p.uid==uid)return true;return false;
}
int Session::available(std::string_view id)const noexcept{int n=0;for(const auto& i:profile_.warehouse)if(i.itemId==id&&!reserved(i.uid))n+=i.count;return n;}
Result Session::resetDraft()noexcept{if(profile_.run.active)return last_=Result::fail(Code::Locked);preparation_=Preparation{};reconcile();return last_={};}
Result Session::selectChassis(int index)noexcept{if(profile_.run.active)return last_=Result::fail(Code::Locked);if(index<0||index>=3)return last_=Result::fail(Code::Invalid);preparation_.chassis=index;return last_={};}
Result Session::equip(std::string_view uid)noexcept{
if(profile_.run.active)return last_=Result::fail(Code::Locked);auto item=find(profile_.warehouse,uid);
if(!item||Grid::slot(item->itemId)<0)return last_=Result::fail(Code::Missing);
int s=Grid::slot(item->itemId);preparation_.selected[s]=uid;preparation_.equipment[s]=catalog_.index(item->itemId);
std::erase_if(preparation_.items,[&](const Prepared& p){return p.uid==uid;});return last_={};
}
Result Session::borrow(int slot)noexcept{
if(profile_.run.active)return last_=Result::fail(Code::Locked);if(slot<0||slot>3)return last_=Result::fail(Code::Invalid);
preparation_.selected[slot]="@borrowed";preparation_.equipment[slot]=0;return last_={};
}
Result Session::prepare(std::string_view uid,Hold hold,bool rotated,int x,int y,int quantity)noexcept{
try{
if(profile_.run.active)return last_=Result::fail(Code::Locked);
const auto* item=find(profile_.warehouse,uid);if(!item||!validHold(hold))return last_=Result::fail(Code::Missing);
if(std::find(preparation_.selected.begin(),preparation_.selected.end(),uid)!=preparation_.selected.end())return last_=Result::fail(Code::Locked,"equipped");
if(quantity<0||quantity>item->count)return last_=Result::fail(Code::Invalid);
std::vector<Item> occupied;
for(const auto& p:preparation_.items)if(p.hold==hold&&p.uid!=uid){auto source=find(profile_.warehouse,p.uid);if(!source)return last_=Result::fail(Code::Missing);auto copy=*source;copy.count=p.count;copy.x=p.x;copy.y=p.y;copy.rotated=p.rotated;occupied.push_back(copy);}
auto preview=*item;preview.count=quantity>0?quantity:item->count;
bool ok=x<0?Grid::autoPlace(occupied,capacity(hold),capacity(hold),preview,rotated,false):Grid::place(occupied,capacity(hold),capacity(hold),preview,x,y,rotated);
if(!ok)return last_=Result::fail(Code::NoSpace);
const auto& placed=occupied.back();std::erase_if(preparation_.items,[&](const Prepared& p){return p.uid==uid;});
preparation_.items.push_back({std::string(uid),hold,placed.count,placed.x,placed.y,placed.rotated});return last_={};
}catch(...){return last_=Result::fail(Code::Invalid);}
}
Result Session::unprepare(std::string_view uid)noexcept{if(profile_.run.active)return last_=Result::fail(Code::Locked);std::erase_if(preparation_.items,[&](const Prepared& p){return p.uid==uid;});return last_={};}
bool Session::deposit(Profile& p,Item item){
if(item.borrowed)return true;
if(Grid::material(item.itemId))for(auto& stack:p.warehouse)if(stack.itemId==item.itemId&&stack.count<10){int moved=std::min(10-stack.count,item.count);stack.count+=moved;item.count-=moved;if(!item.count)return true;}
if(!Grid::autoPlace(p.warehouse,p.warehouseWidth,p.warehouseHeight,item)){item.x=item.y=-1;p.recoveryInbox.push_back(std::move(item));}
return true;
}
bool Session::consume(Profile& p,std::string_view id,int count){
int available=0;for(const auto& i:p.warehouse)if(i.itemId==id&&!reserved(i.uid))available+=i.count;
if(count<0||available<count)return false;
for(auto& i:p.warehouse)if(i.itemId==id&&!reserved(i.uid)){int amount=std::min(count,i.count);count-=amount;i.count-=amount;if(!count)break;}
std::erase_if(p.warehouse,[](const Item& i){return i.count==0;});return true;
}
Result Session::moveWarehouse(std::string_view uid,int x,int y,bool rotated,int quantity)noexcept{
return transact([&](Profile& p){if(auto r=base(p);!r)return r;auto i=edit(p.warehouse,uid);if(!i)return Result::fail(Code::Missing);
if(quantity<0||quantity>i->count)return Result::fail(Code::Invalid);Item moved=*i;
if(quantity==0||quantity==i->count)removeUid(p.warehouse,uid);else{i->count-=quantity;moved.uid=ids_.next();moved.count=quantity;}
if(!Grid::place(p.warehouse,p.warehouseWidth,p.warehouseHeight,moved,x,y,rotated))return Result::fail(Code::NoSpace);
return Result{};},"warehouse.moved");
}
Result Session::mergeWarehouse(std::string_view uid,std::string_view target,int quantity)noexcept{
return transact([&](Profile& p){if(auto r=base(p);!r)return r;auto a=edit(p.warehouse,uid);auto b=edit(p.warehouse,target);
if(!a||!b||a==b||a->itemId!=b->itemId||!Grid::material(a->itemId)||quantity<1||quantity>a->count||quantity+b->count>10)return Result::fail(Code::Invalid);
a->count-=quantity;b->count+=quantity;if(!a->count)removeUid(p.warehouse,uid);return Result{};},"warehouse.merged");
}
Result Session::sortWarehouse()noexcept{
return transact([](Profile& p){if(auto r=base(p);!r)return r;auto items=p.warehouse;p.warehouse.clear();
std::sort(items.begin(),items.end(),[](const Item& a,const Item& b){if(Grid::area(a)!=Grid::area(b))return Grid::area(a)>Grid::area(b);return a.itemId==b.itemId?a.uid<b.uid:a.itemId<b.itemId;});
for(const auto& item:items)if(!Grid::autoPlace(p.warehouse,p.warehouseWidth,p.warehouseHeight,item))return Result::fail(Code::NoSpace);return Result{};},"warehouse.sorted");
}
Result Session::claim(std::string_view uid)noexcept{
return transact([&](Profile& p){if(auto r=base(p);!r)return r;auto inbox=p.recoveryInbox;p.recoveryInbox.clear();bool changed=false;
for(auto i:inbox){if(!uid.empty()&&i.uid!=uid){p.recoveryInbox.push_back(i);continue;}
int before=i.count;if(Grid::material(i.itemId))for(auto& stack:p.warehouse)if(stack.itemId==i.itemId&&stack.count<10){int moved=std::min(10-stack.count,i.count);i.count-=moved;stack.count+=moved;if(!i.count)break;}
if(!i.count||Grid::autoPlace(p.warehouse,p.warehouseWidth,p.warehouseHeight,i))changed=true;
else{p.recoveryInbox.push_back(i);changed|=before!=i.count;}}
return changed?Result{}:Result::fail(Code::NoSpace);},"warehouse.claimed");
}
Result Session::discard(std::string_view uid,bool inbox)noexcept{
return transact([&](Profile& p){if(auto r=base(p);!r)return r;auto& list=inbox?p.recoveryInbox:p.warehouse;if(!find(list,uid))return Result::fail(Code::Missing);removeUid(list,uid);return Result{};},"warehouse.discarded");
}
Result Session::recycle(std::string_view uid)noexcept{
return transact([&](Profile& p){if(auto r=base(p);!r)return r;auto i=find(p.warehouse,uid);if(!i||!Grid::blueprint(i->itemId)||!contains(p.unlocked,i->itemId))return Result::fail(Code::Invalid);removeUid(p.warehouse,uid);deposit(p,make("alloy",2));return Result{};},"warehouse.recycled");
}
Result Session::research(std::string_view id)noexcept{
return transact([&](Profile& p){if(auto r=base(p);!r)return r;auto recipe=Grid::recipe(id);if(recipe.item.empty()||contains(p.unlocked,id))return Result::fail(Code::Invalid);
bool owned=false;for(const auto& i:p.warehouse)if(i.itemId==id&&!reserved(i.uid))owned=true;
if(!owned)return Result::fail(Code::Missing);if(!consume(p,"alloy",2)||!consume(p,"cells",1))return Result::fail(Code::Funds);
p.unlocked.emplace_back(id);return Result{};},"blueprint.researched");
}
Result Session::manufacture(std::string_view id)noexcept{
return transact([&](Profile& p){if(auto r=base(p);!r)return r;auto recipe=Grid::recipe(id);if(recipe.item.empty()||!contains(p.unlocked,id))return Result::fail(Code::Locked);
if(!consume(p,"alloy",recipe.alloy)||!consume(p,"cells",recipe.cells))return Result::fail(Code::Funds);deposit(p,make(recipe.item));return Result{};},"equipment.manufactured");
}
Result Session::begin(int seed,bool trial)noexcept{
auto result=transact([&](Profile& p){if(p.run.active)return Result::fail(Code::Locked);if(!trial&&!p.recoveryInbox.empty())return Result::fail(Code::NoSpace,"inbox");
if(!catalog_.fits(preparation_.chassis,preparation_.equipment))return Result::fail(Code::Budget);
Run r;r.id=ids_.next();r.active=true;r.seed=seed;r.chassis=preparation_.chassis;r.equipment=preparation_.equipment;r.armor=catalog_.chassis[r.chassis].armor;
for(int s=0;s<4;s++){auto uid=preparation_.selected[s];auto wanted=Grid::id(s,r.equipment[s]);auto owned=find(p.warehouse,uid);Item item;
if(uid=="@borrowed"&&r.equipment[s]==0){item=make(wanted);item.borrowed=true;}
else{if(!owned||owned->itemId!=wanted)return Result::fail(Code::Missing);item=*owned;removeUid(p.warehouse,uid);}r.installed.push_back(item);}
for(const auto& request:preparation_.items){auto item=edit(p.warehouse,request.uid);if(!item||request.count<1||request.count>item->count)return Result::fail(Code::Missing);Item moved=*item;
if(request.count==item->count)removeUid(p.warehouse,request.uid);else{item->count-=request.count;moved.uid=ids_.next();moved.count=request.count;}
if(!Grid::place(contents(r,request.hold),capacity(request.hold),capacity(request.hold),moved,request.x,request.y,request.rotated))return Result::fail(Code::NoSpace);}
p.run=std::move(r);p.sorties++;return Result{};},"sortie.started");if(result){resetField();rangeReset();}return result;
}
Result Session::addLoot(std::string_view id,int count,std::string_view key,std::string_view offered)noexcept{
return transact([&](Profile& p){auto& r=p.run;if(!live(r))return Result::fail(Code::Locked);
if(!Grid::known(id)||count<1||count>(Grid::material(id)?10:1))return Result::fail(Code::Invalid);
if(!key.empty()&&contains(r.searched,key))return Result::fail(Code::Duplicate);
int remaining=count;if(Grid::material(id))for(auto& stack:r.cargo)if(stack.itemId==id){int moved=std::min(10-stack.count,remaining);stack.count+=moved;remaining-=moved;if(!remaining)break;}
if(remaining&&!Grid::autoPlace(r.cargo,4,4,make(id,remaining)))return Result::fail(Code::NoSpace);
if(!offered.empty()&&(Grid::slot(offered)<0||!Grid::autoPlace(r.cargo,4,4,make(offered))))return Result::fail(Code::NoSpace);
if(!key.empty())r.searched.emplace_back(key);return Result{};},"loot.collected");
}
Result Session::moveItem(std::string_view uid,Hold from,Hold to,int x,int y,bool rotated,int quantity)noexcept{
return transact([&](Profile& p){auto& r=p.run;if(!live(r)||!validHold(from)||!validHold(to))return Result::fail(Code::Locked);
auto& src=contents(r,from);auto item=edit(src,uid);if(!item)return Result::fail(Code::Missing);if(quantity<0||quantity>item->count)return Result::fail(Code::Invalid);auto moved=*item;
if(quantity==0||quantity==item->count)removeUid(src,uid);else{item->count-=quantity;moved.uid=ids_.next();moved.count=quantity;}
auto& dst=contents(r,to);bool ok=x<0?Grid::autoPlace(dst,capacity(to),capacity(to),moved,rotated):Grid::place(dst,capacity(to),capacity(to),moved,x,y,rotated);
return ok?Result{}:Result::fail(Code::NoSpace);},"inventory.moved");
}
Result Session::mergeItem(std::string_view source,Hold from,std::string_view target,Hold to,int quantity)noexcept{
return transact([&](Profile& p){if(!live(p.run)||!validHold(from)||!validHold(to))return Result::fail(Code::Locked);
auto a=edit(contents(p.run,from),source),b=edit(contents(p.run,to),target);
if(!a||!b||a==b||a->itemId!=b->itemId||!Grid::material(a->itemId)||quantity<1||quantity>a->count||quantity+b->count>10)return Result::fail(Code::Invalid);
a->count-=quantity;b->count+=quantity;if(!a->count)removeUid(contents(p.run,from),source);return Result{};},"inventory.merged");
}
Result Session::swapItems(std::string_view source,Hold from,std::string_view target,Hold to,bool rotated)noexcept{
return transact([&](Profile& p){if(!live(p.run)||!validHold(from)||!validHold(to)||source==target)return Result::fail(Code::Locked);
auto a=find(contents(p.run,from),source),b=find(contents(p.run,to),target);if(!a||!b)return Result::fail(Code::Missing);auto first=*a,second=*b;
removeUid(contents(p.run,from),source);removeUid(contents(p.run,to),target);
bool ok=Grid::place(contents(p.run,to),capacity(to),capacity(to),first,second.x,second.y,rotated)&&Grid::place(contents(p.run,from),capacity(from),capacity(from),second,first.x,first.y,second.rotated);
return ok?Result{}:Result::fail(Code::NoSpace);},"inventory.swapped");
}
Result Session::uninstall(int slot,Hold to,int x,int y,bool rotated)noexcept{
return transact([&](Profile& p){auto& r=p.run;if(!live(r)||slot<0||slot>3||!validHold(to))return Result::fail(Code::Locked);auto item=std::find_if(r.installed.begin(),r.installed.end(),[&](const Item& i){return Grid::slot(i.itemId)==slot;});
if(item==r.installed.end())return Result::fail(Code::Missing);auto moved=*item;
bool ok=x<0?Grid::autoPlace(contents(r,to),capacity(to),capacity(to),moved,rotated):Grid::place(contents(r,to),capacity(to),capacity(to),moved,x,y,rotated);
if(!ok)return Result::fail(Code::NoSpace);r.installed.erase(item);r.equipment[slot]=-1;return Result{};},"equipment.uninstalled");
}
Result Session::install(std::string_view uid,Hold from)noexcept{
return transact([&](Profile& p){auto& r=p.run;if(!live(r)||!validHold(from))return Result::fail(Code::Locked);auto& src=contents(r,from);auto source=find(src,uid);if(!source)return Result::fail(Code::Missing);auto item=*source;int slot=Grid::slot(item.itemId);if(slot<0)return Result::fail(Code::Invalid);
r.equipment[slot]=catalog_.index(item.itemId);if(!catalog_.fits(r.chassis,r.equipment))return Result::fail(Code::Budget);removeUid(src,uid);
auto old=std::find_if(r.installed.begin(),r.installed.end(),[&](const Item& i){return Grid::slot(i.itemId)==slot;});
if(old!=r.installed.end()){auto previous=*old;r.installed.erase(old);if(!Grid::autoPlace(src,capacity(from),capacity(from),previous,previous.rotated))return Result::fail(Code::NoSpace);}
r.installed.push_back(item);return Result{};},"equipment.installed");
}
Result Session::recoverPod(const std::vector<std::string>& selected)noexcept{
return transact([&](Profile& p){auto& r=p.run;if(!live(r)||r.podUsed)return Result::fail(Code::Locked);auto receipt=r.id+":cargo";if(contains(p.receipts,receipt))return Result::fail(Code::Duplicate);
std::set<std::string> unique(selected.begin(),selected.end());if(unique.size()!=selected.size())return Result::fail(Code::Invalid);std::vector<Item> picked;
for(const auto& uid:selected){auto i=find(r.cargo,uid);if(!i)return Result::fail(Code::Missing);picked.push_back(*i);}if(!Grid::podFits(picked))return Result::fail(Code::NoSpace);
for(const auto& i:picked){deposit(p,i);removeUid(r.cargo,i.uid);}r.podUsed=true;p.receipts.push_back(receipt);return Result{};},"cargo.recovered");
}
Result Session::finish(bool destroyed)noexcept{
if(destroyed&&!profile_.run.destructionPending){auto marked=transact([](Profile& p){if(!p.run.active)return Result::fail(Code::Locked);p.run.armor=0;p.run.destructionPending=true;return Result{};},"sortie.destructionPending");if(!marked)return marked;}
return transact([&](Profile& p){auto& r=p.run;auto receipt=r.id+":return";
if(!r.active||r.destructionPending!=destroyed||(!destroyed&&r.armor<=0))return Result::fail(Code::Locked);
if(contains(p.receipts,receipt))return Result::fail(Code::Duplicate);
if(destroyed)p.receipts.push_back(r.id+":secure_eject");
else{for(const auto& i:r.cargo)deposit(p,i);for(const auto& i:r.installed)deposit(p,i);p.extractions++;}
for(const auto& i:r.safeCase)deposit(p,i);r.cargo.clear();r.safeCase.clear();r.installed.clear();r.active=false;r.destructionPending=false;p.receipts.push_back(receipt);return Result{};},destroyed?"sortie.destroyed":"sortie.returned",true);
}
Result Session::objective()noexcept{return transact([](Profile& p){if(!live(p.run)||p.run.objectiveDone)return Result::fail(Code::Locked);p.run.objectiveDone=true;return Result{};},"objective.completed");}
Result Session::nextStage(int branch)noexcept{return transact([&](Profile& p){auto& r=p.run;if(!live(r)||!r.objectiveDone||r.stage>=4||branch<0||branch>1)return Result::fail(Code::Locked);if(r.stage==1)r.branch=branch;r.stage++;r.biome=branch;r.upgrades++;r.objectiveDone=false;r.searched.clear();return Result{};},"route.changed");}
Result Session::completeRoute()noexcept{return transact([](Profile& p){auto& r=p.run;if(!live(r)||r.routeCompleted)return Result::fail(Code::Locked);r.routeCompleted=true;r.objectiveDone=true;p.completed++;return Result{};},"route.completed");}
Result Session::upgrade(int type)noexcept{return transact([&](Profile& p){auto& r=p.run;if(!live(r)||type<0||type>2)return Result::fail(Code::Locked);std::string key="upgrade-"+std::to_string(r.stage);if(contains(r.searched,key))return Result::fail(Code::Duplicate);if(type==0)r.upgrades++;if(type==1)r.armor=std::min(r.armor+35.f,catalog_.chassis[r.chassis].armor);if(type==2)r.energy=100;r.searched.push_back(key);return Result{};},"upgrade.applied");}
void Session::vitals(float armor,float energy,float elapsed,float escort)noexcept{
if(!profile_.run.active||profile_.run.destructionPending)return;
if(std::isfinite(armor))profile_.run.armor=std::max(0.f,armor);
if(std::isfinite(energy))profile_.run.energy=std::clamp(energy,0.f,100.f);
if(std::isfinite(elapsed))profile_.run.elapsed=std::max(0.f,elapsed);
if(std::isfinite(escort))profile_.run.escortArmor=std::max(0.f,escort);
}
Result Session::seedAcceptanceStock()noexcept{
return transact([&](Profile& p){if(auto r=base(p);!r)return r;for(const auto& id:{"coil","siege","pulse","vector","reactive_guard","blueprint_coil_capacitor","blueprint_boost_efficient","blueprint_reactive_guard","alloy","alloy","alloy","cells","cells"})deposit(p,make(id,Grid::material(id)?10:1));return Result{};},"qa.seeded");
}
}
+123
View File
@@ -0,0 +1,123 @@
#include "irons/gameFramework/Game.hpp"
#include <algorithm>
#include <cmath>
#include <fstream>
#include <functional>
#include <iomanip>
#include <iostream>
#include <sstream>
#include <stdexcept>
#include <vector>
using namespace irons::gameFramework;
namespace {
struct Memory : IProfileStorage {
std::string bytes;bool failWrite=false;int writes=0,failAt=-1;Result load(std::string& out)override{if(bytes.empty())return Result::fail(Code::Missing);out=bytes;return {};}
Result commitAtomically(std::string_view data)override{writes++;if(failWrite||writes==failAt)return Result::fail(Code::SaveFailed);bytes=data;return {};}
};
struct Ids : IIdGenerator {int n=0;std::string next()override{std::ostringstream s;s<<std::hex<<std::setfill('0')<<std::setw(32)<<++n;return s.str();}};
void require(bool ok,const char* message="expectation failed"){if(!ok)throw std::runtime_error(message);}
void ok(Result r){require(static_cast<bool>(r),r.detail.empty()?"command failed":r.detail.c_str());}
struct Fixture {
Memory memory;Ids ids;Session session{memory,ids};
Fixture(){ok(session.load());}
Item item(std::string id,int count=1){Item i;i.uid=ids.next();i.itemId=id;i.count=count;return i;}
std::string uid(std::string_view id)const{for(const auto& i:session.state().warehouse)if(i.itemId==id)return i.uid;throw std::runtime_error("item missing");}
void stock(){ok(session.seedAcceptanceStock());}
void patch(const std::function<void(Profile&)>& change){auto p=session.state();change(p);memory.bytes=encode(p);ok(session.load());}
};
int passed=0,failed=0;std::vector<std::pair<std::string,bool>> results;
void test(const std::string& name,const std::function<void()>& action){try{action();passed++;results.push_back({name,true});}catch(const std::exception& e){failed++;results.push_back({name,false});std::cerr<<"FAIL "<<name<<": "<<e.what()<<'\n';}}
}
int main(int argc,char** argv){
test("Starter ownership and V1",[]{Fixture f;require(f.session.state().version==1&&f.session.state().warehouse.size()==4&&f.session.state().sorties==0);ok(validate(f.session.state()));});
test("Source definition catalog",[]{auto c=Catalog::defaults();require(c.equipment.size()==9&&c.chassis.size()==3&&c.chassis[0].speed==8&&c.part("coil")->slot==0);});
for(auto id:{"rapid","coil","siege","guided","pulse","efficient","vector","composite_guard","reactive_guard"})test(std::string("Grid rotation ")+id,[id]{auto a=Grid::size(id),b=Grid::size(id,true);require(a.first==b.second&&a.second==b.first&&Grid::slot(id)>=0);});
test("Grid occupancy rejects overlap",[]{std::vector<Item> v;Item a{"a","coil"},b{"b","coil"};require(Grid::place(v,4,4,a,0,0,false));require(!Grid::place(v,4,4,b,1,1,false));require(v.size()==1);});
test("Grid rejects negative and out of bounds",[]{std::vector<Item> v;Item a{"a","siege"};require(!Grid::place(v,4,4,a,-1,0,false)&&!Grid::place(v,4,4,a,3,0,false));});
test("Pod can pack differently oriented shapes",[]{std::vector<Item> v={Item{"a","guided"},Item{"b","efficient"}};require(Grid::podFits(v));});
test("Pod rejects 2x3 weapon",[]{require(!Grid::podFits({Item{"a","coil"}}));});
test("Pod rejects empty selection",[]{require(!Grid::podFits({}));});
test("Invalid UID uniqueness",[]{Fixture f;auto p=f.session.state();p.warehouse.push_back(p.warehouse.front());require(!validate(p));});
test("Invalid unknown equipment",[]{Fixture f;auto p=f.session.state();p.warehouse.front().itemId="unknown";require(!validate(p));});
test("Invalid material stack",[]{Fixture f;auto p=f.session.state();p.recoveryInbox.push_back({"test","alloy",11});require(!validate(p));});
test("Invalid borrowed warehouse item",[]{Fixture f;auto p=f.session.state();p.warehouse.front().borrowed=true;require(!validate(p));});
test("Invalid duplicate receipts",[]{Fixture f;auto p=f.session.state();p.receipts={"same","same"};require(!validate(p));});
test("V1 codec roundtrip",[]{Fixture f;f.stock();Profile p;ok(decode(encode(f.session.state()),p));require(encode(p)==encode(f.session.state()));});
test("Reject older schema",[]{Profile p;require(!decode("{\"version\":1}",p));});
test("Reject unsupported version",[]{Fixture f;auto p=f.session.state();p.version=2;Profile out;require(decode(encode(p),out).code==Code::Unsupported);});
test("Malformed JSON does not reset a profile",[]{Fixture f;auto before=encode(f.session.state());f.memory.bytes="{bad";require(!f.session.load());require(before==encode(f.session.state()));});
test("Reject NaN vitals",[]{Fixture f;auto p=f.session.state();p.run.armor=std::nanf("");require(!validate(p));});
test("Prepare consumes no stock or sortie",[]{Fixture f;f.stock();auto before=f.memory.bytes;auto uid=f.uid("coil");ok(f.session.prepare(uid,Hold::Cargo));require(before==f.memory.bytes&&f.session.state().sorties==0);});
test("Preparation preserves selected rotation",[]{Fixture f;f.stock();ok(f.session.prepare(f.uid("coil"),Hold::Cargo,true));require(f.session.draft().items[0].rotated);});
test("Safe case rejects oversized gear",[]{Fixture f;f.stock();require(f.session.prepare(f.uid("siege"),Hold::SafeCase).code==Code::NoSpace);});
test("Equipped and staged ownership mutually exclusive",[]{Fixture f;f.stock();auto uid=f.uid("coil");ok(f.session.equip(uid));require(!f.session.prepare(uid,Hold::Cargo));});
test("Equip removes carry reservation",[]{Fixture f;f.stock();auto uid=f.uid("coil");ok(f.session.prepare(uid,Hold::Cargo));ok(f.session.equip(uid));require(f.session.draft().items.empty());});
test("Borrowing selects baseline model",[]{Fixture f;f.stock();ok(f.session.equip(f.uid("coil")));ok(f.session.borrow(0));require(f.session.draft().equipment[0]==0&&f.session.draft().selected[0]=="@borrowed");});
test("Invalid chassis selection",[]{Fixture f;require(!f.session.selectChassis(3));});
test("Light chassis rejects overloaded configuration",[]{Fixture f;f.stock();ok(f.session.selectChassis(0));ok(f.session.equip(f.uid("siege")));ok(f.session.equip(f.uid("vector")));ok(f.session.equip(f.uid("reactive_guard")));require(!f.session.begin(1));});
test("Deployment commits equipped and carried UID once",[]{Fixture f;f.stock();auto uid=f.uid("coil");ok(f.session.prepare(uid,Hold::Cargo,true));ok(f.session.begin(12));require(!find(f.session.state().warehouse,uid)&&find(f.session.state().run.cargo,uid));require(f.session.state().sorties==1);require(!f.session.begin(12));require(f.session.state().sorties==1);});
test("Deployment write failure keeps entire stock",[]{Fixture f;auto before=encode(f.session.state());f.memory.failWrite=true;require(!f.session.begin(1));require(before==encode(f.session.state()));});
test("Active run locks base modifications",[]{Fixture f;ok(f.session.begin(1));require(!f.session.selectChassis(0)&&!f.session.borrow(0)&&!f.session.sortWarehouse()&&!f.session.discard("bad"));});
test("Material splitting creates distinct UID",[]{Fixture f;f.stock();auto uid=f.uid("alloy");auto before=f.session.state().warehouse.size();ok(f.session.moveWarehouse(uid,8,12,false,3));require(f.session.state().warehouse.size()==before+1);require(find(f.session.state().warehouse,uid)->count==7);ok(validate(f.session.state()));});
test("Material split conserves amount",[]{Fixture f;f.stock();auto uid=f.uid("alloy");ok(f.session.moveWarehouse(uid,8,12,false,3));require(total(f.session.state().warehouse,"alloy")==30&&find(f.session.state().warehouse,uid)->count==7);ok(validate(f.session.state()));});
test("Invalid move rolls back serialized state",[]{Fixture f;auto before=encode(f.session.state());auto uid=f.uid("rapid");require(!f.session.moveWarehouse(uid,-1,0,false));require(before==encode(f.session.state()));});
test("Sort retains UIDs and counts",[]{Fixture f;f.stock();auto uid=f.uid("siege");ok(f.session.sortWarehouse());require(find(f.session.state().warehouse,uid)&&total(f.session.state().warehouse,"alloy")==30);});
test("Research cost and persistent unlock",[]{Fixture f;f.stock();ok(f.session.research("blueprint_coil_capacitor"));require(total(f.session.state().warehouse,"alloy")==28&&f.session.state().unlocked.size()==1);});
test("Research cannot charge twice",[]{Fixture f;f.stock();ok(f.session.research("blueprint_coil_capacitor"));auto before=f.memory.bytes;require(!f.session.research("blueprint_coil_capacitor"));require(before==f.memory.bytes);});
test("Manufacture consumes physical materials",[]{Fixture f;f.stock();ok(f.session.research("blueprint_coil_capacitor"));ok(f.session.manufacture("blueprint_coil_capacitor"));require(total(f.session.state().warehouse,"alloy")==24&&total(f.session.state().warehouse,"cells")==17);});
test("Manufacture requires recipe",[]{Fixture f;f.stock();auto before=f.memory.bytes;require(!f.session.manufacture("blueprint_coil_capacitor"));require(before==f.memory.bytes);});
test("Reserved material cannot be spent",[]{Fixture f;f.stock();for(const auto& i:f.session.state().warehouse)if(i.itemId=="alloy")ok(f.session.prepare(i.uid,Hold::Cargo));require(f.session.available("alloy")==0);require(!f.session.research("blueprint_coil_capacitor"));});
test("Recycled known blueprint deposits material",[]{Fixture f;f.stock();auto uid=f.uid("blueprint_coil_capacitor");ok(f.session.research("blueprint_coil_capacitor"));ok(f.session.recycle(uid));require(!find(f.session.state().warehouse,uid)&&total(f.session.state().warehouse,"alloy")==30);});
test("Loot source key prevents duplicate recovery",[]{Fixture f;ok(f.session.begin(1));ok(f.session.addLoot("alloy",4,"crate"));require(f.session.addLoot("alloy",4,"crate").code==Code::Duplicate);require(total(f.session.state().run.cargo,"alloy")==4);});
test("Loot write failure retains searchable source",[]{Fixture f;ok(f.session.begin(1));f.memory.failWrite=true;require(!f.session.addLoot("alloy",4,"crate"));require(f.session.state().run.searched.empty()&&f.session.state().run.cargo.empty());});
test("Loot material stack limit",[]{Fixture f;ok(f.session.begin(1));require(!f.session.addLoot("alloy",11));});
test("Safe case retains UID",[]{Fixture f;ok(f.session.begin(1));ok(f.session.addLoot("vector",1));auto uid=f.session.state().run.cargo[0].uid;ok(f.session.moveItem(uid,Hold::Cargo,Hold::SafeCase));require(find(f.session.state().run.safeCase,uid));});
test("Blocked safe case move is atomic",[]{Fixture f;ok(f.session.begin(1));ok(f.session.addLoot("coil",1));auto before=f.memory.bytes;require(!f.session.moveItem(f.session.state().run.cargo[0].uid,Hold::Cargo,Hold::SafeCase));require(before==f.memory.bytes);});
test("Pod commits removal, receipt, bank and used flag",[]{Fixture f;ok(f.session.begin(1));ok(f.session.addLoot("alloy",4));auto uid=f.session.state().run.cargo[0].uid;ok(f.session.recoverPod({uid}));require(f.session.state().run.podUsed&&f.session.state().run.cargo.empty()&&total(f.session.state().warehouse,"alloy")==4);require(f.session.state().receipts[0].ends_with(":cargo"));});
test("Pod cannot be reused",[]{Fixture f;ok(f.session.begin(1));ok(f.session.addLoot("alloy",4));ok(f.session.recoverPod({f.session.state().run.cargo[0].uid}));ok(f.session.addLoot("cells",1));require(!f.session.recoverPod({f.session.state().run.cargo[0].uid}));});
test("Pod failure publishes no receipt",[]{Fixture f;ok(f.session.begin(1));ok(f.session.addLoot("alloy",4));auto uid=f.session.state().run.cargo[0].uid;f.memory.failWrite=true;require(!f.session.recoverPod({uid}));require(!f.session.state().run.podUsed&&f.session.state().receipts.empty()&&find(f.session.state().run.cargo,uid));});
test("Pod rejects duplicate UIDs",[]{Fixture f;ok(f.session.begin(1));ok(f.session.addLoot("alloy",4));auto uid=f.session.state().run.cargo[0].uid;require(!f.session.recoverPod({uid,uid}));});
test("Death retains safe case and loses external cargo",[]{Fixture f;ok(f.session.begin(1));ok(f.session.addLoot("vector",1));auto uid=f.session.state().run.cargo[0].uid;ok(f.session.moveItem(uid,Hold::Cargo,Hold::SafeCase));ok(f.session.addLoot("alloy",5));ok(f.session.finish(true));require(find(f.session.state().warehouse,uid)&&total(f.session.state().warehouse,"alloy")==0&&!f.session.state().run.active);});
test("Death writes separate secure eject and return receipts",[]{Fixture f;ok(f.session.begin(1));ok(f.session.finish(true));require(f.session.state().receipts.size()==2&&f.session.state().receipts[0].ends_with(":secure_eject"));});
test("Pending death survives finalization failure",[]{Fixture f;ok(f.session.begin(1));ok(f.session.addLoot("alloy",4));auto uid=f.session.state().run.cargo[0].uid;ok(f.session.moveItem(uid,Hold::Cargo,Hold::SafeCase));f.memory.failAt=f.memory.writes+2;require(!f.session.finish(true));require(f.session.state().run.destructionPending&&f.session.state().run.armor==0);Profile disk;ok(decode(f.memory.bytes,disk));require(disk.run.destructionPending);f.memory.failAt=-1;ok(f.session.load());ok(f.session.finish(true));require(total(f.session.state().warehouse,"alloy")==4);});
test("Pending death blocks new transactions",[]{Fixture f;ok(f.session.begin(1));f.memory.failAt=f.memory.writes+2;require(!f.session.finish(true));require(!f.session.addLoot("alloy",1)&&!f.session.objective());});
test("Normal return banks owned loadout and cargo",[]{Fixture f;ok(f.session.begin(1));ok(f.session.addLoot("alloy",6));ok(f.session.finish(false));require(f.session.state().warehouse.size()==5&&f.session.state().extractions==1);});
test("Borrowed parts never become permanent property",[]{Fixture f;for(int s=0;s<4;s++)ok(f.session.borrow(s));ok(f.session.begin(1));ok(f.session.finish(false));require(f.session.state().warehouse.size()==4);});
test("Repeated finish cannot duplicate stock",[]{Fixture f;ok(f.session.begin(1));ok(f.session.finish(false));auto before=f.memory.bytes;require(!f.session.finish(false));require(before==f.memory.bytes);});
test("Return failure keeps all active ownership",[]{Fixture f;ok(f.session.begin(1));auto before=encode(f.session.state());f.memory.failWrite=true;require(!f.session.finish(false));require(before==encode(f.session.state()));});
test("Full warehouse banks return into inbox",[]{Fixture f;f.patch([&](Profile& p){p.warehouse.clear();p.warehouseWidth=p.warehouseHeight=1;p.warehouse.push_back(f.item("alloy",10));});for(int s=0;s<4;s++)ok(f.session.borrow(s));ok(f.session.begin(1));ok(f.session.addLoot("cells",2));ok(f.session.finish(false));require(f.session.state().recoveryInbox.size()==1);require(!f.session.begin(2));});
test("Inbox claim and capacity gate",[]{Fixture f;f.patch([&](Profile& p){p.recoveryInbox.push_back(f.item("alloy",5));});require(!f.session.begin(1));ok(f.session.claim());require(f.session.state().recoveryInbox.empty()&&total(f.session.state().warehouse,"alloy")==5);});
test("Stage transition preserves safe case and pod use",[]{Fixture f;ok(f.session.begin(1));ok(f.session.addLoot("alloy",2));ok(f.session.recoverPod({f.session.state().run.cargo[0].uid}));ok(f.session.addLoot("vector",1));auto uid=f.session.state().run.cargo[0].uid;ok(f.session.moveItem(uid,Hold::Cargo,Hold::SafeCase));ok(f.session.objective());ok(f.session.nextStage(1));require(f.session.state().run.stage==1&&f.session.state().run.podUsed&&find(f.session.state().run.safeCase,uid));});
test("Stage locked before objective",[]{Fixture f;ok(f.session.begin(1));require(!f.session.nextStage(0));});
test("Route completion counts once and does not return loot",[]{Fixture f;ok(f.session.begin(1));ok(f.session.addLoot("alloy",2));ok(f.session.completeRoute());require(f.session.state().completed==1&&f.session.state().run.active&&f.session.state().run.cargo.size()==1);require(!f.session.completeRoute());});
test("Core upgrades reset for new sortie",[]{Fixture f;ok(f.session.begin(1));ok(f.session.upgrade(1));ok(f.session.finish(false));ok(f.session.begin(2));require(f.session.state().run.upgrades==0);});
test("Uninstall produces actual empty weapon slot",[]{Fixture f;ok(f.session.begin(1));ok(f.session.uninstall(0,Hold::Cargo,0,0));require(f.session.state().run.equipment[0]==-1);Combat c;c.apply(f.session.catalog(),f.session.state().run);require(c.rounds==0);});
test("Reinstall preserves weapon UID",[]{Fixture f;ok(f.session.begin(1));auto uid=f.session.state().run.installed[0].uid;ok(f.session.uninstall(0,Hold::Cargo,0,0));ok(f.session.install(uid,Hold::Cargo));require(find(f.session.state().run.installed,uid)&&f.session.state().run.equipment[0]==0);});
test("Weapon needs fresh trigger after menu",[]{Fixture f;ok(f.session.begin(1));Combat c;c.apply(f.session.catalog(),f.session.state().run);require(!f.session.fire(c));f.session.tick(.1f,c,true);require(f.session.fire(c));});
test("Ammo cooldown enforced",[]{Fixture f;ok(f.session.begin(1));Combat c;f.session.tick(.1f,c,true);require(f.session.fire(c));int rounds=c.rounds;require(!f.session.fire(c)&&c.rounds==rounds);});
test("Reload completes through portable clock",[]{Fixture f;ok(f.session.begin(1));Combat c;f.session.tick(.1f,c,true);require(f.session.fire(c));require(f.session.reload(c));f.session.tick(10,c,true);require(c.rounds==f.session.catalog().part("rapid")->magazineSize);});
test("Boost energy and cooldown",[]{Fixture f;ok(f.session.begin(1));Combat c;require(f.session.boost(c));require(f.session.state().run.energy==82&&!f.session.boost(c));});
test("Guard mitigates damage",[]{Fixture f;ok(f.session.begin(1));Combat c;require(f.session.guard(c));f.session.takeDamage(c,40);require(f.session.state().run.armor==170);});
test("Secondary spends portable energy",[]{Fixture f;ok(f.session.begin(1));Combat c;require(f.session.fire(c,true));require(f.session.state().run.energy==85&&!f.session.fire(c,true));});
test("Core enemy damage and death event exactly once",[]{Fixture f;ok(f.session.begin(1));f.session.registerEnemy("enemy",0);require(f.session.damageEnemy("enemy",200));require(!f.session.damageEnemy("enemy",200));auto events=f.session.takeEvents();require(std::count_if(events.begin(),events.end(),[](const DomainEvent& e){return e.key=="enemy.destroyed";})==1);});
test("Enemy shot requires perception fact",[]{Fixture f;ok(f.session.begin(1));f.session.registerEnemy("enemy",0);require(!f.session.stepEnemy("enemy",2,10,false).fire);require(f.session.stepEnemy("enemy",.1f,10,true).fire);require(!f.session.stepEnemy("enemy",.1f,10,true).fire);});
test("Independent stores isolate range from campaign",[]{Fixture a,b;ok(a.session.begin(1));ok(b.session.begin(2));ok(b.session.addLoot("alloy",5));ok(b.session.finish(false));require(total(a.session.state().warehouse,"alloy")==0&&a.session.state().run.active);});
test("Branch choice locks at second sector",[]{Fixture f;ok(f.session.begin(1));ok(f.session.objective());ok(f.session.nextStage(0));ok(f.session.objective());ok(f.session.nextStage(1));ok(f.session.objective());ok(f.session.nextStage(0));require(f.session.state().run.branch==1&&f.session.state().run.biome==0&&f.session.state().run.upgrades==3);});
test("Upgrade source cannot be farmed",[]{Fixture f;ok(f.session.begin(1));ok(f.session.upgrade(0));auto bytes=f.memory.bytes;require(!f.session.upgrade(0)&&f.memory.bytes==bytes);});
test("Pending death disables combat resources",[]{Fixture f;ok(f.session.begin(1));f.memory.failAt=f.memory.writes+2;require(!f.session.finish(true));Combat c;require(!f.session.boost(c)&&!f.session.guard(c)&&!f.session.fire(c,true));});
test("Training gate order and portable clock",[]{Fixture f;ok(f.session.begin(1));Combat c;f.session.rangeGate(1);require(f.session.range().gates==0);f.session.rangeGate(0);f.session.tick(3,c,true);f.session.rangeGate(1);f.session.tick(2,c,true);f.session.rangeGate(2);require(f.session.range().gates==3&&f.session.range().seconds==5);f.session.rangeReset();require(f.session.range().gates==0);});
test("Training targets count hits without enemy death",[]{Fixture f;ok(f.session.begin(1));f.session.registerEnemy("target",4);require(!f.session.damageEnemy("target",1000));require(f.session.range().hits==1&&f.session.enemy("target")->health>0&&!f.session.stepEnemy("target",2,4,true).fire);});
test("Extraction uses portable fifteen second clock",[]{Fixture f;ok(f.session.begin(1));Combat c;ok(f.session.callExtraction());f.session.tick(14,c,true);require(!f.session.field().ready&&!f.session.boardExtraction());f.session.tick(1,c,true);require(f.session.field().ready&&f.session.state().run.active);ok(f.session.boardExtraction());require(!f.session.state().run.active&&f.session.state().extractions==1);});
test("Repeated extraction call cannot reset timer",[]{Fixture f;ok(f.session.begin(1));Combat c;ok(f.session.callExtraction());f.session.tick(5,c,true);require(!f.session.callExtraction()&&f.session.field().extraction==10);});
test("Ready boarding rolls back on storage failure",[]{Fixture f;ok(f.session.begin(1));Combat c;ok(f.session.callExtraction());f.session.tick(15,c,true);f.memory.failWrite=true;require(!f.session.boardExtraction()&&f.session.state().run.active&&f.session.field().ready);f.memory.failWrite=false;ok(f.session.boardExtraction());});
test("Pod channel requires three stable seconds",[]{Fixture f;ok(f.session.begin(1));ok(f.session.addLoot("alloy",2));std::vector<std::string> picked={f.session.state().run.cargo[0].uid};ok(f.session.advanceRecovery(2,true,true,picked));require(!f.session.state().run.podUsed);ok(f.session.advanceRecovery(1,true,true,picked));require(f.session.state().run.podUsed);});
test("Pod motion interruption resets portable progress",[]{Fixture f;ok(f.session.begin(1));ok(f.session.addLoot("alloy",2));std::vector<std::string> picked={f.session.state().run.cargo[0].uid};ok(f.session.advanceRecovery(2,true,true,picked));require(!f.session.advanceRecovery(.1f,true,false,picked)&&f.session.field().podProgress==0);ok(f.session.advanceRecovery(2,true,true,picked));require(!f.session.state().run.podUsed);});
test("New sortie resets field timers",[]{Fixture f;ok(f.session.begin(1));ok(f.session.callExtraction());ok(f.session.finish(false));ok(f.session.begin(2));require(f.session.field().extraction<0&&!f.session.field().ready);});
test("Blast falloff and bounds are engine independent",[]{require(radialDamage(100,0,5)==100&&radialDamage(100,2.5f,5)==77.5f&&radialDamage(100,5,5)==0&&radialDamage(100,-1,5)==0);});
for(int branch=0;branch<2;branch++)test("Five sector route branch "+std::to_string(branch),[branch]{Fixture f;ok(f.session.begin(19));for(int stage=0;stage<4;stage++){ok(f.session.objective());ok(f.session.nextStage(stage==1?branch:0));}require(f.session.state().run.stage==4&&f.session.state().run.branch==branch);if(branch==0){f.session.registerEnemy("boss",3,true);require(f.session.damageEnemy("boss",1000));}else ok(f.session.completeRoute());require(f.session.state().completed==1&&f.session.state().run.active);ok(f.session.callExtraction());Combat c;f.session.tick(15,c,true);ok(f.session.boardExtraction());require(f.session.state().extractions==1&&!f.session.state().run.active);});
test("Boss completion retries failed durable commit",[]{Fixture f;ok(f.session.begin(1));f.session.registerEnemy("boss",3,true);f.memory.failWrite=true;require(f.session.damageEnemy("boss",1000)&&!f.session.state().run.routeCompleted);f.memory.failWrite=false;f.session.stepEnemy("boss",1,10,true);require(f.session.state().run.routeCompleted&&f.session.state().completed==1);f.session.stepEnemy("boss",1,10,true);require(f.session.state().completed==1);});
test("Escort damage bounds belong to core",[]{Fixture f;ok(f.session.begin(1));f.session.damageEscort(75);require(f.session.state().run.escortArmor==175);f.session.damageEscort(1000);require(f.session.state().run.escortArmor==0);f.session.damageEscort(-1);require(f.session.state().run.escortArmor==0);});
std::cout<<"gameFramework: "<<passed<<" passed, "<<failed<<" failed\n";
if(argc>1){std::ofstream out(argv[1]);out<<"{\"passed\":"<<(failed==0?"true":"false")<<",\"total\":"<<results.size()<<",\"passedCount\":"<<passed<<",\"failedCount\":"<<failed<<",\"checks\":[";bool first=true;for(const auto& [name,value]:results){if(!first)out<<',';first=false;out<<"{\"name\":\""<<name<<"\",\"passed\":"<<(value?"true":"false")<<'}';}out<<"]}\n";}
return failed?1:0;
}
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2013-2025 Niels Lohmann
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
File diff suppressed because it is too large Load Diff