dotframe 0.1.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.
Files changed (77) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +21 -0
  3. package/README.md +57 -0
  4. package/assets/audio/hit.mp3 +0 -0
  5. package/assets/audio/loop.mp3 +0 -0
  6. package/assets/crafter.png +0 -0
  7. package/assets/fonts/archivo-black.json +133 -0
  8. package/assets/fonts/archivo-black.png +0 -0
  9. package/assets/fonts/bangers.json +133 -0
  10. package/assets/fonts/bangers.png +0 -0
  11. package/assets/suzanne.glb +0 -0
  12. package/cli/commands/config.ts +35 -0
  13. package/cli/commands/doctor.ts +71 -0
  14. package/cli/commands/new.ts +73 -0
  15. package/cli/commands/play.ts +244 -0
  16. package/cli/commands/ship.ts +94 -0
  17. package/cli/commands/skills.ts +66 -0
  18. package/cli/commands/snap.ts +88 -0
  19. package/cli/lib.ts +148 -0
  20. package/cli/main.ts +95 -0
  21. package/cli/simkit.ts +152 -0
  22. package/cli/tsconfig.json +8 -0
  23. package/native/df_audio.c +266 -0
  24. package/native/df_native.c +605 -0
  25. package/native/ffi.macos.json +318 -0
  26. package/native/ffi.windows.json +322 -0
  27. package/native/third_party/dr_mp3.h +5430 -0
  28. package/native/third_party/stb_image.h +7988 -0
  29. package/native/third_party/stb_image_write.h +1724 -0
  30. package/native/third_party/stb_truetype.h +5079 -0
  31. package/package.json +47 -0
  32. package/scripts/build-native.sh +33 -0
  33. package/scripts/build-web.sh +10 -0
  34. package/scripts/vendor.sh +36 -0
  35. package/skills/assets/SKILL.md +11 -0
  36. package/skills/core/SKILL.md +51 -0
  37. package/skills/core/references/errors.md +22 -0
  38. package/skills/discord/SKILL.md +12 -0
  39. package/skills/export-web/SKILL.md +20 -0
  40. package/skills/game-design/SKILL.md +27 -0
  41. package/skills/game-design/references/sim-walkthrough.md +32 -0
  42. package/skills/ios/SKILL.md +19 -0
  43. package/skills/macos/SKILL.md +16 -0
  44. package/skills/netplay/SKILL.md +34 -0
  45. package/skills/relay/SKILL.md +18 -0
  46. package/src/audio.ts +19 -0
  47. package/src/draw2d.ts +901 -0
  48. package/src/ecs.ts +30 -0
  49. package/src/gltf.ts +82 -0
  50. package/src/gpu.ts +86 -0
  51. package/src/input.ts +215 -0
  52. package/src/math.ts +113 -0
  53. package/src/native/backend.ts +142 -0
  54. package/src/native/ffi.ts +42 -0
  55. package/src/native/library.ts +65 -0
  56. package/src/native/run.ts +88 -0
  57. package/src/physics.ts +57 -0
  58. package/src/platform.ts +12 -0
  59. package/src/raster2d.ts +348 -0
  60. package/src/render.ts +132 -0
  61. package/src/shapes.ts +37 -0
  62. package/src/sim.ts +56 -0
  63. package/src/storage.ts +6 -0
  64. package/src/web/run.ts +338 -0
  65. package/templates/_base/AGENTS.md +7 -0
  66. package/templates/_base/dotframe.json +15 -0
  67. package/templates/_base/gitignore +5 -0
  68. package/templates/_base/index.html +14 -0
  69. package/templates/_base/main.web.ts +27 -0
  70. package/templates/_base/package.json +12 -0
  71. package/templates/_base/sim.ts +31 -0
  72. package/templates/blank/game.ts +64 -0
  73. package/templates/fighter/game.ts +131 -0
  74. package/templates/platformer/game.ts +120 -0
  75. package/tools/check-audio.ts +57 -0
  76. package/tools/gen-library-glue.ts +83 -0
  77. package/tools/make-sprite.ts +92 -0
package/src/ecs.ts ADDED
@@ -0,0 +1,30 @@
1
+ import type { Vec3 } from "./math";
2
+
3
+ // Minimal ECS: entities are numbers, each component kind lives in its own Map.
4
+
5
+ export interface Transform {
6
+ position: Vec3;
7
+ rotation: Vec3;
8
+ scale: Vec3;
9
+ }
10
+
11
+ export interface MeshRef {
12
+ mesh: number;
13
+ color: Vec3;
14
+ }
15
+
16
+ export interface World {
17
+ nextEntity: number;
18
+ transforms: Map<number, Transform>;
19
+ meshes: Map<number, MeshRef>;
20
+ }
21
+
22
+ export function createWorld(): World {
23
+ return { nextEntity: 0, transforms: new Map<number, Transform>(), meshes: new Map<number, MeshRef>() };
24
+ }
25
+
26
+ export function spawn(world: World): number {
27
+ const entity = world.nextEntity;
28
+ world.nextEntity = entity + 1;
29
+ return entity;
30
+ }
package/src/gltf.ts ADDED
@@ -0,0 +1,82 @@
1
+ // Reads the first mesh primitive of a binary glTF (.glb): positions, normals and indices.
2
+
3
+ export interface MeshData {
4
+ positions: Float32Array;
5
+ normals: Float32Array;
6
+ indices: Uint32Array;
7
+ }
8
+
9
+ interface GltfAccessor {
10
+ bufferView: number;
11
+ byteOffset?: number;
12
+ componentType: number;
13
+ count: number;
14
+ }
15
+
16
+ interface GltfBufferView {
17
+ byteOffset?: number;
18
+ byteLength: number;
19
+ }
20
+
21
+ interface GltfDocument {
22
+ accessors: GltfAccessor[];
23
+ bufferViews: GltfBufferView[];
24
+ meshes: { primitives: { attributes: { POSITION: number; NORMAL: number }; indices: number }[] }[];
25
+ }
26
+
27
+ const GLB_MAGIC = 0x46546c67;
28
+ const CHUNK_JSON = 0x4e4f534a;
29
+ const CHUNK_BIN = 0x004e4942;
30
+ const COMPONENT_UNSIGNED_SHORT = 5123;
31
+ const COMPONENT_UNSIGNED_INT = 5125;
32
+
33
+ export function parseGlb(bytes: Uint8Array): MeshData {
34
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
35
+ if (view.getUint32(0, true) !== GLB_MAGIC) throw new Error("not a GLB file");
36
+
37
+ let offset = 12;
38
+ let json = "";
39
+ let binOffset = -1;
40
+ while (offset < bytes.byteLength) {
41
+ const length = view.getUint32(offset, true);
42
+ const type = view.getUint32(offset + 4, true);
43
+ if (type === CHUNK_JSON) json = new TextDecoder().decode(bytes.subarray(offset + 8, offset + 8 + length));
44
+ if (type === CHUNK_BIN) binOffset = bytes.byteOffset + offset + 8;
45
+ offset += 8 + length;
46
+ }
47
+ if (json === "" || binOffset < 0) throw new Error("GLB is missing its JSON or BIN chunk");
48
+
49
+ const doc = JSON.parse(json) as GltfDocument;
50
+ const primitive = doc.meshes[0].primitives[0];
51
+
52
+ const accessorStart = (index: number): number => {
53
+ const accessor = doc.accessors[index];
54
+ const bufferView = doc.bufferViews[accessor.bufferView];
55
+ return binOffset + (bufferView.byteOffset ?? 0) + (accessor.byteOffset ?? 0);
56
+ };
57
+
58
+ const readVec3 = (index: number): Float32Array => {
59
+ const count = doc.accessors[index].count;
60
+ const start = accessorStart(index);
61
+ const out = new Float32Array(count * 3);
62
+ const source = new DataView(bytes.buffer, start, count * 12);
63
+ for (let i = 0; i < count * 3; i++) out[i] = source.getFloat32(i * 4, true);
64
+ return out;
65
+ };
66
+
67
+ const indexAccessor = doc.accessors[primitive.indices];
68
+ const indexStart = accessorStart(primitive.indices);
69
+ const indices = new Uint32Array(indexAccessor.count);
70
+ const indexView = new DataView(bytes.buffer, indexStart);
71
+ for (let i = 0; i < indexAccessor.count; i++) {
72
+ if (indexAccessor.componentType === COMPONENT_UNSIGNED_SHORT) indices[i] = indexView.getUint16(i * 2, true);
73
+ else if (indexAccessor.componentType === COMPONENT_UNSIGNED_INT) indices[i] = indexView.getUint32(i * 4, true);
74
+ else throw new Error(`unsupported index component type ${indexAccessor.componentType}`);
75
+ }
76
+
77
+ return {
78
+ positions: readVec3(primitive.attributes.POSITION),
79
+ normals: readVec3(primitive.attributes.NORMAL),
80
+ indices,
81
+ };
82
+ }
package/src/gpu.ts ADDED
@@ -0,0 +1,86 @@
1
+ import type { Platform } from "./platform";
2
+
3
+ // Backend-agnostic surface shared by game code. Backends return plain objects of functions:
4
+ // scriptc compiles structural types as record copies, so classes cannot stand in for interfaces.
5
+
6
+ export interface WindowOptions {
7
+ width: number;
8
+ height: number;
9
+ title: string;
10
+ }
11
+
12
+ export interface Color {
13
+ r: number;
14
+ g: number;
15
+ b: number;
16
+ }
17
+
18
+ export const BufferUsage = { Vertex: 1, Index: 2, Uniform: 4 };
19
+ // Unorm8x4: four bytes read as a vec4f in [0, 1], for packed colors.
20
+ export const VertexFormat = { Float32x2: 0, Float32x3: 1, Float32x4: 2, Float32: 3, Unorm8x4: 4 };
21
+
22
+ export interface VertexAttribute {
23
+ format: number;
24
+ offset: number;
25
+ location: number;
26
+ }
27
+
28
+ export interface PipelineOptions {
29
+ wgsl: string;
30
+ // Bytes per vertex; 0 when the shader generates vertices itself.
31
+ stride: number;
32
+ attributes: VertexAttribute[];
33
+ depth: boolean;
34
+ // Standard alpha blending, for 2D and transparent sprites.
35
+ blend: boolean;
36
+ }
37
+
38
+ export interface Texture {
39
+ id: number;
40
+ width: number;
41
+ height: number;
42
+ }
43
+
44
+ // Negative handles mean "none". first/count are uint32 indices with an index buffer, vertices otherwise.
45
+ export interface Draw {
46
+ pipeline: number;
47
+ bindGroup: number;
48
+ vertexBuffer: number;
49
+ indexBuffer: number;
50
+ first: number;
51
+ count: number;
52
+ }
53
+
54
+ // Synchronous rendering surface. Code that must also run in scriptc library mode (iOS), where promises are
55
+ // unavailable, depends on this instead of Gpu.
56
+ export interface RenderGpu {
57
+ createBuffer: (usage: number, data: Uint8Array) => number;
58
+ writeBuffer: (buffer: number, data: Uint8Array) => void;
59
+ destroyBuffer: (buffer: number) => void;
60
+ createPipeline: (options: PipelineOptions) => number;
61
+ // Group 0: binding 0 uniform buffer, binding 1 texture, binding 2 nearest sampler; -1 skips one.
62
+ bind: (pipeline: number, buffer: number, texture: number) => number;
63
+ // smooth: linear filtering (fonts, photos); otherwise nearest (pixel art).
64
+ createTexture: (width: number, height: number, rgba: Uint8Array, smooth: boolean) => Texture;
65
+ frame: (clear: Color, draws: Draw[]) => void;
66
+ aspect: () => number;
67
+ }
68
+
69
+ export interface Gpu extends RenderGpu {
70
+ // Decodes PNG bytes.
71
+ createImage: (png: Uint8Array, smooth: boolean) => Promise<Texture>;
72
+ }
73
+
74
+ // Called every frame with elapsed seconds; return false to stop.
75
+ export type Frame = (time: number) => boolean;
76
+
77
+ // Called once after the backend is ready.
78
+ export type Setup = (platform: Platform) => Frame;
79
+
80
+ export function f32Bytes(data: Float32Array): Uint8Array {
81
+ return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
82
+ }
83
+
84
+ export function u32Bytes(data: Uint32Array): Uint8Array {
85
+ return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
86
+ }
package/src/input.ts ADDED
@@ -0,0 +1,215 @@
1
+ // Engine-level key ids. Each maps to a KeyboardEvent.code (web) and an SDL scancode (native).
2
+ // Codes are physical positions: Semicolon is the key right of L, which reads Ñ on a Spanish layout.
3
+
4
+ export const Key = {
5
+ Left: 0,
6
+ Right: 1,
7
+ Up: 2,
8
+ Down: 3,
9
+ Space: 4,
10
+ W: 5,
11
+ A: 6,
12
+ S: 7,
13
+ D: 8,
14
+ J: 9,
15
+ K: 10,
16
+ L: 11,
17
+ Escape: 12,
18
+ Enter: 13,
19
+ B: 14,
20
+ C: 15,
21
+ E: 16,
22
+ F: 17,
23
+ G: 18,
24
+ H: 19,
25
+ I: 20,
26
+ M: 21,
27
+ N: 22,
28
+ O: 23,
29
+ P: 24,
30
+ Q: 25,
31
+ R: 26,
32
+ T: 27,
33
+ U: 28,
34
+ V: 29,
35
+ X: 30,
36
+ Y: 31,
37
+ Z: 32,
38
+ Digit0: 33,
39
+ Digit1: 34,
40
+ Digit2: 35,
41
+ Digit3: 36,
42
+ Digit4: 37,
43
+ Digit5: 38,
44
+ Digit6: 39,
45
+ Digit7: 40,
46
+ Digit8: 41,
47
+ Digit9: 42,
48
+ Tab: 43,
49
+ Backspace: 44,
50
+ Minus: 45,
51
+ Equal: 46,
52
+ BracketLeft: 47,
53
+ BracketRight: 48,
54
+ Backslash: 49,
55
+ Semicolon: 50,
56
+ Quote: 51,
57
+ Backquote: 52,
58
+ Comma: 53,
59
+ Period: 54,
60
+ Slash: 55,
61
+ ShiftLeft: 56,
62
+ ShiftRight: 57,
63
+ ControlLeft: 58,
64
+ ControlRight: 59,
65
+ AltLeft: 60,
66
+ AltRight: 61,
67
+ F1: 62,
68
+ F2: 63,
69
+ F3: 64,
70
+ F4: 65,
71
+ F5: 66,
72
+ F6: 67,
73
+ F7: 68,
74
+ F8: 69,
75
+ F9: 70,
76
+ F10: 71,
77
+ F11: 72,
78
+ F12: 73,
79
+ };
80
+
81
+ // KeyboardEvent.code for each Key id.
82
+ export const keyCodes: string[] = [
83
+ "ArrowLeft",
84
+ "ArrowRight",
85
+ "ArrowUp",
86
+ "ArrowDown",
87
+ "Space",
88
+ "KeyW",
89
+ "KeyA",
90
+ "KeyS",
91
+ "KeyD",
92
+ "KeyJ",
93
+ "KeyK",
94
+ "KeyL",
95
+ "Escape",
96
+ "Enter",
97
+ "KeyB",
98
+ "KeyC",
99
+ "KeyE",
100
+ "KeyF",
101
+ "KeyG",
102
+ "KeyH",
103
+ "KeyI",
104
+ "KeyM",
105
+ "KeyN",
106
+ "KeyO",
107
+ "KeyP",
108
+ "KeyQ",
109
+ "KeyR",
110
+ "KeyT",
111
+ "KeyU",
112
+ "KeyV",
113
+ "KeyX",
114
+ "KeyY",
115
+ "KeyZ",
116
+ "Digit0",
117
+ "Digit1",
118
+ "Digit2",
119
+ "Digit3",
120
+ "Digit4",
121
+ "Digit5",
122
+ "Digit6",
123
+ "Digit7",
124
+ "Digit8",
125
+ "Digit9",
126
+ "Tab",
127
+ "Backspace",
128
+ "Minus",
129
+ "Equal",
130
+ "BracketLeft",
131
+ "BracketRight",
132
+ "Backslash",
133
+ "Semicolon",
134
+ "Quote",
135
+ "Backquote",
136
+ "Comma",
137
+ "Period",
138
+ "Slash",
139
+ "ShiftLeft",
140
+ "ShiftRight",
141
+ "ControlLeft",
142
+ "ControlRight",
143
+ "AltLeft",
144
+ "AltRight",
145
+ "F1",
146
+ "F2",
147
+ "F3",
148
+ "F4",
149
+ "F5",
150
+ "F6",
151
+ "F7",
152
+ "F8",
153
+ "F9",
154
+ "F10",
155
+ "F11",
156
+ "F12",
157
+ ];
158
+
159
+ // SDL scancode for each Key id.
160
+ export const keyScancodes: number[] = [
161
+ 80, 79, 82, 81, 44, 26, 4, 22, 7, 13, 14, 15, 41, 40, 5, 6, 8, 9, 10, 11, 12, 16, 17, 18, 19, 20, 21, 23, 24, 25, 27, 28, 29, 39, 30, 31, 32, 33, 34, 35, 36, 37, 38, 43, 42, 45, 46, 47, 48, 49, 51, 52, 53, 54, 55, 56, 225, 229, 224, 228, 226, 230, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69,
162
+ ];
163
+
164
+ export const GamepadAxis = { LeftX: 0, LeftY: 1, RightX: 2, RightY: 3 };
165
+ // Button ids follow the W3C "standard" gamepad layout on every backend.
166
+ export const GamepadButton = {
167
+ South: 0,
168
+ East: 1,
169
+ West: 2,
170
+ North: 3,
171
+ LeftShoulder: 4,
172
+ RightShoulder: 5,
173
+ LeftTrigger: 6,
174
+ RightTrigger: 7,
175
+ Back: 8,
176
+ Start: 9,
177
+ LeftStick: 10,
178
+ RightStick: 11,
179
+ DpadUp: 12,
180
+ DpadDown: 13,
181
+ DpadLeft: 14,
182
+ DpadRight: 15,
183
+ Guide: 16,
184
+ };
185
+ // SDL_GamepadButton for each standard id; -1 marks the triggers, which SDL reports as axes 4 and 5.
186
+ export const sdlGamepadButtons: number[] = [0, 1, 2, 3, 9, 10, -1, -1, 4, 6, 7, 8, 11, 12, 13, 14, 5];
187
+ export const MouseButton = { Left: 1, Middle: 2, Right: 4 };
188
+
189
+ export interface Pointer {
190
+ // Normalized to the window content: [0, 1] inside, past those bounds outside it.
191
+ x: number;
192
+ y: number;
193
+ // Bitmask of MouseButton values.
194
+ buttons: number;
195
+ }
196
+
197
+ export interface Touch {
198
+ // Stable while the finger stays down.
199
+ id: number;
200
+ // Normalized to the window content, [0, 1] on each axis.
201
+ x: number;
202
+ y: number;
203
+ }
204
+
205
+ export interface Input {
206
+ down: (key: number) => boolean;
207
+ // First held key, or -1; used to rebind controls.
208
+ firstDown: () => number;
209
+ // Gamepad slot pad (0 is player 1); 0 or false when the slot is empty.
210
+ axis: (pad: number, axis: number) => number;
211
+ button: (pad: number, button: number) => boolean;
212
+ pointer: () => Pointer;
213
+ // Fingers currently down, in no particular order.
214
+ touches: () => Touch[];
215
+ }
package/src/math.ts ADDED
@@ -0,0 +1,113 @@
1
+ // Column-major 4x4 matrices in Float32Array, WebGPU clip space (z in [0, 1]).
2
+
3
+ export interface Vec3 {
4
+ x: number;
5
+ y: number;
6
+ z: number;
7
+ }
8
+
9
+ export function vec3(x: number, y: number, z: number): Vec3 {
10
+ return { x, y, z };
11
+ }
12
+
13
+ export function identity(): Float32Array {
14
+ const m = new Float32Array(16);
15
+ m[0] = 1;
16
+ m[5] = 1;
17
+ m[10] = 1;
18
+ m[15] = 1;
19
+ return m;
20
+ }
21
+
22
+ export function multiply(a: Float32Array, b: Float32Array): Float32Array {
23
+ const out = new Float32Array(16);
24
+ for (let col = 0; col < 4; col++) {
25
+ for (let row = 0; row < 4; row++) {
26
+ let sum = 0;
27
+ for (let k = 0; k < 4; k++) sum += a[k * 4 + row] * b[col * 4 + k];
28
+ out[col * 4 + row] = sum;
29
+ }
30
+ }
31
+ return out;
32
+ }
33
+
34
+ export function perspective(fovY: number, aspect: number, near: number, far: number): Float32Array {
35
+ const f = 1 / Math.tan(fovY / 2);
36
+ const m = new Float32Array(16);
37
+ m[0] = f / aspect;
38
+ m[5] = f;
39
+ m[10] = far / (near - far);
40
+ m[11] = -1;
41
+ m[14] = (far * near) / (near - far);
42
+ return m;
43
+ }
44
+
45
+ export function lookAt(eye: Vec3, target: Vec3, up: Vec3): Float32Array {
46
+ let zx = eye.x - target.x;
47
+ let zy = eye.y - target.y;
48
+ let zz = eye.z - target.z;
49
+ const zl = Math.hypot(zx, zy, zz);
50
+ zx /= zl;
51
+ zy /= zl;
52
+ zz /= zl;
53
+ let xx = up.y * zz - up.z * zy;
54
+ let xy = up.z * zx - up.x * zz;
55
+ let xz = up.x * zy - up.y * zx;
56
+ const xl = Math.hypot(xx, xy, xz);
57
+ xx /= xl;
58
+ xy /= xl;
59
+ xz /= xl;
60
+ const yx = zy * xz - zz * xy;
61
+ const yy = zz * xx - zx * xz;
62
+ const yz = zx * xy - zy * xx;
63
+ const m = new Float32Array(16);
64
+ m[0] = xx;
65
+ m[1] = yx;
66
+ m[2] = zx;
67
+ m[4] = xy;
68
+ m[5] = yy;
69
+ m[6] = zy;
70
+ m[8] = xz;
71
+ m[9] = yz;
72
+ m[10] = zz;
73
+ m[12] = -(xx * eye.x + xy * eye.y + xz * eye.z);
74
+ m[13] = -(yx * eye.x + yy * eye.y + yz * eye.z);
75
+ m[14] = -(zx * eye.x + zy * eye.y + zz * eye.z);
76
+ m[15] = 1;
77
+ return m;
78
+ }
79
+
80
+ // Translation * rotation (Y, then X, then Z, in radians) * scale.
81
+ export function compose(position: Vec3, rotation: Vec3, scale: Vec3): Float32Array {
82
+ const cx = Math.cos(rotation.x);
83
+ const sx = Math.sin(rotation.x);
84
+ const cy = Math.cos(rotation.y);
85
+ const sy = Math.sin(rotation.y);
86
+ const cz = Math.cos(rotation.z);
87
+ const sz = Math.sin(rotation.z);
88
+ // R = Ry * Rx * Rz
89
+ const r00 = cy * cz + sy * sx * sz;
90
+ const r01 = -cy * sz + sy * sx * cz;
91
+ const r02 = sy * cx;
92
+ const r10 = cx * sz;
93
+ const r11 = cx * cz;
94
+ const r12 = -sx;
95
+ const r20 = -sy * cz + cy * sx * sz;
96
+ const r21 = sy * sz + cy * sx * cz;
97
+ const r22 = cy * cx;
98
+ const m = new Float32Array(16);
99
+ m[0] = r00 * scale.x;
100
+ m[1] = r10 * scale.x;
101
+ m[2] = r20 * scale.x;
102
+ m[4] = r01 * scale.y;
103
+ m[5] = r11 * scale.y;
104
+ m[6] = r21 * scale.y;
105
+ m[8] = r02 * scale.z;
106
+ m[9] = r12 * scale.z;
107
+ m[10] = r22 * scale.z;
108
+ m[12] = position.x;
109
+ m[13] = position.y;
110
+ m[14] = position.z;
111
+ m[15] = 1;
112
+ return m;
113
+ }
@@ -0,0 +1,142 @@
1
+ // Native platform pieces shared by the executable loop (run.ts) and the library-mode runtime (library.ts).
2
+ // Everything here is synchronous so it also compiles in scriptc library mode, where promises are unavailable.
3
+ import type { AudioPlayer } from "../audio";
4
+ import type { Color, Draw, PipelineOptions, RenderGpu, Texture } from "../gpu";
5
+ import { type Input, keyScancodes, type Pointer, sdlGamepadButtons, type Touch } from "../input";
6
+ import {
7
+ dfAudioOpen,
8
+ dfBegin,
9
+ dfBind,
10
+ dfBuffer,
11
+ dfBufferDestroy,
12
+ dfBufferWrite,
13
+ dfDraw,
14
+ dfEnd,
15
+ dfGamepadAxis,
16
+ dfGamepadButton,
17
+ dfHeight,
18
+ dfImage,
19
+ dfKeyDown,
20
+ dfMasterVolume,
21
+ dfMouse,
22
+ dfMusicPause,
23
+ dfMusicPlay,
24
+ dfMusicStop,
25
+ dfMusicVolume,
26
+ dfPipeline,
27
+ dfPlay,
28
+ dfSound,
29
+ dfTexture,
30
+ dfTextureSize,
31
+ dfTone,
32
+ dfTouch,
33
+ dfTouchCount,
34
+ dfTrack,
35
+ dfWidth,
36
+ } from "./ffi";
37
+
38
+ const PIPELINE_DEPTH = 1;
39
+ const PIPELINE_BLEND = 4;
40
+
41
+ export function createNativeRenderGpu(): RenderGpu {
42
+ const depthPipelines = new Set<number>();
43
+ return {
44
+ createBuffer: (usage: number, data: Uint8Array): number => {
45
+ const buffer = dfBuffer(usage, data);
46
+ if (buffer < 0) throw new Error(`dfBuffer failed: ${buffer}`);
47
+ return buffer;
48
+ },
49
+ writeBuffer: (buffer: number, data: Uint8Array): void => dfBufferWrite(buffer, data),
50
+ destroyBuffer: (buffer: number): void => dfBufferDestroy(buffer),
51
+ createPipeline: (pipelineOptions: PipelineOptions): number => {
52
+ const attributes = new Uint32Array(pipelineOptions.attributes.length * 3);
53
+ for (let i = 0; i < pipelineOptions.attributes.length; i++) {
54
+ const attribute = pipelineOptions.attributes[i];
55
+ attributes[i * 3] = attribute.format;
56
+ attributes[i * 3 + 1] = attribute.offset;
57
+ attributes[i * 3 + 2] = attribute.location;
58
+ }
59
+ const flags = (pipelineOptions.depth ? PIPELINE_DEPTH : 0) | (pipelineOptions.blend ? PIPELINE_BLEND : 0);
60
+ const attributeBytes = new Uint8Array(attributes.buffer, attributes.byteOffset, attributes.byteLength);
61
+ const pipeline = dfPipeline(pipelineOptions.wgsl, pipelineOptions.stride, attributeBytes, flags);
62
+ if (pipeline < 0) throw new Error(`dfPipeline failed: ${pipeline}`);
63
+ if (pipelineOptions.depth) depthPipelines.add(pipeline);
64
+ return pipeline;
65
+ },
66
+ bind: (pipeline: number, buffer: number, texture: number): number => {
67
+ const group = dfBind(pipeline, buffer, texture);
68
+ if (group < 0) throw new Error(`dfBind failed: ${group}`);
69
+ return group;
70
+ },
71
+ createTexture: (width: number, height: number, rgba: Uint8Array, smooth: boolean): Texture => {
72
+ const id = dfTexture(width, height, rgba, smooth);
73
+ if (id < 0) throw new Error(`dfTexture failed: ${id}`);
74
+ return { id, width, height };
75
+ },
76
+ frame: (clear: Color, draws: Draw[]): void => {
77
+ // Pipelines without depth cannot run in a pass with a depth attachment.
78
+ let usesDepth = false;
79
+ for (const draw of draws) if (depthPipelines.has(draw.pipeline)) usesDepth = true;
80
+ if (dfBegin(clear.r, clear.g, clear.b, usesDepth) !== 0) return;
81
+ for (const draw of draws) {
82
+ dfDraw(draw.pipeline, draw.bindGroup, draw.vertexBuffer, draw.indexBuffer, draw.first, draw.count);
83
+ }
84
+ dfEnd();
85
+ },
86
+ aspect: (): number => dfWidth() / Math.max(dfHeight(), 1),
87
+ };
88
+ }
89
+
90
+ // Decodes PNG bytes into a texture immediately.
91
+ export function createNativeImage(png: Uint8Array, smooth: boolean): Texture {
92
+ const id = dfImage(png, smooth);
93
+ if (id < 0) throw new Error(`dfImage failed: ${id}`);
94
+ return { id, width: dfTextureSize(id, 0), height: dfTextureSize(id, 1) };
95
+ }
96
+
97
+ export function createNativeInput(): Input {
98
+ return {
99
+ down: (key: number): boolean => key >= 0 && key < keyScancodes.length && dfKeyDown(keyScancodes[key]),
100
+ firstDown: (): number => {
101
+ for (let key = 0; key < keyScancodes.length; key++) if (dfKeyDown(keyScancodes[key])) return key;
102
+ return -1;
103
+ },
104
+ axis: (pad: number, axis: number): number => dfGamepadAxis(pad, axis),
105
+ button: (pad: number, button: number): boolean => {
106
+ if (button === 6 || button === 7) return dfGamepadAxis(pad, button === 6 ? 4 : 5) > 0.5;
107
+ const sdl = button >= 0 && button < sdlGamepadButtons.length ? sdlGamepadButtons[button] : -1;
108
+ return sdl >= 0 && dfGamepadButton(pad, sdl);
109
+ },
110
+ pointer: (): Pointer => ({ x: dfMouse(0), y: dfMouse(1), buttons: dfMouse(2) }),
111
+ touches: (): Touch[] => {
112
+ const out: Touch[] = [];
113
+ const count = dfTouchCount();
114
+ for (let i = 0; i < count; i++) out.push({ id: dfTouch(i, 0), x: dfTouch(i, 1), y: dfTouch(i, 2) });
115
+ return out;
116
+ },
117
+ };
118
+ }
119
+
120
+ // Opens the audio device; returns false without one, so games continue silently.
121
+ export function openNativeAudio(): boolean {
122
+ const ready = dfAudioOpen() === 0;
123
+ if (!ready) console.error("dotframe: no audio device, continuing without sound");
124
+ return ready;
125
+ }
126
+
127
+ export function createNativeAudioPlayer(): AudioPlayer {
128
+ return {
129
+ play: (sound: number, volume: number, rate: number): void => dfPlay(sound, volume, rate),
130
+ tone: (frequency: number, duration: number, volume: number): void => dfTone(frequency, duration, volume),
131
+ playMusic: (track: number, loop: boolean, volume: number): void => {
132
+ dfMusicPlay(track, loop, volume);
133
+ },
134
+ stopMusic: (): void => dfMusicStop(),
135
+ pauseMusic: (paused: boolean): void => dfMusicPause(paused),
136
+ setMusicVolume: (volume: number): void => dfMusicVolume(volume),
137
+ setMasterVolume: (volume: number): void => dfMasterVolume(volume),
138
+ };
139
+ }
140
+
141
+ export const decodeNativeSound = (mp3: Uint8Array): number => dfSound(mp3);
142
+ export const storeNativeTrack = (mp3: Uint8Array): number => dfTrack(mp3);