@series-inc/rundot-kinetix 0.0.0-bootstrap.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -0
- package/authoring.d.ts +30 -0
- package/index.d.ts +221 -0
- package/native/component_runtime.cpp +341 -0
- package/native/component_runtime.hpp +112 -0
- package/native/deterministic_math.cpp +594 -0
- package/native/deterministic_math.hpp +21 -0
- package/native/kinetix-f64-native-profile.cpp +406 -0
- package/native/kinetix-f64-native-profile.hpp +5 -0
- package/native/kinetix-fixed1000-native-profile.cpp +777 -0
- package/native/kinetix-fixed1000-native-profile.hpp +58 -0
- package/native/kinetix-fixed1000-physics.cpp +887 -0
- package/native/kinetix-fixed1000-physics.hpp +28 -0
- package/native/kinetix-installed-f64-renderer.cpp +344 -0
- package/native/kinetix-installed-f64-renderer.hpp +35 -0
- package/native/kinetix-installed-f64-runtime.cpp +1085 -0
- package/native/kinetix-installed-f64-runtime.hpp +141 -0
- package/native/kinetix-installed-fixed1000-data.hpp +77 -0
- package/native/kinetix-native-main.cpp +37 -0
- package/native/kinetix-native-runtime.cpp +20 -0
- package/native/kinetix-native-runtime.hpp +25 -0
- package/native/kinetix-render-projection.cpp +20 -0
- package/native/kinetix-render-projection.hpp +14 -0
- package/package.json +65 -0
- package/runtime.d.ts +76 -0
- package/scripts/build-native.mjs +67 -0
- package/scripts/emit-product-digests.mjs +33 -0
- package/scripts/preflight.mjs +76 -0
- package/src/index.mjs +57 -0
- package/src/kinetix-authoring.mjs +69 -0
- package/src/kinetix-baker.mjs +587 -0
- package/src/kinetix-deterministic-math.mjs +1044 -0
- package/src/kinetix-fixed1000-runtime-adapter.mjs +33 -0
- package/src/kinetix-fixed1000-runtime.mjs +954 -0
- package/src/kinetix-installed-f64-reference.mjs +157 -0
- package/src/kinetix-installed-f64-render-frames.mjs +53 -0
- package/src/kinetix-installed-f64-renderer.mjs +240 -0
- package/src/kinetix-installed-f64-runtime-adapter.mjs +68 -0
- package/src/kinetix-installed-f64-runtime.mjs +607 -0
- package/src/kinetix-installed-mechanics.mjs +377 -0
- package/src/kinetix-installed-systems.mjs +181 -0
- package/src/kinetix-native-product.mjs +72 -0
- package/src/kinetix-product-contract.mjs +121 -0
- package/src/kinetix-project-compiler.mjs +1017 -0
- package/src/kinetix-render-projection.mjs +28 -0
- package/src/kinetix-runtime-adapter-utils.mjs +168 -0
- package/src/kinetix-runtime-contract.mjs +54 -0
- package/src/kinetix-session-config.mjs +170 -0
- package/src/kinetix-source-snapshot.mjs +24 -0
- package/src/kinetix-survival-runtime-adapter.mjs +305 -0
- package/src/kinetix-survival-runtime.generated.mjs +1 -0
- package/src/kinetix-world-kernel.mjs +580 -0
- package/src/kinetix-world-runtime.mjs +171 -0
- package/src/runtime.mjs +14 -0
- package/src/shared/f64-bits.mjs +14 -0
- package/src/shared/kinetix-envelope-v1.mjs +589 -0
- package/src/shared/render-records-v1.mjs +168 -0
- package/src/shared/sha256.mjs +73 -0
|
@@ -0,0 +1,1085 @@
|
|
|
1
|
+
// Deterministic-f64 component runtime: C++ transliteration of
|
|
2
|
+
// component-reference-runtime-f64.mjs. Every phase, iteration order,
|
|
3
|
+
// floating-point expression shape, and RNG draw mirrors the TS reference so
|
|
4
|
+
// canonical per-tick hashes are bit-identical.
|
|
5
|
+
//
|
|
6
|
+
// Numeric contract: IEEE-754 doubles; +,-,*,/ and sqrt only from hardware;
|
|
7
|
+
// every transcendental from rundot::detmath. JS Math.hypot maps to the shim's
|
|
8
|
+
// variadic left-fold: det_hypot2(det_hypot2(0.0, x), y). Compile with
|
|
9
|
+
// -ffp-contract=off.
|
|
10
|
+
|
|
11
|
+
#include "kinetix-installed-f64-runtime.hpp"
|
|
12
|
+
|
|
13
|
+
#include <array>
|
|
14
|
+
#include <bit>
|
|
15
|
+
#include <cmath>
|
|
16
|
+
#include <cstdint>
|
|
17
|
+
#include <cstring>
|
|
18
|
+
#include <stdexcept>
|
|
19
|
+
|
|
20
|
+
#include "component_runtime.hpp"
|
|
21
|
+
#include "deterministic_math.hpp"
|
|
22
|
+
|
|
23
|
+
namespace rundot::f64rt {
|
|
24
|
+
namespace {
|
|
25
|
+
|
|
26
|
+
constexpr double TWO_PI = 6.283185307179586;
|
|
27
|
+
|
|
28
|
+
[[noreturn]] void fail(const std::string& message) {
|
|
29
|
+
throw std::runtime_error("component runtime f64: " + message);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
double detHypot(double x, double y) {
|
|
33
|
+
return rundot::detmath::det_hypot2(rundot::detmath::det_hypot2(0.0, x), y);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
enum class Kind : std::uint8_t {
|
|
37
|
+
Transform2D,
|
|
38
|
+
TwinStickInput,
|
|
39
|
+
AxisDriveMover,
|
|
40
|
+
HeadingFromVectors,
|
|
41
|
+
SpawnShield,
|
|
42
|
+
WanderMover,
|
|
43
|
+
SeekMover,
|
|
44
|
+
Telegraph,
|
|
45
|
+
ArenaBounds,
|
|
46
|
+
AutoFireWeapon,
|
|
47
|
+
LinearMover,
|
|
48
|
+
Lifetime,
|
|
49
|
+
Health,
|
|
50
|
+
Emitter,
|
|
51
|
+
CollisionCircle,
|
|
52
|
+
ScoreValue,
|
|
53
|
+
ResourceCounter,
|
|
54
|
+
DecayingMultiplier,
|
|
55
|
+
Count,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
constexpr std::size_t kKindCount = static_cast<std::size_t>(Kind::Count);
|
|
59
|
+
|
|
60
|
+
const char* const kKindNames[kKindCount] = {
|
|
61
|
+
"Transform2D",
|
|
62
|
+
"TwinStickInput",
|
|
63
|
+
"AxisDriveMover",
|
|
64
|
+
"HeadingFromVectors",
|
|
65
|
+
"SpawnShield",
|
|
66
|
+
"WanderMover",
|
|
67
|
+
"SeekMover",
|
|
68
|
+
"Telegraph",
|
|
69
|
+
"ArenaBounds",
|
|
70
|
+
"AutoFireWeapon",
|
|
71
|
+
"LinearMover",
|
|
72
|
+
"Lifetime",
|
|
73
|
+
"Health",
|
|
74
|
+
"Emitter",
|
|
75
|
+
"CollisionCircle",
|
|
76
|
+
"ScoreValue",
|
|
77
|
+
"ResourceCounter",
|
|
78
|
+
"DecayingMultiplier",
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
Kind kindFromType(const std::string& type) {
|
|
82
|
+
for (std::size_t index = 0; index < kKindCount; ++index) {
|
|
83
|
+
if (type == kKindNames[index]) return static_cast<Kind>(index);
|
|
84
|
+
}
|
|
85
|
+
fail("unknown component type " + type);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
enum class BoundsMode : std::uint8_t { Clamp, Cull, ReflectNegate, ReflectAbs };
|
|
89
|
+
|
|
90
|
+
struct Component {
|
|
91
|
+
Kind kind = Kind::Transform2D;
|
|
92
|
+
// Transform2D
|
|
93
|
+
Vec2 position{};
|
|
94
|
+
Vec2 velocity{};
|
|
95
|
+
double heading = 0.0;
|
|
96
|
+
// TwinStickInput
|
|
97
|
+
std::string moveField;
|
|
98
|
+
std::string aimField;
|
|
99
|
+
std::string fireField;
|
|
100
|
+
std::int64_t slot = 0;
|
|
101
|
+
Vec2 move{};
|
|
102
|
+
Vec2 aim{};
|
|
103
|
+
bool fire = false;
|
|
104
|
+
// AxisDriveMover
|
|
105
|
+
std::string axisField;
|
|
106
|
+
double maxSpeedPxPerSec = 0.0;
|
|
107
|
+
// SpawnShield (seconds + remaining); Telegraph reuses remaining
|
|
108
|
+
double seconds = 0.0;
|
|
109
|
+
double remaining = 0.0;
|
|
110
|
+
// WanderMover (Emitter reuses speedPx + rngStream)
|
|
111
|
+
double speedPx = 0.0;
|
|
112
|
+
double rerollSeconds = 0.0;
|
|
113
|
+
double sigmaRad = 0.0;
|
|
114
|
+
std::string rngStream;
|
|
115
|
+
double rerollTimer = 0.0;
|
|
116
|
+
// SeekMover
|
|
117
|
+
double baseSpeedPx = 0.0;
|
|
118
|
+
double topSpeedPx = 0.0;
|
|
119
|
+
double accelStartSec = 0.0;
|
|
120
|
+
double accelEndSec = 0.0;
|
|
121
|
+
double aliveSeconds = 0.0;
|
|
122
|
+
// ArenaBounds
|
|
123
|
+
BoundsMode mode = BoundsMode::Clamp;
|
|
124
|
+
bool useRadius = false;
|
|
125
|
+
// AutoFireWeapon
|
|
126
|
+
double fireIntervalSec = 0.0;
|
|
127
|
+
double speedPxPerSec = 0.0;
|
|
128
|
+
double spreadRad = 0.0;
|
|
129
|
+
std::string bulletTemplate;
|
|
130
|
+
double cooldown = 0.0;
|
|
131
|
+
std::uint64_t nextBulletId = 1;
|
|
132
|
+
// Lifetime
|
|
133
|
+
double maxSeconds = 0.0;
|
|
134
|
+
double age = 0.0;
|
|
135
|
+
// Health
|
|
136
|
+
std::int64_t hp = 0;
|
|
137
|
+
// Emitter
|
|
138
|
+
std::string trigger;
|
|
139
|
+
std::string emitTemplate;
|
|
140
|
+
std::int64_t count = 0;
|
|
141
|
+
double extraOffsetPx = 0.0;
|
|
142
|
+
std::string pattern;
|
|
143
|
+
// CollisionCircle
|
|
144
|
+
double radius = 0.0;
|
|
145
|
+
std::string layer;
|
|
146
|
+
// ScoreValue
|
|
147
|
+
std::int64_t basePoints = 0;
|
|
148
|
+
// ResourceCounter
|
|
149
|
+
std::int64_t score = 0;
|
|
150
|
+
std::int64_t deaths = 0;
|
|
151
|
+
// DecayingMultiplier
|
|
152
|
+
double increment = 0.0;
|
|
153
|
+
double cap = 0.0;
|
|
154
|
+
double decayWindow = 0.0;
|
|
155
|
+
double value = 1.0;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const PropertyValue& prop(const ComponentDef& def, const char* name) {
|
|
159
|
+
const auto found = def.properties.find(name);
|
|
160
|
+
if (found == def.properties.end()) fail("missing property " + std::string(name) + " on " + def.type);
|
|
161
|
+
return found->second;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
double propF64(const ComponentDef& def, const char* name) {
|
|
165
|
+
const PropertyValue& value = prop(def, name);
|
|
166
|
+
if (value.kind != PropertyValue::Kind::F64) fail("property " + std::string(name) + " on " + def.type + " is not f64");
|
|
167
|
+
return value.f64;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
Vec2 propVec2(const ComponentDef& def, const char* name) {
|
|
171
|
+
const PropertyValue& value = prop(def, name);
|
|
172
|
+
if (value.kind != PropertyValue::Kind::Vec2) fail("property " + std::string(name) + " on " + def.type + " is not vec2");
|
|
173
|
+
return value.vec2;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const std::string& propStr(const ComponentDef& def, const char* name) {
|
|
177
|
+
const PropertyValue& value = prop(def, name);
|
|
178
|
+
if (value.kind != PropertyValue::Kind::String) fail("property " + std::string(name) + " on " + def.type + " is not string");
|
|
179
|
+
return value.str;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
std::int64_t propInt(const ComponentDef& def, const char* name) {
|
|
183
|
+
const PropertyValue& value = prop(def, name);
|
|
184
|
+
if (value.kind != PropertyValue::Kind::Int) fail("property " + std::string(name) + " on " + def.type + " is not int");
|
|
185
|
+
return value.i64;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
bool propBool(const ComponentDef& def, const char* name) {
|
|
189
|
+
const PropertyValue& value = prop(def, name);
|
|
190
|
+
if (value.kind != PropertyValue::Kind::Bool) fail("property " + std::string(name) + " on " + def.type + " is not bool");
|
|
191
|
+
return value.boolean;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
BoundsMode boundsModeFromString(const std::string& mode) {
|
|
195
|
+
if (mode == "clamp") return BoundsMode::Clamp;
|
|
196
|
+
if (mode == "cull") return BoundsMode::Cull;
|
|
197
|
+
if (mode == "reflect-negate") return BoundsMode::ReflectNegate;
|
|
198
|
+
if (mode == "reflect-abs") return BoundsMode::ReflectAbs;
|
|
199
|
+
fail("unknown bounds mode " + mode);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
Component instantiateComponent(const ComponentDef& def) {
|
|
203
|
+
Component component;
|
|
204
|
+
component.kind = kindFromType(def.type);
|
|
205
|
+
switch (component.kind) {
|
|
206
|
+
case Kind::Transform2D:
|
|
207
|
+
component.position = propVec2(def, "position");
|
|
208
|
+
component.velocity = propVec2(def, "velocity");
|
|
209
|
+
component.heading = propF64(def, "heading");
|
|
210
|
+
break;
|
|
211
|
+
case Kind::TwinStickInput:
|
|
212
|
+
component.moveField = propStr(def, "moveField");
|
|
213
|
+
component.aimField = propStr(def, "aimField");
|
|
214
|
+
component.fireField = propStr(def, "fireField");
|
|
215
|
+
component.slot = propInt(def, "slot");
|
|
216
|
+
break;
|
|
217
|
+
case Kind::AxisDriveMover:
|
|
218
|
+
component.axisField = propStr(def, "axisField");
|
|
219
|
+
component.maxSpeedPxPerSec = propF64(def, "maxSpeedPxPerSec");
|
|
220
|
+
break;
|
|
221
|
+
case Kind::HeadingFromVectors:
|
|
222
|
+
component.aimField = propStr(def, "aimField");
|
|
223
|
+
component.moveField = propStr(def, "moveField");
|
|
224
|
+
break;
|
|
225
|
+
case Kind::SpawnShield:
|
|
226
|
+
component.seconds = propF64(def, "seconds");
|
|
227
|
+
component.remaining = component.seconds;
|
|
228
|
+
break;
|
|
229
|
+
case Kind::WanderMover:
|
|
230
|
+
component.speedPx = propF64(def, "speedPx");
|
|
231
|
+
component.rerollSeconds = propF64(def, "rerollSeconds");
|
|
232
|
+
component.sigmaRad = propF64(def, "sigmaRad");
|
|
233
|
+
component.rngStream = propStr(def, "rngStream");
|
|
234
|
+
component.rerollTimer = component.rerollSeconds;
|
|
235
|
+
break;
|
|
236
|
+
case Kind::SeekMover:
|
|
237
|
+
component.baseSpeedPx = propF64(def, "baseSpeedPx");
|
|
238
|
+
component.topSpeedPx = propF64(def, "topSpeedPx");
|
|
239
|
+
component.accelStartSec = propF64(def, "accelStartSec");
|
|
240
|
+
component.accelEndSec = propF64(def, "accelEndSec");
|
|
241
|
+
break;
|
|
242
|
+
case Kind::Telegraph:
|
|
243
|
+
component.remaining = propF64(def, "seconds");
|
|
244
|
+
break;
|
|
245
|
+
case Kind::ArenaBounds:
|
|
246
|
+
component.mode = boundsModeFromString(propStr(def, "mode"));
|
|
247
|
+
component.useRadius = propBool(def, "useRadius");
|
|
248
|
+
break;
|
|
249
|
+
case Kind::AutoFireWeapon:
|
|
250
|
+
component.fireIntervalSec = propF64(def, "fireIntervalSec");
|
|
251
|
+
component.speedPxPerSec = propF64(def, "speedPxPerSec");
|
|
252
|
+
component.spreadRad = propF64(def, "spreadRad");
|
|
253
|
+
component.bulletTemplate = propStr(def, "bulletTemplate");
|
|
254
|
+
component.aimField = propStr(def, "aimField");
|
|
255
|
+
component.fireField = propStr(def, "fireField");
|
|
256
|
+
component.slot = propInt(def, "slot");
|
|
257
|
+
break;
|
|
258
|
+
case Kind::LinearMover:
|
|
259
|
+
break;
|
|
260
|
+
case Kind::Lifetime:
|
|
261
|
+
component.maxSeconds = propF64(def, "maxSeconds");
|
|
262
|
+
break;
|
|
263
|
+
case Kind::Health:
|
|
264
|
+
component.hp = propInt(def, "hp");
|
|
265
|
+
break;
|
|
266
|
+
case Kind::Emitter:
|
|
267
|
+
component.trigger = propStr(def, "trigger");
|
|
268
|
+
component.emitTemplate = propStr(def, "template");
|
|
269
|
+
component.count = propInt(def, "count");
|
|
270
|
+
component.speedPx = propF64(def, "speedPx");
|
|
271
|
+
component.extraOffsetPx = propF64(def, "extraOffsetPx");
|
|
272
|
+
component.pattern = propStr(def, "pattern");
|
|
273
|
+
component.rngStream = propStr(def, "rngStream");
|
|
274
|
+
break;
|
|
275
|
+
case Kind::CollisionCircle:
|
|
276
|
+
component.radius = propF64(def, "radius");
|
|
277
|
+
component.layer = propStr(def, "layer");
|
|
278
|
+
break;
|
|
279
|
+
case Kind::ScoreValue:
|
|
280
|
+
component.basePoints = propInt(def, "basePoints");
|
|
281
|
+
break;
|
|
282
|
+
case Kind::ResourceCounter:
|
|
283
|
+
component.score = propInt(def, "score");
|
|
284
|
+
component.deaths = 0;
|
|
285
|
+
break;
|
|
286
|
+
case Kind::DecayingMultiplier:
|
|
287
|
+
component.increment = propF64(def, "increment");
|
|
288
|
+
component.cap = propF64(def, "cap");
|
|
289
|
+
component.decayWindow = propF64(def, "decayWindow");
|
|
290
|
+
break;
|
|
291
|
+
default:
|
|
292
|
+
fail("unhandled component kind");
|
|
293
|
+
}
|
|
294
|
+
return component;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
struct Entity {
|
|
298
|
+
std::string ref;
|
|
299
|
+
std::uint64_t spawnId = 0;
|
|
300
|
+
std::vector<Component> components;
|
|
301
|
+
std::array<int, kKindCount> byKind{};
|
|
302
|
+
std::map<std::string, Vec2> vecFields;
|
|
303
|
+
std::map<std::string, bool> boolFields;
|
|
304
|
+
bool alive = true;
|
|
305
|
+
bool hasSpawnPosition = false;
|
|
306
|
+
Vec2 spawnPosition{};
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
Component* findComponent(Entity& entity, Kind kind) {
|
|
310
|
+
const int index = entity.byKind[static_cast<std::size_t>(kind)];
|
|
311
|
+
return index < 0 ? nullptr : &entity.components[static_cast<std::size_t>(index)];
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
Entity makeEntity(const std::string& ref, std::uint64_t spawnId, const std::vector<ComponentDef>& defs) {
|
|
315
|
+
Entity entity;
|
|
316
|
+
entity.ref = ref;
|
|
317
|
+
entity.spawnId = spawnId;
|
|
318
|
+
entity.byKind.fill(-1);
|
|
319
|
+
entity.components.reserve(defs.size());
|
|
320
|
+
for (const ComponentDef& def : defs) {
|
|
321
|
+
Component component = instantiateComponent(def);
|
|
322
|
+
const std::size_t kindIndex = static_cast<std::size_t>(component.kind);
|
|
323
|
+
if (entity.byKind[kindIndex] >= 0) fail("duplicate component " + def.type + " on " + ref);
|
|
324
|
+
entity.byKind[kindIndex] = static_cast<int>(entity.components.size());
|
|
325
|
+
entity.components.push_back(std::move(component));
|
|
326
|
+
}
|
|
327
|
+
if (Component* transform = findComponent(entity, Kind::Transform2D)) {
|
|
328
|
+
entity.hasSpawnPosition = true;
|
|
329
|
+
entity.spawnPosition = transform->position;
|
|
330
|
+
}
|
|
331
|
+
return entity;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
struct RngStreamRt {
|
|
335
|
+
std::string id;
|
|
336
|
+
std::uint32_t state = 0;
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
struct TemplateRt {
|
|
340
|
+
std::string id;
|
|
341
|
+
std::int64_t pool = 0;
|
|
342
|
+
Entity proto;
|
|
343
|
+
std::int64_t liveCount = 0;
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
struct Runtime {
|
|
347
|
+
const PackDef* pack = nullptr;
|
|
348
|
+
double dt = 1.0 / 60.0;
|
|
349
|
+
double width = 1280.0;
|
|
350
|
+
double height = 720.0;
|
|
351
|
+
std::int64_t tick = 0;
|
|
352
|
+
std::vector<RngStreamRt> rngStreams;
|
|
353
|
+
std::map<std::string, std::size_t> rngIndex;
|
|
354
|
+
std::vector<Entity> statics;
|
|
355
|
+
std::vector<Entity> pooled;
|
|
356
|
+
std::uint64_t nextSpawnId = 1;
|
|
357
|
+
std::vector<TemplateRt> templates;
|
|
358
|
+
std::map<std::string, std::size_t> templateIndex;
|
|
359
|
+
std::int64_t spawnsThisTick = 0;
|
|
360
|
+
std::size_t pooledCapacity = 0;
|
|
361
|
+
// Render-facing event log for the current tick (mirrors JS
|
|
362
|
+
// runtime.frameEvents). Not part of canonical state or hashing.
|
|
363
|
+
std::vector<FrameEventDef> frameEvents;
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
Entity& entityAt(Runtime& runtime, int handle) {
|
|
367
|
+
const int staticCount = static_cast<int>(runtime.statics.size());
|
|
368
|
+
return handle < staticCount ? runtime.statics[static_cast<std::size_t>(handle)]
|
|
369
|
+
: runtime.pooled[static_cast<std::size_t>(handle - staticCount)];
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
Runtime createRuntime(const PackDef& pack) {
|
|
373
|
+
Runtime runtime;
|
|
374
|
+
runtime.pack = &pack;
|
|
375
|
+
for (const RngStreamDef& stream : pack.rngStreams) {
|
|
376
|
+
runtime.rngIndex[stream.id] = runtime.rngStreams.size();
|
|
377
|
+
runtime.rngStreams.push_back({stream.id, stream.seed});
|
|
378
|
+
}
|
|
379
|
+
for (const EntityDef& entity : pack.entities) {
|
|
380
|
+
runtime.statics.push_back(makeEntity(entity.id, 0, entity.components));
|
|
381
|
+
}
|
|
382
|
+
for (const TemplateDef& templateDef : pack.templates) {
|
|
383
|
+
runtime.templateIndex[templateDef.id] = runtime.templates.size();
|
|
384
|
+
runtime.templates.push_back({templateDef.id, templateDef.pool, makeEntity(templateDef.id, 0, templateDef.components), 0});
|
|
385
|
+
}
|
|
386
|
+
// Alive pooled entities are bounded by the summed template pools; dead ones
|
|
387
|
+
// linger only until the end-of-tick filter, so per-tick growth is bounded by
|
|
388
|
+
// maxSpawnsPerTick. Reserving the worst case keeps element pointers stable
|
|
389
|
+
// across mid-tick spawns.
|
|
390
|
+
std::size_t totalPool = 0;
|
|
391
|
+
for (const TemplateDef& templateDef : pack.templates) totalPool += static_cast<std::size_t>(templateDef.pool);
|
|
392
|
+
runtime.pooledCapacity = totalPool + static_cast<std::size_t>(pack.maxSpawnsPerTick);
|
|
393
|
+
runtime.pooled.reserve(runtime.pooledCapacity);
|
|
394
|
+
return runtime;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// liveEntities() in the reference includes ALL static entities (even dead
|
|
398
|
+
// ones) plus alive pooled entities; only hashing filters statics by alive.
|
|
399
|
+
std::vector<int> liveEntities(const Runtime& runtime) {
|
|
400
|
+
std::vector<int> handles;
|
|
401
|
+
handles.reserve(runtime.statics.size() + runtime.pooled.size());
|
|
402
|
+
const int staticCount = static_cast<int>(runtime.statics.size());
|
|
403
|
+
for (int index = 0; index < staticCount; ++index) handles.push_back(index);
|
|
404
|
+
for (std::size_t index = 0; index < runtime.pooled.size(); ++index) {
|
|
405
|
+
if (runtime.pooled[index].alive) handles.push_back(staticCount + static_cast<int>(index));
|
|
406
|
+
}
|
|
407
|
+
return handles;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
std::uint32_t mulberry32Next(std::uint32_t& state) {
|
|
411
|
+
state = state + 0x6d2b79f5u;
|
|
412
|
+
std::uint32_t t = state;
|
|
413
|
+
t = static_cast<std::uint32_t>(static_cast<std::uint64_t>(t ^ (t >> 15)) * (t | 1u));
|
|
414
|
+
t = t ^ (t + static_cast<std::uint32_t>(static_cast<std::uint64_t>(t ^ (t >> 7)) * (t | 61u)));
|
|
415
|
+
return t ^ (t >> 14);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
double rngFloat(Runtime& runtime, const std::string& streamId) {
|
|
419
|
+
const auto found = runtime.rngIndex.find(streamId);
|
|
420
|
+
if (found == runtime.rngIndex.end()) fail("unknown rng stream " + streamId);
|
|
421
|
+
RngStreamRt& stream = runtime.rngStreams[found->second];
|
|
422
|
+
const std::uint32_t value = mulberry32Next(stream.state);
|
|
423
|
+
return static_cast<double>(value) / 4294967296.0;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
double gaussian(Runtime& runtime, const std::string& streamId) {
|
|
427
|
+
const double raw = rngFloat(runtime, streamId);
|
|
428
|
+
const double u1 = raw > 1e-9 ? raw : 1e-9; // Math.max(raw, 1e-9); raw is never NaN
|
|
429
|
+
const double u2 = rngFloat(runtime, streamId);
|
|
430
|
+
return std::sqrt(-2.0 * rundot::detmath::det_log(u1)) * rundot::detmath::det_cos(TWO_PI * u2);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
int spawnFromTemplate(Runtime& runtime, const std::string& templateId) {
|
|
434
|
+
const auto found = runtime.templateIndex.find(templateId);
|
|
435
|
+
if (found == runtime.templateIndex.end()) fail("unknown template " + templateId);
|
|
436
|
+
TemplateRt& templateRt = runtime.templates[found->second];
|
|
437
|
+
if (templateRt.liveCount >= templateRt.pool) return -1;
|
|
438
|
+
if (runtime.spawnsThisTick >= runtime.pack->maxSpawnsPerTick) return -1;
|
|
439
|
+
Entity entity = templateRt.proto;
|
|
440
|
+
entity.spawnId = runtime.nextSpawnId;
|
|
441
|
+
runtime.nextSpawnId += 1;
|
|
442
|
+
runtime.spawnsThisTick += 1;
|
|
443
|
+
templateRt.liveCount += 1;
|
|
444
|
+
runtime.pooled.push_back(std::move(entity));
|
|
445
|
+
return static_cast<int>(runtime.statics.size()) + static_cast<int>(runtime.pooled.size()) - 1;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
void despawn(Runtime& runtime, Entity& entity) {
|
|
449
|
+
if (!entity.alive) return;
|
|
450
|
+
entity.alive = false;
|
|
451
|
+
const auto found = runtime.templateIndex.find(entity.ref);
|
|
452
|
+
if (found == runtime.templateIndex.end()) fail("despawn of non-pooled entity " + entity.ref);
|
|
453
|
+
runtime.templates[found->second].liveCount -= 1;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
bool shipIsShielded(Entity& entity) {
|
|
457
|
+
Component* shield = findComponent(entity, Kind::SpawnShield);
|
|
458
|
+
return shield != nullptr && shield->remaining > 0;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
bool entityTelegraphing(Entity& entity) {
|
|
462
|
+
Component* telegraph = findComponent(entity, Kind::Telegraph);
|
|
463
|
+
return telegraph != nullptr && telegraph->remaining > 0;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
bool nearestShipPosition(Runtime& runtime, const Vec2& from, Vec2& out) {
|
|
467
|
+
bool found = false;
|
|
468
|
+
double bestDistSq = 0.0;
|
|
469
|
+
for (Entity& entity : runtime.statics) {
|
|
470
|
+
if (findComponent(entity, Kind::AxisDriveMover) == nullptr) continue;
|
|
471
|
+
Component* transform = findComponent(entity, Kind::Transform2D);
|
|
472
|
+
const double dx = transform->position.x - from.x;
|
|
473
|
+
const double dy = transform->position.y - from.y;
|
|
474
|
+
const double distSq = dx * dx + dy * dy;
|
|
475
|
+
if (!found || distSq < bestDistSq) {
|
|
476
|
+
found = true;
|
|
477
|
+
bestDistSq = distSq;
|
|
478
|
+
out = transform->position;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return found;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
void applyBounds(Runtime& runtime, Entity& entity, Component& bounds) {
|
|
485
|
+
Component* transform = findComponent(entity, Kind::Transform2D);
|
|
486
|
+
Component* collision = findComponent(entity, Kind::CollisionCircle);
|
|
487
|
+
const double radius = bounds.useRadius && collision != nullptr ? collision->radius : 0.0;
|
|
488
|
+
Vec2& position = transform->position;
|
|
489
|
+
Vec2& velocity = transform->velocity;
|
|
490
|
+
if (bounds.mode == BoundsMode::Clamp) {
|
|
491
|
+
if (position.x < radius) position.x = radius;
|
|
492
|
+
if (position.y < radius) position.y = radius;
|
|
493
|
+
if (position.x > runtime.width - radius) position.x = runtime.width - radius;
|
|
494
|
+
if (position.y > runtime.height - radius) position.y = runtime.height - radius;
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
if (bounds.mode == BoundsMode::Cull) {
|
|
498
|
+
if (position.x < 0 || position.x > runtime.width || position.y < 0 || position.y > runtime.height) {
|
|
499
|
+
despawn(runtime, entity);
|
|
500
|
+
}
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
if (bounds.mode == BoundsMode::ReflectNegate) {
|
|
504
|
+
if (position.x < radius) { position.x = radius; velocity.x = -velocity.x; }
|
|
505
|
+
if (position.y < radius) { position.y = radius; velocity.y = -velocity.y; }
|
|
506
|
+
if (position.x > runtime.width - radius) { position.x = runtime.width - radius; velocity.x = -velocity.x; }
|
|
507
|
+
if (position.y > runtime.height - radius) { position.y = runtime.height - radius; velocity.y = -velocity.y; }
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
// reflect-abs
|
|
511
|
+
if (position.x < radius) { position.x = radius; velocity.x = std::fabs(velocity.x); }
|
|
512
|
+
if (position.y < radius) { position.y = radius; velocity.y = std::fabs(velocity.y); }
|
|
513
|
+
if (position.x > runtime.width - radius) { position.x = runtime.width - radius; velocity.x = -std::fabs(velocity.x); }
|
|
514
|
+
if (position.y > runtime.height - radius) { position.y = runtime.height - radius; velocity.y = -std::fabs(velocity.y); }
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Installed projectile-family deterministic spread.
|
|
518
|
+
double spreadForBullet(double id, double spreadRad) {
|
|
519
|
+
const double seeded = rundot::detmath::det_sin(id * 12.9898 + 78.233) * 43758.5453;
|
|
520
|
+
const double unit = seeded - std::floor(seeded);
|
|
521
|
+
return (unit * 2 - 1) * spreadRad;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
bool circleHit(const Vec2& a, double ra, const Vec2& b, double rb) {
|
|
525
|
+
const double dx = a.x - b.x;
|
|
526
|
+
const double dy = a.y - b.y;
|
|
527
|
+
const double r = ra + rb;
|
|
528
|
+
return dx * dx + dy * dy <= r * r;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
struct DeathEvent {
|
|
532
|
+
bool enemyKilled = false; // false = ship-death
|
|
533
|
+
int handle = -1;
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
Vec2 getVecField(Entity& entity, const std::string& name) {
|
|
537
|
+
const auto found = entity.vecFields.find(name);
|
|
538
|
+
return found == entity.vecFields.end() ? Vec2{} : found->second;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
bool getBoolField(Entity& entity, const std::string& name) {
|
|
542
|
+
const auto found = entity.boolFields.find(name);
|
|
543
|
+
return found != entity.boolFields.end() && found->second;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
void runCollisionPass(Runtime& runtime, const std::vector<int>& live, const char* action, std::vector<DeathEvent>& events) {
|
|
547
|
+
for (const CollisionRuleDef& rule : runtime.pack->collisionRules) {
|
|
548
|
+
if (rule.action != action) continue;
|
|
549
|
+
std::vector<int> groupA;
|
|
550
|
+
std::vector<int> groupB;
|
|
551
|
+
for (const int handle : live) {
|
|
552
|
+
Entity& entity = entityAt(runtime, handle);
|
|
553
|
+
if (!entity.alive) continue;
|
|
554
|
+
Component* circle = findComponent(entity, Kind::CollisionCircle);
|
|
555
|
+
if (circle == nullptr) continue;
|
|
556
|
+
if (circle->layer == rule.layerA) groupA.push_back(handle);
|
|
557
|
+
if (circle->layer == rule.layerB) groupB.push_back(handle);
|
|
558
|
+
}
|
|
559
|
+
if (rule.action == "bullet-kill") {
|
|
560
|
+
for (const int bulletHandle : groupA) {
|
|
561
|
+
Entity& bullet = entityAt(runtime, bulletHandle);
|
|
562
|
+
if (!bullet.alive) continue;
|
|
563
|
+
Component* bulletCircle = findComponent(bullet, Kind::CollisionCircle);
|
|
564
|
+
Component* bulletTransform = findComponent(bullet, Kind::Transform2D);
|
|
565
|
+
for (const int enemyHandle : groupB) {
|
|
566
|
+
Entity& enemy = entityAt(runtime, enemyHandle);
|
|
567
|
+
if (!enemy.alive || entityTelegraphing(enemy)) continue;
|
|
568
|
+
Component* enemyCircle = findComponent(enemy, Kind::CollisionCircle);
|
|
569
|
+
Component* enemyTransform = findComponent(enemy, Kind::Transform2D);
|
|
570
|
+
if (!circleHit(bulletTransform->position, bulletCircle->radius, enemyTransform->position, enemyCircle->radius)) continue;
|
|
571
|
+
Component* health = findComponent(enemy, Kind::Health);
|
|
572
|
+
health->hp -= 1;
|
|
573
|
+
despawn(runtime, bullet);
|
|
574
|
+
if (health->hp <= 0) {
|
|
575
|
+
events.push_back({true, enemyHandle});
|
|
576
|
+
}
|
|
577
|
+
break;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
} else if (rule.action == "ship-death") {
|
|
581
|
+
for (const int shipHandle : groupA) {
|
|
582
|
+
Entity& ship = entityAt(runtime, shipHandle);
|
|
583
|
+
if (shipIsShielded(ship)) continue;
|
|
584
|
+
Component* shipCircle = findComponent(ship, Kind::CollisionCircle);
|
|
585
|
+
Component* shipTransform = findComponent(ship, Kind::Transform2D);
|
|
586
|
+
for (const int enemyHandle : groupB) {
|
|
587
|
+
Entity& enemy = entityAt(runtime, enemyHandle);
|
|
588
|
+
if (!enemy.alive || entityTelegraphing(enemy)) continue;
|
|
589
|
+
Component* enemyCircle = findComponent(enemy, Kind::CollisionCircle);
|
|
590
|
+
Component* enemyTransform = findComponent(enemy, Kind::Transform2D);
|
|
591
|
+
if (!circleHit(shipTransform->position, shipCircle->radius, enemyTransform->position, enemyCircle->radius)) continue;
|
|
592
|
+
events.push_back({false, shipHandle});
|
|
593
|
+
break;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
void handleDeathEvents(Runtime& runtime, const std::vector<DeathEvent>& events) {
|
|
601
|
+
Entity* game = nullptr;
|
|
602
|
+
for (Entity& entity : runtime.statics) {
|
|
603
|
+
if (findComponent(entity, Kind::ResourceCounter) != nullptr) {
|
|
604
|
+
game = &entity;
|
|
605
|
+
break;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
for (const DeathEvent& event : events) {
|
|
609
|
+
Entity& target = entityAt(runtime, event.handle);
|
|
610
|
+
{
|
|
611
|
+
// Mirrors the JS push at the TOP of handleDeathEvents' per-event loop:
|
|
612
|
+
// position read before any respawn/despawn mutation.
|
|
613
|
+
Component* transform = findComponent(target, Kind::Transform2D);
|
|
614
|
+
runtime.frameEvents.push_back({event.enemyKilled ? "enemy-killed" : "ship-death", target.ref,
|
|
615
|
+
transform->position.x, transform->position.y});
|
|
616
|
+
}
|
|
617
|
+
if (event.enemyKilled) {
|
|
618
|
+
Component* emitter = findComponent(target, Kind::Emitter);
|
|
619
|
+
if (emitter != nullptr && emitter->trigger == "on-death") {
|
|
620
|
+
Component* transform = findComponent(target, Kind::Transform2D);
|
|
621
|
+
Component* circle = findComponent(target, Kind::CollisionCircle);
|
|
622
|
+
const double speed = detHypot(transform->velocity.x, transform->velocity.y);
|
|
623
|
+
const double baseAngle = speed > 1
|
|
624
|
+
? rundot::detmath::det_atan2(transform->velocity.y, transform->velocity.x)
|
|
625
|
+
: rngFloat(runtime, emitter->rngStream) * TWO_PI;
|
|
626
|
+
const double phase = rngFloat(runtime, emitter->rngStream) * TWO_PI;
|
|
627
|
+
const auto templateFound = runtime.templateIndex.find(emitter->emitTemplate);
|
|
628
|
+
if (templateFound == runtime.templateIndex.end()) fail("unknown emitter template " + emitter->emitTemplate);
|
|
629
|
+
Entity& templateProto = runtime.templates[templateFound->second].proto;
|
|
630
|
+
Component* templateCircle = findComponent(templateProto, Kind::CollisionCircle);
|
|
631
|
+
const double childRadius = templateCircle != nullptr ? templateCircle->radius : 0.0;
|
|
632
|
+
const double spawnOffset = circle->radius + childRadius + emitter->extraOffsetPx;
|
|
633
|
+
// Snapshot spawn state used inside the loop: pooled growth may
|
|
634
|
+
// reallocate and invalidate transform/emitter pointers.
|
|
635
|
+
const Vec2 origin = transform->position;
|
|
636
|
+
const double emitSpeed = emitter->speedPx;
|
|
637
|
+
const std::string emitTemplate = emitter->emitTemplate;
|
|
638
|
+
const std::int64_t emitCount = emitter->count;
|
|
639
|
+
for (std::int64_t index = 0; index < emitCount; index += 1) {
|
|
640
|
+
const double angle = baseAngle + phase + (static_cast<double>(index) / static_cast<double>(emitCount)) * TWO_PI;
|
|
641
|
+
const double cos = rundot::detmath::det_cos(angle);
|
|
642
|
+
const double sin = rundot::detmath::det_sin(angle);
|
|
643
|
+
const int childHandle = spawnFromTemplate(runtime, emitTemplate);
|
|
644
|
+
if (childHandle >= 0) {
|
|
645
|
+
Entity& child = entityAt(runtime, childHandle);
|
|
646
|
+
Component* childTransform = findComponent(child, Kind::Transform2D);
|
|
647
|
+
childTransform->position.x = origin.x + cos * spawnOffset;
|
|
648
|
+
childTransform->position.y = origin.y + sin * spawnOffset;
|
|
649
|
+
childTransform->velocity.x = cos * emitSpeed;
|
|
650
|
+
childTransform->velocity.y = sin * emitSpeed;
|
|
651
|
+
// JS pushes 'spawned' inside spawnFromTemplate after setup ran;
|
|
652
|
+
// here setup is inlined at the call site, so push after it.
|
|
653
|
+
runtime.frameEvents.push_back({"spawned", emitTemplate, childTransform->position.x, childTransform->position.y});
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
Entity& entity = entityAt(runtime, event.handle);
|
|
658
|
+
Component* scoreValue = findComponent(entity, Kind::ScoreValue);
|
|
659
|
+
if (scoreValue != nullptr && game != nullptr) {
|
|
660
|
+
findComponent(*game, Kind::ResourceCounter)->score += scoreValue->basePoints;
|
|
661
|
+
}
|
|
662
|
+
if (entity.spawnId == 0) {
|
|
663
|
+
entity.alive = false;
|
|
664
|
+
} else {
|
|
665
|
+
despawn(runtime, entity);
|
|
666
|
+
}
|
|
667
|
+
} else {
|
|
668
|
+
if (game != nullptr) findComponent(*game, Kind::ResourceCounter)->deaths += 1;
|
|
669
|
+
Entity& entity = entityAt(runtime, event.handle);
|
|
670
|
+
Component* transform = findComponent(entity, Kind::Transform2D);
|
|
671
|
+
transform->position.x = entity.spawnPosition.x;
|
|
672
|
+
transform->position.y = entity.spawnPosition.y;
|
|
673
|
+
transform->velocity.x = 0;
|
|
674
|
+
transform->velocity.y = 0;
|
|
675
|
+
Component* shield = findComponent(entity, Kind::SpawnShield);
|
|
676
|
+
if (shield != nullptr) shield->remaining = shield->seconds;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
void tickRuntime(Runtime& runtime, const std::vector<SlotInputDef>& inputs) {
|
|
682
|
+
const double dt = runtime.dt;
|
|
683
|
+
runtime.spawnsThisTick = 0;
|
|
684
|
+
runtime.frameEvents.clear();
|
|
685
|
+
runtime.pooled.reserve(runtime.pooledCapacity);
|
|
686
|
+
const std::vector<int> live = liveEntities(runtime);
|
|
687
|
+
|
|
688
|
+
// 1. inputs
|
|
689
|
+
for (const int handle : live) {
|
|
690
|
+
Entity& entity = entityAt(runtime, handle);
|
|
691
|
+
Component* twinStick = findComponent(entity, Kind::TwinStickInput);
|
|
692
|
+
if (twinStick == nullptr) continue;
|
|
693
|
+
SlotInputDef slotInput;
|
|
694
|
+
if (twinStick->slot >= 0 && static_cast<std::size_t>(twinStick->slot) < inputs.size()) {
|
|
695
|
+
slotInput = inputs[static_cast<std::size_t>(twinStick->slot)];
|
|
696
|
+
}
|
|
697
|
+
twinStick->move = slotInput.movement;
|
|
698
|
+
twinStick->aim = slotInput.aim;
|
|
699
|
+
twinStick->fire = slotInput.fire;
|
|
700
|
+
entity.vecFields[twinStick->moveField] = twinStick->move;
|
|
701
|
+
entity.vecFields[twinStick->aimField] = twinStick->aim;
|
|
702
|
+
entity.boolFields[twinStick->fireField] = twinStick->fire;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// 2. shield + telegraph countdowns
|
|
706
|
+
for (const int handle : live) {
|
|
707
|
+
Entity& entity = entityAt(runtime, handle);
|
|
708
|
+
Component* shield = findComponent(entity, Kind::SpawnShield);
|
|
709
|
+
if (shield != nullptr && shield->remaining > 0) {
|
|
710
|
+
const double next = shield->remaining - dt;
|
|
711
|
+
shield->remaining = next > 0 ? next : 0; // Math.max(0, next)
|
|
712
|
+
}
|
|
713
|
+
Component* telegraph = findComponent(entity, Kind::Telegraph);
|
|
714
|
+
if (telegraph != nullptr && telegraph->remaining > 0) {
|
|
715
|
+
const double next = telegraph->remaining - dt;
|
|
716
|
+
telegraph->remaining = next > 0 ? next : 0;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// 3. driven movement
|
|
721
|
+
for (const int handle : live) {
|
|
722
|
+
Entity& entity = entityAt(runtime, handle);
|
|
723
|
+
Component* drive = findComponent(entity, Kind::AxisDriveMover);
|
|
724
|
+
if (drive == nullptr) continue;
|
|
725
|
+
Component* transform = findComponent(entity, Kind::Transform2D);
|
|
726
|
+
const Vec2 movement = getVecField(entity, drive->axisField);
|
|
727
|
+
const double mag = detHypot(movement.x, movement.y);
|
|
728
|
+
if (mag > 0.0001) {
|
|
729
|
+
const double denom = mag > 1 ? mag : 1; // Math.max(mag, 1)
|
|
730
|
+
const double nx = movement.x / denom;
|
|
731
|
+
const double ny = movement.y / denom;
|
|
732
|
+
transform->position.x += nx * drive->maxSpeedPxPerSec * dt;
|
|
733
|
+
transform->position.y += ny * drive->maxSpeedPxPerSec * dt;
|
|
734
|
+
}
|
|
735
|
+
Component* bounds = findComponent(entity, Kind::ArenaBounds);
|
|
736
|
+
if (bounds != nullptr) applyBounds(runtime, entity, *bounds);
|
|
737
|
+
Component* headingComponent = findComponent(entity, Kind::HeadingFromVectors);
|
|
738
|
+
if (headingComponent != nullptr) {
|
|
739
|
+
const Vec2 aim = getVecField(entity, headingComponent->aimField);
|
|
740
|
+
const double aimMag = detHypot(aim.x, aim.y);
|
|
741
|
+
if (aimMag > 0.0001) {
|
|
742
|
+
transform->heading = rundot::detmath::det_atan2(aim.y, aim.x);
|
|
743
|
+
} else if (mag > 0.0001) {
|
|
744
|
+
transform->heading = rundot::detmath::det_atan2(movement.y, movement.x);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// 4. auto-fire (BulletSwarm.autoFire transcription)
|
|
750
|
+
for (const int handle : live) {
|
|
751
|
+
Entity& entity = entityAt(runtime, handle);
|
|
752
|
+
Component* weapon = findComponent(entity, Kind::AutoFireWeapon);
|
|
753
|
+
if (weapon == nullptr) continue;
|
|
754
|
+
const bool fire = getBoolField(entity, weapon->fireField);
|
|
755
|
+
if (!fire) {
|
|
756
|
+
weapon->cooldown = 0;
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
const Vec2 aim = getVecField(entity, weapon->aimField);
|
|
760
|
+
const double aimMag = detHypot(aim.x, aim.y);
|
|
761
|
+
if (aimMag < 0.0001) continue;
|
|
762
|
+
Component* transform = findComponent(entity, Kind::Transform2D);
|
|
763
|
+
const Vec2 origin = transform->position;
|
|
764
|
+
double cooldown = weapon->cooldown - dt;
|
|
765
|
+
int fired = 0;
|
|
766
|
+
while (cooldown <= 0) {
|
|
767
|
+
cooldown += weapon->fireIntervalSec;
|
|
768
|
+
const double nx = aim.x / aimMag;
|
|
769
|
+
const double ny = aim.y / aimMag;
|
|
770
|
+
const std::uint64_t bulletId = weapon->nextBulletId;
|
|
771
|
+
const double spread = spreadForBullet(static_cast<double>(bulletId), weapon->spreadRad);
|
|
772
|
+
const double cos = rundot::detmath::det_cos(spread);
|
|
773
|
+
const double sin = rundot::detmath::det_sin(spread);
|
|
774
|
+
const double sx = nx * cos - ny * sin;
|
|
775
|
+
const double sy = nx * sin + ny * cos;
|
|
776
|
+
const int bulletHandle = spawnFromTemplate(runtime, weapon->bulletTemplate);
|
|
777
|
+
if (bulletHandle < 0) break; // pool cap: break WITHOUT incrementing nextBulletId
|
|
778
|
+
Entity& bullet = entityAt(runtime, bulletHandle);
|
|
779
|
+
Component* bulletTransform = findComponent(bullet, Kind::Transform2D);
|
|
780
|
+
bulletTransform->position.x = origin.x;
|
|
781
|
+
bulletTransform->position.y = origin.y;
|
|
782
|
+
bulletTransform->velocity.x = sx * weapon->speedPxPerSec;
|
|
783
|
+
bulletTransform->velocity.y = sy * weapon->speedPxPerSec;
|
|
784
|
+
bulletTransform->heading = rundot::detmath::det_atan2(sy, sx);
|
|
785
|
+
// JS pushes 'spawned' inside spawnFromTemplate after setup ran; here
|
|
786
|
+
// setup is inlined at the call site, so push after it.
|
|
787
|
+
runtime.frameEvents.push_back({"spawned", weapon->bulletTemplate, bulletTransform->position.x, bulletTransform->position.y});
|
|
788
|
+
weapon->nextBulletId += 1;
|
|
789
|
+
fired += 1;
|
|
790
|
+
if (fired > 100) break;
|
|
791
|
+
}
|
|
792
|
+
weapon->cooldown = cooldown;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// refresh live to include this tick's spawns for integration
|
|
796
|
+
const std::vector<int> liveAfterFire = liveEntities(runtime);
|
|
797
|
+
|
|
798
|
+
// 5. linear movers integrate + bounds
|
|
799
|
+
for (const int handle : liveAfterFire) {
|
|
800
|
+
Entity& entity = entityAt(runtime, handle);
|
|
801
|
+
if (!entity.alive || findComponent(entity, Kind::LinearMover) == nullptr) continue;
|
|
802
|
+
Component* transform = findComponent(entity, Kind::Transform2D);
|
|
803
|
+
transform->position.x += transform->velocity.x * dt;
|
|
804
|
+
transform->position.y += transform->velocity.y * dt;
|
|
805
|
+
Component* lifetime = findComponent(entity, Kind::Lifetime);
|
|
806
|
+
if (lifetime != nullptr) lifetime->age += dt;
|
|
807
|
+
Component* bounds = findComponent(entity, Kind::ArenaBounds);
|
|
808
|
+
if (bounds != nullptr) applyBounds(runtime, entity, *bounds);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
std::vector<DeathEvent> events;
|
|
812
|
+
// 6. pre-AI bullet collisions
|
|
813
|
+
runCollisionPass(runtime, liveAfterFire, "bullet-kill", events);
|
|
814
|
+
handleDeathEvents(runtime, events);
|
|
815
|
+
events.clear();
|
|
816
|
+
|
|
817
|
+
// 7. steering behaviors
|
|
818
|
+
for (const int handle : liveEntities(runtime)) {
|
|
819
|
+
Entity& entity = entityAt(runtime, handle);
|
|
820
|
+
if (!entity.alive || entityTelegraphing(entity)) continue;
|
|
821
|
+
Component* wander = findComponent(entity, Kind::WanderMover);
|
|
822
|
+
if (wander != nullptr) {
|
|
823
|
+
Component* transform = findComponent(entity, Kind::Transform2D);
|
|
824
|
+
wander->rerollTimer -= dt;
|
|
825
|
+
if (wander->rerollTimer <= 0) {
|
|
826
|
+
wander->rerollTimer += wander->rerollSeconds;
|
|
827
|
+
const double current = rundot::detmath::det_atan2(transform->velocity.y, transform->velocity.x);
|
|
828
|
+
const double nudged = current + gaussian(runtime, wander->rngStream) * wander->sigmaRad;
|
|
829
|
+
transform->velocity.x = rundot::detmath::det_cos(nudged) * wander->speedPx;
|
|
830
|
+
transform->velocity.y = rundot::detmath::det_sin(nudged) * wander->speedPx;
|
|
831
|
+
}
|
|
832
|
+
transform->position.x += transform->velocity.x * dt;
|
|
833
|
+
transform->position.y += transform->velocity.y * dt;
|
|
834
|
+
Component* bounds = findComponent(entity, Kind::ArenaBounds);
|
|
835
|
+
if (bounds != nullptr) applyBounds(runtime, entity, *bounds);
|
|
836
|
+
}
|
|
837
|
+
Component* seek = findComponent(entity, Kind::SeekMover);
|
|
838
|
+
if (seek != nullptr) {
|
|
839
|
+
Component* transform = findComponent(entity, Kind::Transform2D);
|
|
840
|
+
seek->aliveSeconds += dt;
|
|
841
|
+
const double t = seek->aliveSeconds;
|
|
842
|
+
double speed = seek->baseSpeedPx;
|
|
843
|
+
if (t > seek->accelStartSec) {
|
|
844
|
+
const double raw = (t - seek->accelStartSec) / (seek->accelEndSec - seek->accelStartSec);
|
|
845
|
+
const double lerp = raw < 1 ? raw : 1; // Math.min(1, raw)
|
|
846
|
+
speed = seek->baseSpeedPx + (seek->topSpeedPx - seek->baseSpeedPx) * lerp;
|
|
847
|
+
}
|
|
848
|
+
Vec2 target;
|
|
849
|
+
if (nearestShipPosition(runtime, transform->position, target)) {
|
|
850
|
+
const double dx = target.x - transform->position.x;
|
|
851
|
+
const double dy = target.y - transform->position.y;
|
|
852
|
+
const double mag = detHypot(dx, dy);
|
|
853
|
+
if (mag > 0.0001) {
|
|
854
|
+
transform->velocity.x = (dx / mag) * speed;
|
|
855
|
+
transform->velocity.y = (dy / mag) * speed;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
transform->position.x += transform->velocity.x * dt;
|
|
859
|
+
transform->position.y += transform->velocity.y * dt;
|
|
860
|
+
Component* bounds = findComponent(entity, Kind::ArenaBounds);
|
|
861
|
+
if (bounds != nullptr) applyBounds(runtime, entity, *bounds);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// 8. post-AI collisions
|
|
866
|
+
const std::vector<int> liveAfterAi = liveEntities(runtime);
|
|
867
|
+
runCollisionPass(runtime, liveAfterAi, "bullet-kill", events);
|
|
868
|
+
runCollisionPass(runtime, liveAfterAi, "ship-death", events);
|
|
869
|
+
handleDeathEvents(runtime, events);
|
|
870
|
+
events.clear();
|
|
871
|
+
|
|
872
|
+
// 9. lifetimes
|
|
873
|
+
for (const int handle : liveEntities(runtime)) {
|
|
874
|
+
Entity& entity = entityAt(runtime, handle);
|
|
875
|
+
Component* lifetime = findComponent(entity, Kind::Lifetime);
|
|
876
|
+
if (lifetime != nullptr && lifetime->age >= lifetime->maxSeconds) despawn(runtime, entity);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
std::vector<Entity> survivors;
|
|
880
|
+
survivors.reserve(runtime.pooled.size());
|
|
881
|
+
for (Entity& entity : runtime.pooled) {
|
|
882
|
+
if (entity.alive) survivors.push_back(std::move(entity));
|
|
883
|
+
}
|
|
884
|
+
runtime.pooled = std::move(survivors);
|
|
885
|
+
runtime.tick += 1;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
void putBytes(std::vector<std::uint8_t>& buffer, const void* data, std::size_t size) {
|
|
889
|
+
const std::uint8_t* bytes = static_cast<const std::uint8_t*>(data);
|
|
890
|
+
buffer.insert(buffer.end(), bytes, bytes + size);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
void putU32(std::vector<std::uint8_t>& buffer, std::uint32_t value) {
|
|
894
|
+
const std::uint8_t bytes[4] = {
|
|
895
|
+
static_cast<std::uint8_t>(value & 0xff),
|
|
896
|
+
static_cast<std::uint8_t>((value >> 8) & 0xff),
|
|
897
|
+
static_cast<std::uint8_t>((value >> 16) & 0xff),
|
|
898
|
+
static_cast<std::uint8_t>((value >> 24) & 0xff),
|
|
899
|
+
};
|
|
900
|
+
putBytes(buffer, bytes, 4);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
void putU64(std::vector<std::uint8_t>& buffer, std::uint64_t value) {
|
|
904
|
+
std::uint8_t bytes[8];
|
|
905
|
+
for (int index = 0; index < 8; ++index) bytes[index] = static_cast<std::uint8_t>((value >> (index * 8)) & 0xff);
|
|
906
|
+
putBytes(buffer, bytes, 8);
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
void putI64(std::vector<std::uint8_t>& buffer, std::int64_t value) {
|
|
910
|
+
putU64(buffer, static_cast<std::uint64_t>(value));
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
void putF64(std::vector<std::uint8_t>& buffer, double value) {
|
|
914
|
+
putU64(buffer, std::bit_cast<std::uint64_t>(value));
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
void putString(std::vector<std::uint8_t>& buffer, const std::string& value) {
|
|
918
|
+
putU32(buffer, static_cast<std::uint32_t>(value.size()));
|
|
919
|
+
putBytes(buffer, value.data(), value.size());
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
void hashEntity(std::vector<std::uint8_t>& buffer, Entity& entity) {
|
|
923
|
+
putString(buffer, entity.ref);
|
|
924
|
+
putU64(buffer, entity.spawnId);
|
|
925
|
+
for (Component& component : entity.components) {
|
|
926
|
+
putString(buffer, kKindNames[static_cast<std::size_t>(component.kind)]);
|
|
927
|
+
switch (component.kind) {
|
|
928
|
+
case Kind::Transform2D:
|
|
929
|
+
putF64(buffer, component.position.x);
|
|
930
|
+
putF64(buffer, component.position.y);
|
|
931
|
+
putF64(buffer, component.velocity.x);
|
|
932
|
+
putF64(buffer, component.velocity.y);
|
|
933
|
+
putF64(buffer, component.heading);
|
|
934
|
+
break;
|
|
935
|
+
case Kind::TwinStickInput:
|
|
936
|
+
putF64(buffer, component.move.x);
|
|
937
|
+
putF64(buffer, component.move.y);
|
|
938
|
+
putF64(buffer, component.aim.x);
|
|
939
|
+
putF64(buffer, component.aim.y);
|
|
940
|
+
buffer.push_back(component.fire ? 1 : 0);
|
|
941
|
+
break;
|
|
942
|
+
case Kind::SpawnShield:
|
|
943
|
+
case Kind::Telegraph:
|
|
944
|
+
putF64(buffer, component.remaining);
|
|
945
|
+
break;
|
|
946
|
+
case Kind::WanderMover:
|
|
947
|
+
putF64(buffer, component.rerollTimer);
|
|
948
|
+
break;
|
|
949
|
+
case Kind::SeekMover:
|
|
950
|
+
putF64(buffer, component.aliveSeconds);
|
|
951
|
+
break;
|
|
952
|
+
case Kind::AutoFireWeapon:
|
|
953
|
+
putF64(buffer, component.cooldown);
|
|
954
|
+
putU64(buffer, component.nextBulletId);
|
|
955
|
+
break;
|
|
956
|
+
case Kind::Lifetime:
|
|
957
|
+
putF64(buffer, component.age);
|
|
958
|
+
break;
|
|
959
|
+
case Kind::Health:
|
|
960
|
+
putI64(buffer, component.hp);
|
|
961
|
+
break;
|
|
962
|
+
case Kind::ResourceCounter:
|
|
963
|
+
putI64(buffer, component.score);
|
|
964
|
+
putI64(buffer, component.deaths);
|
|
965
|
+
break;
|
|
966
|
+
default:
|
|
967
|
+
break;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
std::string canonicalTickHash(Runtime& runtime) {
|
|
973
|
+
std::vector<std::uint8_t> buffer;
|
|
974
|
+
buffer.reserve(1 << 16);
|
|
975
|
+
putU64(buffer, static_cast<std::uint64_t>(runtime.tick));
|
|
976
|
+
for (const RngStreamRt& stream : runtime.rngStreams) {
|
|
977
|
+
putString(buffer, stream.id);
|
|
978
|
+
putU32(buffer, stream.state);
|
|
979
|
+
}
|
|
980
|
+
for (Entity& entity : runtime.statics) {
|
|
981
|
+
if (entity.alive) hashEntity(buffer, entity);
|
|
982
|
+
}
|
|
983
|
+
// pooled is maintained in spawn order == ascending spawnId
|
|
984
|
+
for (Entity& entity : runtime.pooled) {
|
|
985
|
+
if (entity.alive) hashEntity(buffer, entity);
|
|
986
|
+
}
|
|
987
|
+
return rundot::sha256Hex(buffer.data(), buffer.size());
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
} // namespace
|
|
991
|
+
|
|
992
|
+
F64RunResult runReplayF64(const PackDef& pack, const ReplayDef& replay, bool hashEveryTick) {
|
|
993
|
+
Runtime runtime = createRuntime(pack);
|
|
994
|
+
std::map<std::int64_t, const std::vector<SlotInputDef>*> inputsByTick;
|
|
995
|
+
for (const InputFrameDef& frame : replay.inputFrames) {
|
|
996
|
+
inputsByTick[frame.tick] = &frame.slots;
|
|
997
|
+
}
|
|
998
|
+
std::vector<SlotInputDef> currentInputs(static_cast<std::size_t>(pack.inputSlots));
|
|
999
|
+
|
|
1000
|
+
F64RunResult result;
|
|
1001
|
+
std::string streamAccum;
|
|
1002
|
+
for (std::int64_t tick = 0; tick < replay.tickCount; tick += 1) {
|
|
1003
|
+
const auto frame = inputsByTick.find(tick);
|
|
1004
|
+
if (frame != inputsByTick.end()) currentInputs = *frame->second;
|
|
1005
|
+
tickRuntime(runtime, currentInputs);
|
|
1006
|
+
if (hashEveryTick || tick == replay.tickCount - 1) {
|
|
1007
|
+
const std::string tickHash = canonicalTickHash(runtime);
|
|
1008
|
+
if (hashEveryTick) {
|
|
1009
|
+
result.tickHashes.push_back(tickHash);
|
|
1010
|
+
} else {
|
|
1011
|
+
result.tickHashes.assign(1, tickHash);
|
|
1012
|
+
}
|
|
1013
|
+
streamAccum += tickHash;
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
result.finalDigest = rundot::sha256Hex(reinterpret_cast<const std::uint8_t*>(streamAccum.data()), streamAccum.size());
|
|
1017
|
+
result.completedTicks = replay.tickCount;
|
|
1018
|
+
std::int64_t alivePooled = 0;
|
|
1019
|
+
for (const Entity& entity : runtime.pooled) {
|
|
1020
|
+
if (entity.alive) alivePooled += 1;
|
|
1021
|
+
}
|
|
1022
|
+
// liveEntities() counts all statics (even dead) plus alive pooled
|
|
1023
|
+
result.liveEntitiesAtEnd = static_cast<std::int64_t>(runtime.statics.size()) + alivePooled;
|
|
1024
|
+
for (Entity& entity : runtime.statics) {
|
|
1025
|
+
Component* counter = findComponent(entity, Kind::ResourceCounter);
|
|
1026
|
+
if (counter != nullptr) {
|
|
1027
|
+
result.finalScore = counter->score;
|
|
1028
|
+
result.shipDeaths = counter->deaths;
|
|
1029
|
+
break;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
return result;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// component_renderer_f64.cpp includes this .cpp verbatim (see its header
|
|
1036
|
+
// comment); it defines RUNDOT_F64RT_OMIT_SIM_STEPPER so the stepper's
|
|
1037
|
+
// external-linkage symbols are only emitted from this TU.
|
|
1038
|
+
#if !defined(RUNDOT_F64RT_OMIT_SIM_STEPPER)
|
|
1039
|
+
|
|
1040
|
+
struct F64SimStepper::Impl {
|
|
1041
|
+
Runtime runtime;
|
|
1042
|
+
};
|
|
1043
|
+
|
|
1044
|
+
F64SimStepper::F64SimStepper(const PackDef& pack) : impl_(std::make_unique<Impl>()) {
|
|
1045
|
+
impl_->runtime = createRuntime(pack);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
F64SimStepper::~F64SimStepper() = default;
|
|
1049
|
+
|
|
1050
|
+
void F64SimStepper::tick(const std::vector<SlotInputDef>& inputs) {
|
|
1051
|
+
tickRuntime(impl_->runtime, inputs);
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
void F64SimStepper::liveView(std::vector<SimEntityView>& out) {
|
|
1055
|
+
Runtime& runtime = impl_->runtime;
|
|
1056
|
+
out.clear();
|
|
1057
|
+
for (const int handle : liveEntities(runtime)) {
|
|
1058
|
+
Entity& entity = entityAt(runtime, handle);
|
|
1059
|
+
if (!entity.alive) continue; // killed statics stay listed; never render them
|
|
1060
|
+
SimEntityView view;
|
|
1061
|
+
view.ref = entity.ref;
|
|
1062
|
+
view.spawnId = entity.spawnId;
|
|
1063
|
+
if (Component* transform = findComponent(entity, Kind::Transform2D)) {
|
|
1064
|
+
view.hasTransform = true;
|
|
1065
|
+
view.x = transform->position.x;
|
|
1066
|
+
view.y = transform->position.y;
|
|
1067
|
+
view.vx = transform->velocity.x;
|
|
1068
|
+
view.vy = transform->velocity.y;
|
|
1069
|
+
view.heading = transform->heading;
|
|
1070
|
+
}
|
|
1071
|
+
if (Component* telegraph = findComponent(entity, Kind::Telegraph)) {
|
|
1072
|
+
view.hasTelegraph = true;
|
|
1073
|
+
view.telegraphRemaining = telegraph->remaining;
|
|
1074
|
+
}
|
|
1075
|
+
out.push_back(std::move(view));
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
const std::vector<FrameEventDef>& F64SimStepper::frameEvents() const {
|
|
1080
|
+
return impl_->runtime.frameEvents;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
#endif // !defined(RUNDOT_F64RT_OMIT_SIM_STEPPER)
|
|
1084
|
+
|
|
1085
|
+
} // namespace rundot::f64rt
|