@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,157 @@
|
|
|
1
|
+
// TypeScript-reference wrapper for the deterministic-f64 component runtime:
|
|
2
|
+
// canonical hashing + replay execution on top of the installed sim core.
|
|
3
|
+
// The canonical hash layout is documented beside the installed runtime and
|
|
4
|
+
// mirrored by its portable implementation.
|
|
5
|
+
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
7
|
+
import {
|
|
8
|
+
createF64Runtime,
|
|
9
|
+
detMath,
|
|
10
|
+
f64FromHex,
|
|
11
|
+
liveEntities,
|
|
12
|
+
mulberry32Next,
|
|
13
|
+
tickF64Runtime,
|
|
14
|
+
} from './kinetix-installed-f64-runtime.mjs';
|
|
15
|
+
|
|
16
|
+
export { createF64Runtime, detMath, liveEntities, mulberry32Next, tickF64Runtime };
|
|
17
|
+
|
|
18
|
+
function decodeF64(value) {
|
|
19
|
+
return f64FromHex(value);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function writeString(chunks, value) {
|
|
23
|
+
const bytes = Buffer.from(value, 'utf8');
|
|
24
|
+
const length = Buffer.alloc(4);
|
|
25
|
+
length.writeUInt32LE(bytes.length);
|
|
26
|
+
chunks.push(length, bytes);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function writeF64(chunks, value) {
|
|
30
|
+
const bytes = Buffer.alloc(8);
|
|
31
|
+
bytes.writeDoubleLE(value);
|
|
32
|
+
chunks.push(bytes);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function writeU64(chunks, value) {
|
|
36
|
+
const bytes = Buffer.alloc(8);
|
|
37
|
+
bytes.writeBigUInt64LE(BigInt(value));
|
|
38
|
+
chunks.push(bytes);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function writeI64(chunks, value) {
|
|
42
|
+
const bytes = Buffer.alloc(8);
|
|
43
|
+
bytes.writeBigInt64LE(BigInt(value));
|
|
44
|
+
chunks.push(bytes);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function canonicalF64TickHash(runtime) {
|
|
48
|
+
const chunks = [];
|
|
49
|
+
writeU64(chunks, BigInt(runtime.tick));
|
|
50
|
+
for (const streamId of runtime.rngStreamOrder) {
|
|
51
|
+
writeString(chunks, streamId);
|
|
52
|
+
const state = Buffer.alloc(4);
|
|
53
|
+
state.writeUInt32LE(runtime.rngStreams.get(streamId).state);
|
|
54
|
+
chunks.push(state);
|
|
55
|
+
}
|
|
56
|
+
const ordered = [
|
|
57
|
+
...runtime.staticEntities.filter((entity) => entity.alive),
|
|
58
|
+
...runtime.pooled.filter((entity) => entity.alive).sort((a, b) => (a.spawnId < b.spawnId ? -1 : 1)),
|
|
59
|
+
];
|
|
60
|
+
for (const entity of ordered) {
|
|
61
|
+
writeString(chunks, entity.ref);
|
|
62
|
+
writeU64(chunks, entity.spawnId);
|
|
63
|
+
for (const component of entity.components) {
|
|
64
|
+
writeString(chunks, component.type);
|
|
65
|
+
switch (component.type) {
|
|
66
|
+
case 'Transform2D':
|
|
67
|
+
writeF64(chunks, component.position.x);
|
|
68
|
+
writeF64(chunks, component.position.y);
|
|
69
|
+
writeF64(chunks, component.velocity.x);
|
|
70
|
+
writeF64(chunks, component.velocity.y);
|
|
71
|
+
writeF64(chunks, component.heading);
|
|
72
|
+
break;
|
|
73
|
+
case 'TwinStickInput':
|
|
74
|
+
writeF64(chunks, component.move.x);
|
|
75
|
+
writeF64(chunks, component.move.y);
|
|
76
|
+
writeF64(chunks, component.aim.x);
|
|
77
|
+
writeF64(chunks, component.aim.y);
|
|
78
|
+
chunks.push(Buffer.from([component.fire ? 1 : 0]));
|
|
79
|
+
break;
|
|
80
|
+
case 'SpawnShield':
|
|
81
|
+
writeF64(chunks, component.remaining);
|
|
82
|
+
break;
|
|
83
|
+
case 'Telegraph':
|
|
84
|
+
writeF64(chunks, component.remaining);
|
|
85
|
+
break;
|
|
86
|
+
case 'WanderMover':
|
|
87
|
+
writeF64(chunks, component.rerollTimer);
|
|
88
|
+
break;
|
|
89
|
+
case 'SeekMover':
|
|
90
|
+
writeF64(chunks, component.aliveSeconds);
|
|
91
|
+
break;
|
|
92
|
+
case 'AutoFireWeapon':
|
|
93
|
+
writeF64(chunks, component.cooldown);
|
|
94
|
+
writeU64(chunks, BigInt(component.nextBulletId));
|
|
95
|
+
break;
|
|
96
|
+
case 'Lifetime':
|
|
97
|
+
writeF64(chunks, component.age);
|
|
98
|
+
break;
|
|
99
|
+
case 'Health':
|
|
100
|
+
writeI64(chunks, BigInt(component.hp));
|
|
101
|
+
break;
|
|
102
|
+
case 'ResourceCounter':
|
|
103
|
+
writeI64(chunks, component.score);
|
|
104
|
+
writeI64(chunks, component.deaths);
|
|
105
|
+
break;
|
|
106
|
+
default:
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return createHash('sha256').update(Buffer.concat(chunks)).digest('hex');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function runF64Replay(pack, replay, options = {}) {
|
|
115
|
+
const hashEveryTick = options.hashEveryTick !== false;
|
|
116
|
+
const runtime = createF64Runtime(pack, options);
|
|
117
|
+
const inputsByTick = new Map();
|
|
118
|
+
for (const frame of replay.inputFrames ?? []) {
|
|
119
|
+
inputsByTick.set(frame.tick, frame.slots);
|
|
120
|
+
}
|
|
121
|
+
let currentInputs = Array.from({ length: pack.inputSlots }, () => ({ movement: { x: 0, y: 0 }, aim: { x: 0, y: 0 }, fire: false }));
|
|
122
|
+
|
|
123
|
+
const tickHashes = [];
|
|
124
|
+
const streamHash = createHash('sha256');
|
|
125
|
+
const startedAt = process.hrtime.bigint();
|
|
126
|
+
for (let tick = 0; tick < replay.tickCount; tick += 1) {
|
|
127
|
+
const frame = inputsByTick.get(tick);
|
|
128
|
+
if (frame) {
|
|
129
|
+
currentInputs = frame.map((slot) => ({
|
|
130
|
+
movement: { x: decodeF64(slot.movement[0]), y: decodeF64(slot.movement[1]) },
|
|
131
|
+
aim: { x: decodeF64(slot.aim[0]), y: decodeF64(slot.aim[1]) },
|
|
132
|
+
fire: slot.fire === true,
|
|
133
|
+
}));
|
|
134
|
+
}
|
|
135
|
+
tickF64Runtime(runtime, currentInputs);
|
|
136
|
+
if (hashEveryTick || tick === replay.tickCount - 1) {
|
|
137
|
+
const tickHash = canonicalF64TickHash(runtime);
|
|
138
|
+
if (hashEveryTick) tickHashes.push(tickHash);
|
|
139
|
+
else tickHashes[0] = tickHash;
|
|
140
|
+
streamHash.update(tickHash);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const elapsedMs = Number(process.hrtime.bigint() - startedAt) / 1e6;
|
|
144
|
+
return {
|
|
145
|
+
tickHashes,
|
|
146
|
+
finalDigest: streamHash.digest('hex'),
|
|
147
|
+
metrics: {
|
|
148
|
+
runtime: 'typescript-reference-f64',
|
|
149
|
+
tickCount: replay.tickCount,
|
|
150
|
+
elapsedMs,
|
|
151
|
+
ticksPerSecond: elapsedMs > 0 ? Math.round((replay.tickCount / elapsedMs) * 1000) : null,
|
|
152
|
+
liveEntitiesAtEnd: liveEntities(runtime).length,
|
|
153
|
+
finalScore: String(runtime.staticEntities.find((entity) => entity.byType.get('ResourceCounter'))?.byType.get('ResourceCounter').score ?? 0n),
|
|
154
|
+
shipDeaths: String(runtime.staticEntities.find((entity) => entity.byType.get('ResourceCounter'))?.byType.get('ResourceCounter').deaths ?? 0n),
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Node-side frame dump for the installed visual proof: runs a data-only
|
|
2
|
+
// replay, renders every 2nd tick (60Hz sim -> 30fps
|
|
3
|
+
// video) with the deterministic parity renderer, and writes raw RGB24 frames
|
|
4
|
+
// plus per-frame sha256 hashes. This is also the acceptance oracle for the
|
|
5
|
+
// C++ renderer.
|
|
6
|
+
//
|
|
7
|
+
// node spikes/component-runtime/render-frames-node.mjs <outDir> [packPath replayPath]
|
|
8
|
+
|
|
9
|
+
import { createHash } from 'node:crypto';
|
|
10
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
11
|
+
import { dirname, join, resolve } from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
import { createF64Runtime, tickF64Runtime, f64FromHex } from './kinetix-installed-f64-runtime.mjs';
|
|
14
|
+
import { renderF64Frame, FRAME_WIDTH, FRAME_HEIGHT } from './kinetix-installed-f64-renderer.mjs';
|
|
15
|
+
|
|
16
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const prototypeRoot = resolve(here, '../..');
|
|
18
|
+
const outDir = resolve(process.argv[2] ?? join(prototypeRoot, 'artifacts/component-runtime/parity-frames-node'));
|
|
19
|
+
if (process.argv[3] === undefined || process.argv[4] === undefined) throw new Error('KINETIX_F64_RENDER_INPUT_REQUIRED');
|
|
20
|
+
const packPath = resolve(process.argv[3]);
|
|
21
|
+
const replayPath = resolve(process.argv[4]);
|
|
22
|
+
|
|
23
|
+
const pack = JSON.parse(readFileSync(packPath, 'utf8'));
|
|
24
|
+
const replay = JSON.parse(readFileSync(replayPath, 'utf8'));
|
|
25
|
+
|
|
26
|
+
mkdirSync(outDir, { recursive: true });
|
|
27
|
+
const runtime = createF64Runtime(pack);
|
|
28
|
+
const inputsByTick = new Map((replay.inputFrames ?? []).map((frame) => [frame.tick, frame.slots]));
|
|
29
|
+
let currentInputs = Array.from({ length: pack.inputSlots }, () => ({ movement: { x: 0, y: 0 }, aim: { x: 0, y: 0 }, fire: false }));
|
|
30
|
+
|
|
31
|
+
const frame = new Uint8Array(FRAME_WIDTH * FRAME_HEIGHT * 3);
|
|
32
|
+
const hashes = [];
|
|
33
|
+
let frameIndex = 0;
|
|
34
|
+
for (let tick = 0; tick < replay.tickCount; tick += 1) {
|
|
35
|
+
const slots = inputsByTick.get(tick);
|
|
36
|
+
if (slots) {
|
|
37
|
+
currentInputs = slots.map((slot) => ({
|
|
38
|
+
movement: { x: f64FromHex(slot.movement[0]), y: f64FromHex(slot.movement[1]) },
|
|
39
|
+
aim: { x: f64FromHex(slot.aim[0]), y: f64FromHex(slot.aim[1]) },
|
|
40
|
+
fire: slot.fire === true,
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
tickF64Runtime(runtime, currentInputs);
|
|
44
|
+
if (tick % 2 === 1) {
|
|
45
|
+
renderF64Frame(runtime, frame);
|
|
46
|
+
const name = `frame-${String(frameIndex).padStart(3, '0')}.rgb`;
|
|
47
|
+
writeFileSync(join(outDir, name), frame);
|
|
48
|
+
hashes.push(createHash('sha256').update(frame).digest('hex'));
|
|
49
|
+
frameIndex += 1;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
writeFileSync(join(outDir, 'frame-hashes.json'), `${JSON.stringify({ width: FRAME_WIDTH, height: FRAME_HEIGHT, frames: frameIndex, hashes }, null, 2)}\n`);
|
|
53
|
+
console.log(JSON.stringify({ side: 'node-reference', frames: frameIndex, firstHash: hashes[0], lastHash: hashes[hashes.length - 1] }));
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// Deterministic parity renderer for the installed f64 runtime.
|
|
2
|
+
// Pure function of sim state -> 1280x720 RGB24 frame. Browser-safe.
|
|
3
|
+
//
|
|
4
|
+
// Bit-exactness contract (mirrored in the portable installed renderer):
|
|
5
|
+
// - only +,-,*,/ double arithmetic, comparisons, floor/ceil/abs and integer
|
|
6
|
+
// pixel loops; the ONLY transcendentals are detMath.cos/sin for the ship
|
|
7
|
+
// heading triangle (battery-proven identical in C++);
|
|
8
|
+
// - hard-edged shapes (no anti-aliasing, no alpha blending except constant
|
|
9
|
+
// 50% mix implemented as integer (a+b)>>1);
|
|
10
|
+
// - draw order: background grid -> arena border -> telegraph rings -> enemies
|
|
11
|
+
// (statics in pack order, pooled by ascending spawnId) -> bullets -> ship ->
|
|
12
|
+
// HUD score bar. Within a category the same entity order as hashing.
|
|
13
|
+
//
|
|
14
|
+
// The renderer never mutates runtime state.
|
|
15
|
+
|
|
16
|
+
import { detMath } from './kinetix-installed-f64-runtime.mjs';
|
|
17
|
+
|
|
18
|
+
export const FRAME_WIDTH = 1280;
|
|
19
|
+
export const FRAME_HEIGHT = 720;
|
|
20
|
+
|
|
21
|
+
const COLORS = Object.freeze({
|
|
22
|
+
background: [10, 10, 18],
|
|
23
|
+
grid: [20, 20, 40],
|
|
24
|
+
border: [42, 42, 85],
|
|
25
|
+
ship: [102, 255, 238],
|
|
26
|
+
shipShield: [220, 240, 255],
|
|
27
|
+
bulletCore: [255, 243, 199],
|
|
28
|
+
bulletGlow: [138, 122, 64],
|
|
29
|
+
wanderer: [176, 64, 255],
|
|
30
|
+
shard: [207, 122, 255],
|
|
31
|
+
grunt: [170, 255, 51],
|
|
32
|
+
telegraph: [90, 90, 140],
|
|
33
|
+
scoreBar: [255, 216, 77],
|
|
34
|
+
deathPip: [255, 80, 80],
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
function putPixel(frame, x, y, color) {
|
|
38
|
+
if (x < 0 || y < 0 || x >= FRAME_WIDTH || y >= FRAME_HEIGHT) return;
|
|
39
|
+
const offset = (y * FRAME_WIDTH + x) * 3;
|
|
40
|
+
frame[offset] = color[0];
|
|
41
|
+
frame[offset + 1] = color[1];
|
|
42
|
+
frame[offset + 2] = color[2];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function mixPixel(frame, x, y, color) {
|
|
46
|
+
if (x < 0 || y < 0 || x >= FRAME_WIDTH || y >= FRAME_HEIGHT) return;
|
|
47
|
+
const offset = (y * FRAME_WIDTH + x) * 3;
|
|
48
|
+
frame[offset] = (frame[offset] + color[0]) >> 1;
|
|
49
|
+
frame[offset + 1] = (frame[offset + 1] + color[1]) >> 1;
|
|
50
|
+
frame[offset + 2] = (frame[offset + 2] + color[2]) >> 1;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function fillCircle(frame, cx, cy, radius, color) {
|
|
54
|
+
const x0 = Math.max(0, Math.floor(cx - radius));
|
|
55
|
+
const x1 = Math.min(FRAME_WIDTH - 1, Math.ceil(cx + radius));
|
|
56
|
+
const y0 = Math.max(0, Math.floor(cy - radius));
|
|
57
|
+
const y1 = Math.min(FRAME_HEIGHT - 1, Math.ceil(cy + radius));
|
|
58
|
+
const r2 = radius * radius;
|
|
59
|
+
for (let y = y0; y <= y1; y += 1) {
|
|
60
|
+
for (let x = x0; x <= x1; x += 1) {
|
|
61
|
+
const dx = x + 0.5 - cx;
|
|
62
|
+
const dy = y + 0.5 - cy;
|
|
63
|
+
if (dx * dx + dy * dy <= r2) putPixel(frame, x, y, color);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function ring(frame, cx, cy, innerRadius, outerRadius, color) {
|
|
69
|
+
const x0 = Math.max(0, Math.floor(cx - outerRadius));
|
|
70
|
+
const x1 = Math.min(FRAME_WIDTH - 1, Math.ceil(cx + outerRadius));
|
|
71
|
+
const y0 = Math.max(0, Math.floor(cy - outerRadius));
|
|
72
|
+
const y1 = Math.min(FRAME_HEIGHT - 1, Math.ceil(cy + outerRadius));
|
|
73
|
+
const inner2 = innerRadius * innerRadius;
|
|
74
|
+
const outer2 = outerRadius * outerRadius;
|
|
75
|
+
for (let y = y0; y <= y1; y += 1) {
|
|
76
|
+
for (let x = x0; x <= x1; x += 1) {
|
|
77
|
+
const dx = x + 0.5 - cx;
|
|
78
|
+
const dy = y + 0.5 - cy;
|
|
79
|
+
const d2 = dx * dx + dy * dy;
|
|
80
|
+
if (d2 >= inner2 && d2 <= outer2) putPixel(frame, x, y, color);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function fillDiamond(frame, cx, cy, size, color) {
|
|
86
|
+
const x0 = Math.max(0, Math.floor(cx - size));
|
|
87
|
+
const x1 = Math.min(FRAME_WIDTH - 1, Math.ceil(cx + size));
|
|
88
|
+
const y0 = Math.max(0, Math.floor(cy - size));
|
|
89
|
+
const y1 = Math.min(FRAME_HEIGHT - 1, Math.ceil(cy + size));
|
|
90
|
+
for (let y = y0; y <= y1; y += 1) {
|
|
91
|
+
for (let x = x0; x <= x1; x += 1) {
|
|
92
|
+
const dx = x + 0.5 - cx;
|
|
93
|
+
const dy = y + 0.5 - cy;
|
|
94
|
+
const adx = dx < 0 ? -dx : dx;
|
|
95
|
+
const ady = dy < 0 ? -dy : dy;
|
|
96
|
+
if (adx + ady <= size) putPixel(frame, x, y, color);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function edge(ax, ay, bx, by, px, py) {
|
|
102
|
+
return (bx - ax) * (py - ay) - (by - ay) * (px - ax);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function fillTriangle(frame, ax, ay, bx, by, cx, cy, color) {
|
|
106
|
+
const minX = Math.max(0, Math.floor(Math.min(ax, bx, cx)));
|
|
107
|
+
const maxX = Math.min(FRAME_WIDTH - 1, Math.ceil(Math.max(ax, bx, cx)));
|
|
108
|
+
const minY = Math.max(0, Math.floor(Math.min(ay, by, cy)));
|
|
109
|
+
const maxY = Math.min(FRAME_HEIGHT - 1, Math.ceil(Math.max(ay, by, cy)));
|
|
110
|
+
const area = edge(ax, ay, bx, by, cx, cy);
|
|
111
|
+
if (area === 0) return;
|
|
112
|
+
for (let y = minY; y <= maxY; y += 1) {
|
|
113
|
+
for (let x = minX; x <= maxX; x += 1) {
|
|
114
|
+
const px = x + 0.5;
|
|
115
|
+
const py = y + 0.5;
|
|
116
|
+
const w0 = edge(ax, ay, bx, by, px, py);
|
|
117
|
+
const w1 = edge(bx, by, cx, cy, px, py);
|
|
118
|
+
const w2 = edge(cx, cy, ax, ay, px, py);
|
|
119
|
+
if (area > 0 ? w0 >= 0 && w1 >= 0 && w2 >= 0 : w0 <= 0 && w1 <= 0 && w2 <= 0) {
|
|
120
|
+
putPixel(frame, x, y, color);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function orderedEntities(runtime) {
|
|
127
|
+
return [
|
|
128
|
+
...runtime.staticEntities.filter((entity) => entity.alive),
|
|
129
|
+
...runtime.pooled.filter((entity) => entity.alive).sort((a, b) => (a.spawnId < b.spawnId ? -1 : 1)),
|
|
130
|
+
];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function renderF64Frame(runtime, frame) {
|
|
134
|
+
// background
|
|
135
|
+
for (let offset = 0; offset < frame.length; offset += 3) {
|
|
136
|
+
frame[offset] = COLORS.background[0];
|
|
137
|
+
frame[offset + 1] = COLORS.background[1];
|
|
138
|
+
frame[offset + 2] = COLORS.background[2];
|
|
139
|
+
}
|
|
140
|
+
// grid every 80 px
|
|
141
|
+
for (let x = 0; x < FRAME_WIDTH; x += 80) {
|
|
142
|
+
for (let y = 0; y < FRAME_HEIGHT; y += 1) putPixel(frame, x, y, COLORS.grid);
|
|
143
|
+
}
|
|
144
|
+
for (let y = 0; y < FRAME_HEIGHT; y += 80) {
|
|
145
|
+
for (let x = 0; x < FRAME_WIDTH; x += 1) putPixel(frame, x, y, COLORS.grid);
|
|
146
|
+
}
|
|
147
|
+
// arena border (2 px)
|
|
148
|
+
for (let x = 0; x < FRAME_WIDTH; x += 1) {
|
|
149
|
+
putPixel(frame, x, 0, COLORS.border);
|
|
150
|
+
putPixel(frame, x, 1, COLORS.border);
|
|
151
|
+
putPixel(frame, x, FRAME_HEIGHT - 2, COLORS.border);
|
|
152
|
+
putPixel(frame, x, FRAME_HEIGHT - 1, COLORS.border);
|
|
153
|
+
}
|
|
154
|
+
for (let y = 0; y < FRAME_HEIGHT; y += 1) {
|
|
155
|
+
putPixel(frame, 0, y, COLORS.border);
|
|
156
|
+
putPixel(frame, 1, y, COLORS.border);
|
|
157
|
+
putPixel(frame, FRAME_WIDTH - 2, y, COLORS.border);
|
|
158
|
+
putPixel(frame, FRAME_WIDTH - 1, y, COLORS.border);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const entities = orderedEntities(runtime);
|
|
162
|
+
|
|
163
|
+
// telegraph rings (behind bodies)
|
|
164
|
+
for (const entity of entities) {
|
|
165
|
+
const telegraph = entity.byType.get('Telegraph');
|
|
166
|
+
if (!telegraph || telegraph.remaining <= 0) continue;
|
|
167
|
+
const transform = entity.byType.get('Transform2D');
|
|
168
|
+
const circle = entity.byType.get('CollisionCircle');
|
|
169
|
+
const base = circle ? circle.radius : 8;
|
|
170
|
+
const radius = base * (1.5 + telegraph.remaining);
|
|
171
|
+
ring(frame, transform.position.x, transform.position.y, radius - 1.5, radius, COLORS.telegraph);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// enemies + bullets + ship
|
|
175
|
+
for (const entity of entities) {
|
|
176
|
+
const transform = entity.byType.get('Transform2D');
|
|
177
|
+
if (!transform) continue;
|
|
178
|
+
const telegraphing = (() => {
|
|
179
|
+
const telegraph = entity.byType.get('Telegraph');
|
|
180
|
+
return telegraph ? telegraph.remaining > 0 : false;
|
|
181
|
+
})();
|
|
182
|
+
|
|
183
|
+
if (entity.byType.get('WanderMover')) {
|
|
184
|
+
if (!telegraphing) {
|
|
185
|
+
fillCircle(frame, transform.position.x, transform.position.y, 12, COLORS.wanderer);
|
|
186
|
+
}
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (entity.byType.get('SeekMover')) {
|
|
190
|
+
if (!telegraphing) {
|
|
191
|
+
fillDiamond(frame, transform.position.x, transform.position.y, 11, COLORS.grunt);
|
|
192
|
+
}
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (entity.ref === 'shard') {
|
|
196
|
+
fillCircle(frame, transform.position.x, transform.position.y, 7.5, COLORS.shard);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (entity.ref === 'bullet') {
|
|
200
|
+
ring(frame, transform.position.x, transform.position.y, 4, 7, COLORS.bulletGlow);
|
|
201
|
+
fillCircle(frame, transform.position.x, transform.position.y, 4, COLORS.bulletCore);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (entity.byType.get('AxisDriveMover')) {
|
|
205
|
+
const heading = transform.heading;
|
|
206
|
+
const cos = detMath.cos(heading);
|
|
207
|
+
const sin = detMath.sin(heading);
|
|
208
|
+
const size = 14;
|
|
209
|
+
const noseX = transform.position.x + cos * size;
|
|
210
|
+
const noseY = transform.position.y + sin * size;
|
|
211
|
+
const leftX = transform.position.x + (-cos * 0.7 - sin * 0.6) * size;
|
|
212
|
+
const leftY = transform.position.y + (-sin * 0.7 + cos * 0.6) * size;
|
|
213
|
+
const rightX = transform.position.x + (-cos * 0.7 + sin * 0.6) * size;
|
|
214
|
+
const rightY = transform.position.y + (-sin * 0.7 - cos * 0.6) * size;
|
|
215
|
+
fillTriangle(frame, noseX, noseY, leftX, leftY, rightX, rightY, COLORS.ship);
|
|
216
|
+
const shield = entity.byType.get('SpawnShield');
|
|
217
|
+
if (shield && shield.remaining > 0) {
|
|
218
|
+
ring(frame, transform.position.x, transform.position.y, 16, 18, COLORS.shipShield);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// HUD: score bar + death pips (mix 50% over background for a HUD feel)
|
|
224
|
+
const game = runtime.staticEntities.find((entity) => entity.byType.get('ResourceCounter'));
|
|
225
|
+
if (game) {
|
|
226
|
+
const counter = game.byType.get('ResourceCounter');
|
|
227
|
+
const score = Number(counter.score);
|
|
228
|
+
const barWidth = Math.floor(Math.min(score, 2000) * 0.15);
|
|
229
|
+
for (let y = 12; y < 22; y += 1) {
|
|
230
|
+
for (let x = 16; x < 16 + barWidth; x += 1) mixPixel(frame, x, y, COLORS.scoreBar);
|
|
231
|
+
}
|
|
232
|
+
const deaths = Number(counter.deaths);
|
|
233
|
+
for (let pip = 0; pip < Math.min(deaths, 10); pip += 1) {
|
|
234
|
+
for (let y = 26; y < 34; y += 1) {
|
|
235
|
+
for (let x = 16 + pip * 12; x < 24 + pip * 12; x += 1) putPixel(frame, x, y, COLORS.deathPip);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return frame;
|
|
240
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { createF64Runtime, tickF64Runtime } from './kinetix-installed-f64-runtime.mjs';
|
|
2
|
+
import { createInstalledRuntimeAdapter } from './kinetix-runtime-adapter-utils.mjs';
|
|
3
|
+
import { decodeKinetixSessionConfig } from './kinetix-session-config.mjs';
|
|
4
|
+
|
|
5
|
+
function entityState(entity) {
|
|
6
|
+
return {
|
|
7
|
+
ref: entity.ref,
|
|
8
|
+
spawnId: entity.spawnId,
|
|
9
|
+
components: structuredClone(entity.components),
|
|
10
|
+
fields: [...entity.fields.entries()].map(([key, value]) => [key, structuredClone(value)]),
|
|
11
|
+
alive: entity.alive,
|
|
12
|
+
spawnPosition: structuredClone(entity.spawnPosition),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function restoreEntity(state) {
|
|
17
|
+
const components = structuredClone(state.components);
|
|
18
|
+
return {
|
|
19
|
+
ref: state.ref,
|
|
20
|
+
spawnId: state.spawnId,
|
|
21
|
+
components,
|
|
22
|
+
fields: new Map(state.fields.map(([key, value]) => [key, structuredClone(value)])),
|
|
23
|
+
alive: state.alive,
|
|
24
|
+
spawnPosition: structuredClone(state.spawnPosition),
|
|
25
|
+
byType: new Map(components.map((component) => [component.type, component])),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function createInstalledF64Runtime({ pack, identity, sessionConfigSchema, sessionConfigBytes, options = {} }) {
|
|
30
|
+
const session = decodeKinetixSessionConfig(sessionConfigSchema, sessionConfigBytes);
|
|
31
|
+
const configuredPack = structuredClone(pack);
|
|
32
|
+
configuredPack.inputSlots = session.playerCount;
|
|
33
|
+
configuredPack.rngStreams = configuredPack.rngStreams.map((stream, index) => ({
|
|
34
|
+
...stream,
|
|
35
|
+
seed: (session.seed + index) >>> 0,
|
|
36
|
+
}));
|
|
37
|
+
const engine = createF64Runtime(configuredPack, options);
|
|
38
|
+
|
|
39
|
+
const captureState = () => ({
|
|
40
|
+
tick: engine.tick,
|
|
41
|
+
rngStreams: [...engine.rngStreams.entries()].map(([key, value]) => [key, structuredClone(value)]),
|
|
42
|
+
staticEntities: engine.staticEntities.map(entityState),
|
|
43
|
+
pooled: engine.pooled.map(entityState),
|
|
44
|
+
nextSpawnId: engine.nextSpawnId,
|
|
45
|
+
poolCounts: [...engine.poolCounts.entries()],
|
|
46
|
+
spawnsThisTick: engine.spawnsThisTick,
|
|
47
|
+
frameEvents: structuredClone(engine.frameEvents),
|
|
48
|
+
});
|
|
49
|
+
const restoreState = (state) => {
|
|
50
|
+
engine.tick = state.tick;
|
|
51
|
+
engine.rngStreams = new Map(state.rngStreams.map(([key, value]) => [key, structuredClone(value)]));
|
|
52
|
+
engine.staticEntities = state.staticEntities.map(restoreEntity);
|
|
53
|
+
engine.pooled = state.pooled.map(restoreEntity);
|
|
54
|
+
engine.nextSpawnId = state.nextSpawnId;
|
|
55
|
+
engine.poolCounts = new Map(state.poolCounts);
|
|
56
|
+
engine.spawnsThisTick = state.spawnsThisTick;
|
|
57
|
+
engine.frameEvents = structuredClone(state.frameEvents);
|
|
58
|
+
};
|
|
59
|
+
return createInstalledRuntimeAdapter({
|
|
60
|
+
identity,
|
|
61
|
+
sessionConfigBytes,
|
|
62
|
+
captureState,
|
|
63
|
+
restoreState,
|
|
64
|
+
frameOf: (state) => state.tick,
|
|
65
|
+
step: (inputs) => tickF64Runtime(engine, inputs),
|
|
66
|
+
capturedStateIsDetached: true,
|
|
67
|
+
});
|
|
68
|
+
}
|