迁移独立 gameFramework 核心与 V1 存档规则
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
+25526
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -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'}
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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))
|
||||
@@ -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')
|
||||
@@ -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()
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);}
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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);}
|
||||
}
|
||||
@@ -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;}
|
||||
}
|
||||
@@ -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);}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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.
|
||||
+25526
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user