@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,377 @@
|
|
|
1
|
+
export const kinetixMechanicEventBits = Object.freeze({
|
|
2
|
+
phase: 1,
|
|
3
|
+
spawn: 2,
|
|
4
|
+
score: 4,
|
|
5
|
+
bomb: 8,
|
|
6
|
+
multiplier: 16,
|
|
7
|
+
boundary: 32,
|
|
8
|
+
stagedAxis: 64,
|
|
9
|
+
targetDrift: 128,
|
|
10
|
+
gravityWell: 256,
|
|
11
|
+
followChain: 512,
|
|
12
|
+
growth: 1024,
|
|
13
|
+
randomSteering: 2048,
|
|
14
|
+
projectile: 4096,
|
|
15
|
+
driveFire: 8192,
|
|
16
|
+
pickup: 16384,
|
|
17
|
+
death: 32768,
|
|
18
|
+
respawn: 65536,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export const kinetixRequiredMechanicEventMask = Object.values(kinetixMechanicEventBits)
|
|
22
|
+
.reduce((mask, bit) => mask | bit, 0);
|
|
23
|
+
|
|
24
|
+
export const kinetixInstalledMechanicFamilyOrder = Object.freeze([
|
|
25
|
+
'phase-spawn-score-policy',
|
|
26
|
+
'consumable-charge-economy',
|
|
27
|
+
'score-multiplier-decay',
|
|
28
|
+
'boundary-crossing-oscillator',
|
|
29
|
+
'staged-axis-runner',
|
|
30
|
+
'target-seeking-drift',
|
|
31
|
+
'gravity-well-state-machine',
|
|
32
|
+
'segmented-follow-chain',
|
|
33
|
+
'spawn-pellet-growth',
|
|
34
|
+
'bounded-random-steering',
|
|
35
|
+
'projectile-lifecycle-collision',
|
|
36
|
+
'axis-drive-fire-control',
|
|
37
|
+
'pickup-drift-lifecycle',
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
export function decodeInstalledMechanicConfig(config, decodeF64) {
|
|
41
|
+
const decode = (value) => {
|
|
42
|
+
if (typeof value === 'string') return decodeF64(value);
|
|
43
|
+
if (Array.isArray(value)) return value.map(decode);
|
|
44
|
+
if (typeof value === 'object' && value !== null) {
|
|
45
|
+
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, decode(entry)]));
|
|
46
|
+
}
|
|
47
|
+
return value;
|
|
48
|
+
};
|
|
49
|
+
return decode(config);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function createInstalledMechanicState(config) {
|
|
53
|
+
return {
|
|
54
|
+
tick: 0,
|
|
55
|
+
phase: 0,
|
|
56
|
+
phaseElapsed: 0,
|
|
57
|
+
deathElapsed: 0,
|
|
58
|
+
deathOccurred: false,
|
|
59
|
+
spawnCooldown: 0,
|
|
60
|
+
score: 0,
|
|
61
|
+
charges: config.charge.startCharges,
|
|
62
|
+
lives: config.charge.startLives,
|
|
63
|
+
chargesEarned: 0,
|
|
64
|
+
livesEarned: 0,
|
|
65
|
+
previousBomb: false,
|
|
66
|
+
multiplier: 1,
|
|
67
|
+
multiplierPeak: 1,
|
|
68
|
+
multiplierDecayFrom: 1,
|
|
69
|
+
multiplierDecayElapsed: 0,
|
|
70
|
+
multiplierDecaying: false,
|
|
71
|
+
oscillatorX: config.boundary.min,
|
|
72
|
+
oscillatorY: 0,
|
|
73
|
+
oscillatorDirection: 1,
|
|
74
|
+
stagedX: 0,
|
|
75
|
+
stagedY: 0,
|
|
76
|
+
stagedMode: 0,
|
|
77
|
+
targetX: config.target.startX,
|
|
78
|
+
targetY: config.target.startY,
|
|
79
|
+
targetAge: 0,
|
|
80
|
+
gravityRadius: 0,
|
|
81
|
+
gravityHp: config.gravity.hitPoints,
|
|
82
|
+
gravityIngested: 0,
|
|
83
|
+
gravityStage: 0,
|
|
84
|
+
chainHeadX: 0,
|
|
85
|
+
chainHeadY: 0,
|
|
86
|
+
chainTailX: -config.follow.spacing * config.follow.segments,
|
|
87
|
+
chainTailY: 0,
|
|
88
|
+
growthAge: 0,
|
|
89
|
+
growthRadius: config.growth.startRadius,
|
|
90
|
+
growthCycles: 0,
|
|
91
|
+
randomX: config.random.startX,
|
|
92
|
+
randomY: config.random.startY,
|
|
93
|
+
randomVx: config.random.speed,
|
|
94
|
+
randomVy: 0,
|
|
95
|
+
randomTimer: 0,
|
|
96
|
+
randomState: config.random.seed >>> 0,
|
|
97
|
+
projectileX: 0,
|
|
98
|
+
projectileY: 0,
|
|
99
|
+
projectileVx: 0,
|
|
100
|
+
projectileVy: 0,
|
|
101
|
+
projectileAge: 0,
|
|
102
|
+
projectileActive: false,
|
|
103
|
+
shipX: config.drive.startX,
|
|
104
|
+
shipY: config.drive.startY,
|
|
105
|
+
fireCooldown: 0,
|
|
106
|
+
shotsFired: 0,
|
|
107
|
+
pickupX: config.pickup.startX,
|
|
108
|
+
pickupY: config.pickup.startY,
|
|
109
|
+
pickupAge: 0,
|
|
110
|
+
pickupActive: true,
|
|
111
|
+
eventMask: 0,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function clamp(value, min, max) {
|
|
116
|
+
return Math.max(min, Math.min(max, value));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function normalized(vector) {
|
|
120
|
+
const magnitude = Math.hypot(vector.x, vector.y);
|
|
121
|
+
if (magnitude <= 0.0001) return { x: 0, y: 0 };
|
|
122
|
+
return { x: vector.x / magnitude, y: vector.y / magnitude };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function nextRandom(state) {
|
|
126
|
+
return (Math.imul(state, 1664525) + 1013904223) >>> 0;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function stepInstalledMechanics(current, input, config, dt) {
|
|
130
|
+
const state = structuredClone(current);
|
|
131
|
+
state.tick += 1;
|
|
132
|
+
state.phaseElapsed += dt;
|
|
133
|
+
|
|
134
|
+
let scored = false;
|
|
135
|
+
if (state.phase === 0 && state.phaseElapsed >= config.phase.spawnDelay) {
|
|
136
|
+
state.phase = 1;
|
|
137
|
+
state.spawnCooldown = 0;
|
|
138
|
+
state.eventMask |= kinetixMechanicEventBits.phase;
|
|
139
|
+
}
|
|
140
|
+
if (state.phase === 1) {
|
|
141
|
+
state.spawnCooldown -= dt;
|
|
142
|
+
while (state.spawnCooldown <= 0) {
|
|
143
|
+
state.spawnCooldown += config.phase.spawnInterval;
|
|
144
|
+
state.score += config.phase.scorePerSpawn;
|
|
145
|
+
scored = true;
|
|
146
|
+
state.eventMask |= kinetixMechanicEventBits.spawn | kinetixMechanicEventBits.score;
|
|
147
|
+
}
|
|
148
|
+
if (!state.deathOccurred && state.phaseElapsed >= config.phase.deathAt) {
|
|
149
|
+
state.phase = 2;
|
|
150
|
+
state.deathElapsed = 0;
|
|
151
|
+
state.deathOccurred = true;
|
|
152
|
+
state.lives = Math.max(0, state.lives - 1);
|
|
153
|
+
state.multiplierDecaying = true;
|
|
154
|
+
state.multiplierDecayFrom = state.multiplier;
|
|
155
|
+
state.multiplierDecayElapsed = 0;
|
|
156
|
+
state.eventMask |= kinetixMechanicEventBits.death;
|
|
157
|
+
}
|
|
158
|
+
} else if (state.phase === 2) {
|
|
159
|
+
state.deathElapsed += dt;
|
|
160
|
+
if (state.deathElapsed >= config.phase.respawnDelay) {
|
|
161
|
+
state.phase = 1;
|
|
162
|
+
state.multiplierDecaying = false;
|
|
163
|
+
state.eventMask |= kinetixMechanicEventBits.respawn;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const chargeMilestones = Math.floor(state.score / config.charge.chargeEveryScore);
|
|
168
|
+
if (chargeMilestones > state.chargesEarned) {
|
|
169
|
+
state.charges = Math.min(
|
|
170
|
+
config.charge.maxCharges,
|
|
171
|
+
state.charges + chargeMilestones - state.chargesEarned,
|
|
172
|
+
);
|
|
173
|
+
state.chargesEarned = chargeMilestones;
|
|
174
|
+
}
|
|
175
|
+
const lifeMilestones = Math.floor(state.score / config.charge.lifeEveryScore);
|
|
176
|
+
if (lifeMilestones > state.livesEarned) {
|
|
177
|
+
state.lives += lifeMilestones - state.livesEarned;
|
|
178
|
+
state.livesEarned = lifeMilestones;
|
|
179
|
+
}
|
|
180
|
+
if (input.bomb && !state.previousBomb && state.charges > 0) {
|
|
181
|
+
state.charges -= 1;
|
|
182
|
+
state.eventMask |= kinetixMechanicEventBits.bomb;
|
|
183
|
+
}
|
|
184
|
+
state.previousBomb = input.bomb;
|
|
185
|
+
|
|
186
|
+
if (scored && !state.multiplierDecaying) {
|
|
187
|
+
state.multiplier = Math.min(config.multiplier.cap, state.multiplier + config.multiplier.increment);
|
|
188
|
+
state.multiplierPeak = Math.max(state.multiplierPeak, state.multiplier);
|
|
189
|
+
state.eventMask |= kinetixMechanicEventBits.multiplier;
|
|
190
|
+
}
|
|
191
|
+
if (state.multiplierDecaying) {
|
|
192
|
+
state.multiplierDecayElapsed += dt;
|
|
193
|
+
if (state.multiplierDecayElapsed >= config.multiplier.decayWindow) {
|
|
194
|
+
state.multiplier = 1;
|
|
195
|
+
state.multiplierDecaying = false;
|
|
196
|
+
} else {
|
|
197
|
+
const progress = state.multiplierDecayElapsed / config.multiplier.decayWindow;
|
|
198
|
+
state.multiplier = state.multiplierDecayFrom + (1 - state.multiplierDecayFrom) * progress;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
state.oscillatorX += state.oscillatorDirection * config.boundary.speed * dt;
|
|
203
|
+
state.oscillatorY += state.oscillatorDirection * config.boundary.lateralSpeed * dt;
|
|
204
|
+
if (state.oscillatorX <= config.boundary.min || state.oscillatorX >= config.boundary.max) {
|
|
205
|
+
state.oscillatorX = clamp(state.oscillatorX, config.boundary.min, config.boundary.max);
|
|
206
|
+
state.oscillatorDirection *= -1;
|
|
207
|
+
}
|
|
208
|
+
state.eventMask |= kinetixMechanicEventBits.boundary;
|
|
209
|
+
|
|
210
|
+
if (state.stagedMode === 0) {
|
|
211
|
+
state.stagedX += config.staged.approachSpeed * dt;
|
|
212
|
+
if (state.stagedX >= config.staged.switchX) state.stagedMode = 1;
|
|
213
|
+
} else {
|
|
214
|
+
state.stagedY += (input.fire ? -1 : 1) * config.staged.dodgeSpeed * dt;
|
|
215
|
+
}
|
|
216
|
+
state.eventMask |= kinetixMechanicEventBits.stagedAxis;
|
|
217
|
+
|
|
218
|
+
state.targetAge += dt;
|
|
219
|
+
const targetProgress = clamp(
|
|
220
|
+
(state.targetAge - config.target.accelStart) / (config.target.accelEnd - config.target.accelStart),
|
|
221
|
+
0,
|
|
222
|
+
1,
|
|
223
|
+
);
|
|
224
|
+
const targetSpeed = config.target.baseSpeed
|
|
225
|
+
+ (config.target.topSpeed - config.target.baseSpeed) * targetProgress;
|
|
226
|
+
const targetDirection = normalized({ x: state.shipX - state.targetX, y: state.shipY - state.targetY });
|
|
227
|
+
state.targetX += targetDirection.x * targetSpeed * dt;
|
|
228
|
+
state.targetY += targetDirection.y * targetSpeed * dt;
|
|
229
|
+
state.eventMask |= kinetixMechanicEventBits.targetDrift;
|
|
230
|
+
|
|
231
|
+
if (state.gravityStage === 0) {
|
|
232
|
+
state.gravityRadius = Math.min(
|
|
233
|
+
config.gravity.activeRadius,
|
|
234
|
+
state.gravityRadius + (config.gravity.activeRadius / config.gravity.growSeconds) * dt,
|
|
235
|
+
);
|
|
236
|
+
if (state.gravityRadius >= config.gravity.activeRadius) state.gravityStage = 1;
|
|
237
|
+
} else if (state.gravityStage === 1) {
|
|
238
|
+
if (scored) state.gravityIngested += 1;
|
|
239
|
+
if (input.fire && state.tick % 30 === 0) state.gravityHp -= 1;
|
|
240
|
+
if (state.gravityHp <= 0) state.gravityStage = 2;
|
|
241
|
+
}
|
|
242
|
+
state.eventMask |= kinetixMechanicEventBits.gravityWell;
|
|
243
|
+
|
|
244
|
+
const chainDirection = normalized({ x: state.shipX - state.chainHeadX, y: state.shipY - state.chainHeadY });
|
|
245
|
+
state.chainHeadX += chainDirection.x * config.follow.speed * dt;
|
|
246
|
+
state.chainHeadY += chainDirection.y * config.follow.speed * dt;
|
|
247
|
+
const followAlpha = Math.min(1, config.follow.followRate * dt);
|
|
248
|
+
const desiredTailX = state.chainHeadX - chainDirection.x * config.follow.spacing * config.follow.segments;
|
|
249
|
+
const desiredTailY = state.chainHeadY - chainDirection.y * config.follow.spacing * config.follow.segments;
|
|
250
|
+
state.chainTailX += (desiredTailX - state.chainTailX) * followAlpha;
|
|
251
|
+
state.chainTailY += (desiredTailY - state.chainTailY) * followAlpha;
|
|
252
|
+
state.eventMask |= kinetixMechanicEventBits.followChain;
|
|
253
|
+
|
|
254
|
+
state.growthAge += dt;
|
|
255
|
+
state.growthRadius = config.growth.startRadius + (
|
|
256
|
+
config.growth.endRadius - config.growth.startRadius
|
|
257
|
+
) * Math.min(1, state.growthAge / config.growth.growSeconds);
|
|
258
|
+
if (state.growthAge >= config.growth.lifetime) {
|
|
259
|
+
state.growthAge -= config.growth.lifetime;
|
|
260
|
+
state.growthRadius = config.growth.startRadius;
|
|
261
|
+
state.growthCycles += 1;
|
|
262
|
+
}
|
|
263
|
+
state.eventMask |= kinetixMechanicEventBits.growth;
|
|
264
|
+
|
|
265
|
+
state.randomTimer -= dt;
|
|
266
|
+
if (state.randomTimer <= 0) {
|
|
267
|
+
state.randomTimer += config.random.rerollSeconds;
|
|
268
|
+
state.randomState = nextRandom(state.randomState);
|
|
269
|
+
state.randomVx = ((state.randomState / 4294967296) * 2 - 1) * config.random.speed;
|
|
270
|
+
state.randomState = nextRandom(state.randomState);
|
|
271
|
+
state.randomVy = ((state.randomState / 4294967296) * 2 - 1) * config.random.speed;
|
|
272
|
+
}
|
|
273
|
+
state.randomX += state.randomVx * dt;
|
|
274
|
+
state.randomY += state.randomVy * dt;
|
|
275
|
+
if (state.randomX <= config.random.minX || state.randomX >= config.random.maxX) {
|
|
276
|
+
state.randomX = clamp(state.randomX, config.random.minX, config.random.maxX);
|
|
277
|
+
state.randomVx *= -1;
|
|
278
|
+
}
|
|
279
|
+
if (state.randomY <= config.random.minY || state.randomY >= config.random.maxY) {
|
|
280
|
+
state.randomY = clamp(state.randomY, config.random.minY, config.random.maxY);
|
|
281
|
+
state.randomVy *= -1;
|
|
282
|
+
}
|
|
283
|
+
state.eventMask |= kinetixMechanicEventBits.randomSteering;
|
|
284
|
+
|
|
285
|
+
const movement = normalized(input.movement);
|
|
286
|
+
state.shipX = clamp(
|
|
287
|
+
state.shipX + movement.x * config.drive.maxSpeed * dt,
|
|
288
|
+
config.drive.minX,
|
|
289
|
+
config.drive.maxX,
|
|
290
|
+
);
|
|
291
|
+
state.shipY = clamp(
|
|
292
|
+
state.shipY + movement.y * config.drive.maxSpeed * dt,
|
|
293
|
+
config.drive.minY,
|
|
294
|
+
config.drive.maxY,
|
|
295
|
+
);
|
|
296
|
+
state.fireCooldown -= dt;
|
|
297
|
+
const aim = normalized(input.aim);
|
|
298
|
+
if (input.fire && (aim.x !== 0 || aim.y !== 0) && state.fireCooldown <= 0) {
|
|
299
|
+
state.fireCooldown += config.drive.fireInterval;
|
|
300
|
+
state.shotsFired += 1;
|
|
301
|
+
if (!state.projectileActive) {
|
|
302
|
+
state.projectileActive = true;
|
|
303
|
+
state.projectileX = state.shipX;
|
|
304
|
+
state.projectileY = state.shipY;
|
|
305
|
+
state.projectileVx = aim.x * config.projectile.speed;
|
|
306
|
+
state.projectileVy = aim.y * config.projectile.speed;
|
|
307
|
+
state.projectileAge = 0;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
state.eventMask |= kinetixMechanicEventBits.driveFire;
|
|
311
|
+
|
|
312
|
+
if (state.projectileActive) {
|
|
313
|
+
const gravityDx = state.targetX - state.projectileX;
|
|
314
|
+
const gravityDy = state.targetY - state.projectileY;
|
|
315
|
+
const gravityDistanceSq = gravityDx * gravityDx + gravityDy * gravityDy;
|
|
316
|
+
const gravityDistance = Math.sqrt(gravityDistanceSq);
|
|
317
|
+
if (gravityDistance > 0 && gravityDistance <= state.gravityRadius) {
|
|
318
|
+
const force = config.gravity.pullStrength / Math.max(1, gravityDistanceSq);
|
|
319
|
+
state.projectileVx += (gravityDx / gravityDistance) * force * config.projectile.gravityScale * dt;
|
|
320
|
+
state.projectileVy += (gravityDy / gravityDistance) * force * config.projectile.gravityScale * dt;
|
|
321
|
+
}
|
|
322
|
+
state.projectileX += state.projectileVx * dt;
|
|
323
|
+
state.projectileY += state.projectileVy * dt;
|
|
324
|
+
state.projectileAge += dt;
|
|
325
|
+
if (state.projectileAge >= config.projectile.maxAge
|
|
326
|
+
|| state.projectileX < config.projectile.minX
|
|
327
|
+
|| state.projectileX > config.projectile.maxX
|
|
328
|
+
|| state.projectileY < config.projectile.minY
|
|
329
|
+
|| state.projectileY > config.projectile.maxY) {
|
|
330
|
+
state.projectileActive = false;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
state.eventMask |= kinetixMechanicEventBits.projectile;
|
|
334
|
+
|
|
335
|
+
if (state.pickupActive) {
|
|
336
|
+
state.pickupAge += dt;
|
|
337
|
+
state.pickupX += config.pickup.driftSpeed * dt;
|
|
338
|
+
const pickupDistance = Math.hypot(state.pickupX - state.shipX, state.pickupY - state.shipY);
|
|
339
|
+
if (pickupDistance <= config.pickup.collectRadius || state.pickupAge >= config.pickup.lifetime) {
|
|
340
|
+
state.pickupActive = false;
|
|
341
|
+
state.eventMask |= kinetixMechanicEventBits.pickup;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return state;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export function canonicalInstalledMechanicState(state) {
|
|
349
|
+
return {
|
|
350
|
+
tick: state.tick,
|
|
351
|
+
eventMask: state.eventMask,
|
|
352
|
+
score: state.score,
|
|
353
|
+
charges: state.charges,
|
|
354
|
+
lives: state.lives,
|
|
355
|
+
shotsFired: state.shotsFired,
|
|
356
|
+
growthCycles: state.growthCycles,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export function installedMechanicReplayInput(tick) {
|
|
361
|
+
return {
|
|
362
|
+
movement: tick < 180 ? { x: 1, y: 0 } : { x: -1, y: 0.25 },
|
|
363
|
+
aim: { x: 1, y: tick % 120 < 60 ? 0.25 : -0.25 },
|
|
364
|
+
fire: tick % 3 !== 0,
|
|
365
|
+
bomb: tick === 45 || tick === 300,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export function runInstalledMechanicReplay(config, ticks = 600) {
|
|
370
|
+
let state = createInstalledMechanicState(config);
|
|
371
|
+
const states = [];
|
|
372
|
+
for (let tick = 0; tick < ticks; tick += 1) {
|
|
373
|
+
state = stepInstalledMechanics(state, installedMechanicReplayInput(tick), config, 1 / 60);
|
|
374
|
+
states.push(canonicalInstalledMechanicState(state));
|
|
375
|
+
}
|
|
376
|
+
return { state, bytes: Buffer.from(JSON.stringify(states)) };
|
|
377
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createFixedRuntime,
|
|
3
|
+
executeFixedRuntimeSystem,
|
|
4
|
+
} from './kinetix-fixed1000-runtime.mjs';
|
|
5
|
+
|
|
6
|
+
function family({ id, phase, order, reads, writes, numericProfile, structuralCommands = [] }) {
|
|
7
|
+
return Object.freeze({
|
|
8
|
+
id,
|
|
9
|
+
phase,
|
|
10
|
+
order,
|
|
11
|
+
reads: Object.freeze(reads),
|
|
12
|
+
writes: Object.freeze(writes),
|
|
13
|
+
numericProfiles: Object.freeze([numericProfile]),
|
|
14
|
+
structuralCommands: Object.freeze(structuralCommands),
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const fixed = 'fixed-1000-3d';
|
|
19
|
+
const f64 = 'deterministic-f64';
|
|
20
|
+
|
|
21
|
+
export const kinetixInstalledSystemFamilies = Object.freeze([
|
|
22
|
+
family({ id: 'slot-input-resolution', phase: 'fixed-simulation', order: 100, reads: ['InputFrame', 'SlotState'], writes: ['VehicleInput', 'SlotState'], numericProfile: fixed }),
|
|
23
|
+
family({ id: 'vehicle-drive-body-build', phase: 'fixed-simulation', order: 200, reads: ['VehicleInput', 'FixedTransform', 'ColliderRecipe'], writes: ['RigidBody3D'], numericProfile: fixed }),
|
|
24
|
+
family({ id: 'vehicle-physics-step', phase: 'fixed-simulation', order: 300, reads: ['RigidBody3D', 'ColliderRecipe'], writes: ['RigidBody3D', 'VehicleContactState', 'FixedTransform'], numericProfile: fixed }),
|
|
25
|
+
family({ id: 'vehicle-lap-finish', phase: 'fixed-simulation', order: 400, reads: ['FixedTransform', 'TrackCells', 'LapState'], writes: ['LapState', 'FinishState'], numericProfile: fixed }),
|
|
26
|
+
family({ id: 'match-phase-transition', phase: 'post-simulation', order: 500, reads: ['SlotState', 'FinishState', 'MatchPhase'], writes: ['MatchPhase'], numericProfile: fixed }),
|
|
27
|
+
family({ id: 'arena-input-mapping', phase: 'fixed-simulation', order: 100, reads: ['InputFrame'], writes: ['DriveIntent', 'FireIntent'], numericProfile: f64 }),
|
|
28
|
+
family({ id: 'arena-countdowns', phase: 'fixed-simulation', order: 200, reads: ['Countdown'], writes: ['Countdown'], numericProfile: f64 }),
|
|
29
|
+
family({ id: 'arena-axis-drive', phase: 'fixed-simulation', order: 300, reads: ['DriveIntent', 'Transform2D'], writes: ['Velocity2D'], numericProfile: f64 }),
|
|
30
|
+
family({ id: 'arena-autofire', phase: 'fixed-simulation', order: 400, reads: ['FireIntent', 'Transform2D', 'Countdown'], writes: ['Countdown'], numericProfile: f64, structuralCommands: ['spawnPrefab'] }),
|
|
31
|
+
family({ id: 'arena-linear-integration-bounds', phase: 'fixed-simulation', order: 500, reads: ['Transform2D', 'Velocity2D', 'ArenaBounds'], writes: ['Transform2D', 'Velocity2D'], numericProfile: f64 }),
|
|
32
|
+
family({ id: 'arena-collision-pre-ai', phase: 'fixed-simulation', order: 600, reads: ['Transform2D', 'CircleCollider', 'CollisionLayer'], writes: ['Health'], numericProfile: f64, structuralCommands: ['despawnPrefab'] }),
|
|
33
|
+
family({ id: 'arena-bounded-enemy-steering', phase: 'fixed-simulation', order: 700, reads: ['Transform2D', 'EnemySteering', 'ArenaBounds'], writes: ['Velocity2D'], numericProfile: f64 }),
|
|
34
|
+
family({ id: 'arena-collision-post-ai', phase: 'fixed-simulation', order: 800, reads: ['Transform2D', 'CircleCollider', 'CollisionLayer'], writes: ['Health'], numericProfile: f64, structuralCommands: ['despawnPrefab'] }),
|
|
35
|
+
family({ id: 'arena-lifetimes', phase: 'post-simulation', order: 900, reads: ['Lifetime'], writes: ['Lifetime'], numericProfile: f64, structuralCommands: ['despawnPrefab'] }),
|
|
36
|
+
family({ id: 'phase-spawn-score-policy', phase: 'post-simulation', order: 1000, reads: ['RunPhase', 'SpawnPolicy', 'ScoreState'], writes: ['RunPhase', 'SpawnPolicy', 'ScoreState'], numericProfile: f64, structuralCommands: ['spawnPrefab', 'despawnPrefab'] }),
|
|
37
|
+
family({ id: 'consumable-charge-economy', phase: 'post-simulation', order: 1100, reads: ['ScoreState', 'ChargeEconomy'], writes: ['ChargeEconomy'], numericProfile: f64 }),
|
|
38
|
+
family({ id: 'score-multiplier-decay', phase: 'post-simulation', order: 1200, reads: ['ScoreState', 'MultiplierState'], writes: ['MultiplierState'], numericProfile: f64 }),
|
|
39
|
+
family({ id: 'boundary-crossing-oscillator', phase: 'fixed-simulation', order: 710, reads: ['Transform2D', 'OscillatorState', 'ArenaBounds'], writes: ['Transform2D', 'OscillatorState'], numericProfile: f64 }),
|
|
40
|
+
family({ id: 'staged-axis-runner', phase: 'fixed-simulation', order: 720, reads: ['Transform2D', 'StageState', 'ProjectileField'], writes: ['Transform2D', 'StageState'], numericProfile: f64 }),
|
|
41
|
+
family({ id: 'target-seeking-drift', phase: 'fixed-simulation', order: 730, reads: ['Transform2D', 'TargetState'], writes: ['Transform2D'], numericProfile: f64 }),
|
|
42
|
+
family({ id: 'gravity-well-state-machine', phase: 'fixed-simulation', order: 740, reads: ['Transform2D', 'GravityWellState'], writes: ['Transform2D', 'GravityWellState'], numericProfile: f64, structuralCommands: ['spawnPrefab', 'despawnPrefab'] }),
|
|
43
|
+
family({ id: 'segmented-follow-chain', phase: 'fixed-simulation', order: 750, reads: ['Transform2D', 'FollowChainState'], writes: ['Transform2D', 'FollowChainState'], numericProfile: f64 }),
|
|
44
|
+
family({ id: 'spawn-pellet-growth', phase: 'fixed-simulation', order: 760, reads: ['Transform2D', 'GrowthState', 'Lifetime'], writes: ['Transform2D', 'GrowthState', 'Lifetime'], numericProfile: f64, structuralCommands: ['despawnPrefab'] }),
|
|
45
|
+
family({ id: 'bounded-random-steering', phase: 'fixed-simulation', order: 770, reads: ['Transform2D', 'RandomSteeringState', 'ArenaBounds'], writes: ['Transform2D', 'RandomSteeringState'], numericProfile: f64 }),
|
|
46
|
+
family({ id: 'projectile-lifecycle-collision', phase: 'fixed-simulation', order: 780, reads: ['Transform2D', 'ProjectileState', 'CollisionLayer'], writes: ['Transform2D', 'ProjectileState'], numericProfile: f64, structuralCommands: ['spawnPrefab', 'despawnPrefab'] }),
|
|
47
|
+
family({ id: 'axis-drive-fire-control', phase: 'fixed-simulation', order: 790, reads: ['DriveIntent', 'FireIntent', 'Transform2D'], writes: ['Transform2D', 'FireIntent'], numericProfile: f64 }),
|
|
48
|
+
family({ id: 'pickup-drift-lifecycle', phase: 'fixed-simulation', order: 795, reads: ['Transform2D', 'PickupState', 'Lifetime'], writes: ['Transform2D', 'PickupState', 'Lifetime'], numericProfile: f64, structuralCommands: ['despawnPrefab'] }),
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
export const kinetixInstalledSourceSignatures = Object.freeze({
|
|
52
|
+
'slot-input-resolution': Object.freeze([{ call: 'resolveInputs' }, { call: 'push', receiver: 'cmds' }]),
|
|
53
|
+
'vehicle-drive-body-build': Object.freeze([{ call: 'driveVehicle' }, { call: 'stepVehicle' }]),
|
|
54
|
+
'vehicle-physics-step': Object.freeze([{ call: 'stepPhysicsWorld' }, { call: 'stepDeterministicPhysicsWorld3D' }]),
|
|
55
|
+
'vehicle-lap-finish': Object.freeze([{ call: 'stepLap' }]),
|
|
56
|
+
'match-phase-transition': Object.freeze([{ call: 'stepPhase' }, { call: 'snapToGrid' }]),
|
|
57
|
+
'arena-input-mapping': Object.freeze([{ call: 'readInputFrame' }, { call: 'normalizeInputs' }]),
|
|
58
|
+
'arena-countdowns': Object.freeze([{ call: 'advanceCountdown' }, { call: 'advanceSparkMagnetSurge' }]),
|
|
59
|
+
'arena-axis-drive': Object.freeze([{ call: 'applyAxisDrive' }, { call: 'update', receiver: 'ship' }]),
|
|
60
|
+
'arena-autofire': Object.freeze([{ call: 'autoFire' }]),
|
|
61
|
+
'arena-linear-integration-bounds': Object.freeze([{ call: 'integrateBounds' }, { call: 'update', receiver: 'bullets' }]),
|
|
62
|
+
'arena-collision-pre-ai': Object.freeze([{ call: 'collisionPass', argument: 'pre-ai' }, { call: 'processBulletCollisions', occurrence: 0 }]),
|
|
63
|
+
'arena-bounded-enemy-steering': Object.freeze([{ call: 'steerEnemies' }, { call: 'nearestLivingShipPosition' }]),
|
|
64
|
+
'arena-collision-post-ai': Object.freeze([{ call: 'collisionPass', argument: 'post-ai' }, { call: 'processBulletCollisions', occurrence: 1 }]),
|
|
65
|
+
'arena-lifetimes': Object.freeze([{ call: 'advanceLifetimes' }, { call: 'collectDead' }]),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
export function installedSystemFamiliesForProfile(numericProfile) {
|
|
69
|
+
return kinetixInstalledSystemFamilies.filter((family) => family.numericProfiles.includes(numericProfile));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function createInstalledKinetixProfile(numericProfile, options = {}) {
|
|
73
|
+
const families = installedSystemFamiliesForProfile(numericProfile);
|
|
74
|
+
if (families.length === 0) throw new Error('KINETIX_NUMERIC_PROFILE_UNSUPPORTED');
|
|
75
|
+
const abi = numericProfile === fixed ? 'fixed-1000-3d.v1' : 'deterministic-f64-2d.v1';
|
|
76
|
+
const runtimeContract = kinetixRuntimeContractForProfile(numericProfile);
|
|
77
|
+
let fixedRuntime = null;
|
|
78
|
+
return {
|
|
79
|
+
abi,
|
|
80
|
+
runtimeContract,
|
|
81
|
+
componentSchemas: new Map([
|
|
82
|
+
['KinetixRuntimeState', { currentSchemaVersion: 1, domains: ['simulation'], validate: () => [] }],
|
|
83
|
+
['KinetixRenderRecord', { currentSchemaVersion: 1, domains: ['presentation'], validate: () => [] }],
|
|
84
|
+
['KinetixRenderBinding', { currentSchemaVersion: 1, domains: ['binding'], validate: () => [] }],
|
|
85
|
+
]),
|
|
86
|
+
systems: families.map((installedFamily) => ({
|
|
87
|
+
id: installedFamily.id,
|
|
88
|
+
version: 1,
|
|
89
|
+
phase: installedFamily.phase,
|
|
90
|
+
order: installedFamily.order,
|
|
91
|
+
query: { all: [], none: [] },
|
|
92
|
+
reads: [],
|
|
93
|
+
writes: [],
|
|
94
|
+
structuralCommands: [...installedFamily.structuralCommands],
|
|
95
|
+
})),
|
|
96
|
+
bakeEntity: () => ({ simulation: [], presentation: [], binding: [] }),
|
|
97
|
+
bakePrefab: () => ({ simulation: [], presentation: [], binding: [] }),
|
|
98
|
+
executeSystem: numericProfile === fixed
|
|
99
|
+
? ({ systemId, inputFrame, simulation, initialization }) => {
|
|
100
|
+
const properties = simulation.read('runtime-root', 'KinetixRuntimeState');
|
|
101
|
+
const pack = properties.installedProfileData;
|
|
102
|
+
if (fixedRuntime === null) {
|
|
103
|
+
const configuredPack = {
|
|
104
|
+
...pack,
|
|
105
|
+
slotCount: initialization.slotCount ?? pack.slotCount,
|
|
106
|
+
humanCount: initialization.humanCount ?? pack.humanCount,
|
|
107
|
+
initialPhase: initialization.initialPhase ?? pack.initialPhase,
|
|
108
|
+
};
|
|
109
|
+
fixedRuntime = createFixedRuntime(configuredPack, options.fixedPhysics);
|
|
110
|
+
} else if (properties.authoritativeState !== undefined
|
|
111
|
+
&& fixedRuntime.pendingInstalledTick === undefined) {
|
|
112
|
+
fixedRuntime.state = structuredClone(properties.authoritativeState);
|
|
113
|
+
}
|
|
114
|
+
executeFixedRuntimeSystem(fixedRuntime, systemId, inputFrame.slots);
|
|
115
|
+
if (fixedRuntime.pendingInstalledTick === undefined) {
|
|
116
|
+
simulation.write('runtime-root', 'KinetixRuntimeState', {
|
|
117
|
+
...properties,
|
|
118
|
+
authoritativeState: structuredClone(fixedRuntime.state),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
: undefined,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function kinetixRuntimeContractForProfile(numericProfile) {
|
|
127
|
+
if (numericProfile === fixed) return fixedRuntimeContract('fixed-1000-3d.v1');
|
|
128
|
+
if (numericProfile === f64) return f64RuntimeContract('deterministic-f64-2d.v1');
|
|
129
|
+
throw new Error('KINETIX_NUMERIC_PROFILE_UNSUPPORTED');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function fixedRuntimeContract(abi) {
|
|
133
|
+
return Object.freeze({
|
|
134
|
+
tickRate: 30,
|
|
135
|
+
targetProfile: fixed,
|
|
136
|
+
numericProfile: 'fixed-1000',
|
|
137
|
+
deterministicAbi: abi,
|
|
138
|
+
deterministicVersion: 'rundot.kinetix.fixed-1000-runtime.v1',
|
|
139
|
+
inputSchema: Object.freeze({
|
|
140
|
+
version: 'rundot.kinetix-input-schema.v1',
|
|
141
|
+
slots: 'bounded-array',
|
|
142
|
+
fields: Object.freeze([
|
|
143
|
+
Object.freeze({ name: 'steerX', type: 'fixed-1000' }),
|
|
144
|
+
Object.freeze({ name: 'throttle', type: 'fixed-1000' }),
|
|
145
|
+
Object.freeze({ name: 'handbrake', type: 'boolean' }),
|
|
146
|
+
]),
|
|
147
|
+
}),
|
|
148
|
+
stateSchema: Object.freeze({
|
|
149
|
+
version: 'rundot.kinetix-state-schema.v1',
|
|
150
|
+
checkpoint: 'opaque-canonical-bytes',
|
|
151
|
+
numericProfile: 'fixed-1000',
|
|
152
|
+
componentRoot: 'KinetixRuntimeState',
|
|
153
|
+
}),
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function f64RuntimeContract(abi) {
|
|
158
|
+
return Object.freeze({
|
|
159
|
+
tickRate: 30,
|
|
160
|
+
targetProfile: f64,
|
|
161
|
+
numericProfile: f64,
|
|
162
|
+
deterministicAbi: abi,
|
|
163
|
+
deterministicVersion: 'rundot.kinetix.deterministic-f64-runtime.v1',
|
|
164
|
+
inputSchema: Object.freeze({
|
|
165
|
+
version: 'rundot.kinetix-input-schema.v1',
|
|
166
|
+
slots: 'bounded-array',
|
|
167
|
+
fields: Object.freeze([
|
|
168
|
+
Object.freeze({ name: 'movementVector', type: 'f64x2' }),
|
|
169
|
+
Object.freeze({ name: 'aimVector', type: 'f64x2' }),
|
|
170
|
+
Object.freeze({ name: 'fireDown', type: 'boolean' }),
|
|
171
|
+
Object.freeze({ name: 'bombPressed', type: 'boolean' }),
|
|
172
|
+
]),
|
|
173
|
+
}),
|
|
174
|
+
stateSchema: Object.freeze({
|
|
175
|
+
version: 'rundot.kinetix-state-schema.v1',
|
|
176
|
+
checkpoint: 'opaque-canonical-bytes',
|
|
177
|
+
numericProfile: f64,
|
|
178
|
+
componentRoot: 'KinetixRuntimeState',
|
|
179
|
+
}),
|
|
180
|
+
});
|
|
181
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import {
|
|
2
|
+
bakeKinetixEnvelope,
|
|
3
|
+
scanKinetixPayloadTree,
|
|
4
|
+
stableKinetixProductBytes,
|
|
5
|
+
} from './kinetix-baker.mjs';
|
|
6
|
+
import { createInstalledKinetixProfile } from './kinetix-installed-systems.mjs';
|
|
7
|
+
import { compileKinetixProject, compileTrustedKinetixSource } from './kinetix-project-compiler.mjs';
|
|
8
|
+
|
|
9
|
+
function compileProducts(input, compile) {
|
|
10
|
+
const compiled = compile(input);
|
|
11
|
+
if (compiled.diagnostics.length > 0 || compiled.envelope === null) {
|
|
12
|
+
throw new Error(`KINETIX_PROJECT_INELIGIBLE ${JSON.stringify(compiled.diagnostics)}`);
|
|
13
|
+
}
|
|
14
|
+
const profileId = compiled.envelope.profiles[0].id;
|
|
15
|
+
const installedProfiles = new Map([[profileId, createInstalledKinetixProfile(input.targetProfile)]]);
|
|
16
|
+
const baked = bakeKinetixEnvelope(compiled.envelope, installedProfiles);
|
|
17
|
+
if (!baked.valid) throw new Error(`KINETIX_BAKE_FAILED ${JSON.stringify(baked.diagnostics)}`);
|
|
18
|
+
return { compiled, products: baked.products, runtimeIdentity: baked.runtimeIdentity };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function compileStableKinetixProducts(input) {
|
|
22
|
+
return compileStableProducts(input, compileKinetixProject);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function compileStableKinetixProductsFromTrustedSource(input) {
|
|
26
|
+
return compileStableProducts(input, compileTrustedKinetixSource);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function compileStableProducts(input, compile) {
|
|
30
|
+
const first = compileProducts(input, compile);
|
|
31
|
+
const second = compileProducts(input, compile);
|
|
32
|
+
if (!stableKinetixProductBytes(first.products).equals(stableKinetixProductBytes(second.products))) {
|
|
33
|
+
throw new Error('KINETIX_PRODUCT_NONDETERMINISTIC');
|
|
34
|
+
}
|
|
35
|
+
scanKinetixPayloadTree(first.products);
|
|
36
|
+
return first;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function nativeFixed1000ProductBytes(products, pack) {
|
|
40
|
+
const chunks = [Buffer.from('KNATIVE1')];
|
|
41
|
+
const appendU32 = (value) => {
|
|
42
|
+
const bytes = Buffer.alloc(4);
|
|
43
|
+
bytes.writeUInt32LE(value);
|
|
44
|
+
chunks.push(bytes);
|
|
45
|
+
};
|
|
46
|
+
const appendI64 = (value) => {
|
|
47
|
+
const bytes = Buffer.alloc(8);
|
|
48
|
+
bytes.writeBigInt64LE(BigInt(value));
|
|
49
|
+
chunks.push(bytes);
|
|
50
|
+
};
|
|
51
|
+
const appendString = (value) => {
|
|
52
|
+
const bytes = Buffer.from(value);
|
|
53
|
+
appendU32(bytes.length);
|
|
54
|
+
chunks.push(bytes);
|
|
55
|
+
};
|
|
56
|
+
appendString(products.sourceDigest);
|
|
57
|
+
const simulation = products.manifest.payloads.find((payload) => payload.name === 'simulation');
|
|
58
|
+
if (!simulation) throw new Error('KINETIX_SIMULATION_PAYLOAD_MISSING');
|
|
59
|
+
appendString(simulation.digest);
|
|
60
|
+
appendU32(products.manifest.systems.length);
|
|
61
|
+
for (const system of products.manifest.systems) {
|
|
62
|
+
appendString(system.id);
|
|
63
|
+
appendString(system.phase);
|
|
64
|
+
appendI64(system.phaseIndex);
|
|
65
|
+
appendI64(system.order);
|
|
66
|
+
}
|
|
67
|
+
const profile = Buffer.from(pack.installedProfileBinary, 'base64');
|
|
68
|
+
appendString(pack.installedProfileBinarySha256);
|
|
69
|
+
appendU32(profile.length);
|
|
70
|
+
chunks.push(profile);
|
|
71
|
+
return Buffer.concat(chunks);
|
|
72
|
+
}
|