dotframe 0.1.5 → 0.1.6
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/CHANGELOG.md +8 -0
- package/native/df_native.c +2 -0
- package/package.json +1 -1
- package/skill-data/macos/SKILL.md +27 -0
- package/skill-data/netplay/SKILL.md +2 -0
- package/templates/_base/src/setup.ts +27 -5
- package/templates/blank/game.ts +2 -0
- package/templates/fighter/game.ts +2 -0
- package/templates/platformer/game.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.6
|
|
4
|
+
|
|
5
|
+
From the Craft Ones native port.
|
|
6
|
+
|
|
7
|
+
- Native: mouse buttons read 0 while the window lacks input focus, so clicks in other apps no longer reach the game. The pointer position stays global, so aiming is unchanged.
|
|
8
|
+
- Templates: `src/setup.ts` reads the pointer and touches with a hold-to-charge, release-to-act pattern (a charge meter while holding, a tone on release), on web and native. Each `game.ts` declares the `POINTER_BIT` a release sets.
|
|
9
|
+
- Skills: the macos guide gains "Porting existing TypeScript" (what scriptc rejects and the fix for each, plus runtime traps such as structural coercion copying and out-of-range reads) and "Debugging a native crash" with lldb. Netplay warns against checksums built on `JSON.stringify`.
|
|
10
|
+
|
|
3
11
|
## 0.1.5
|
|
4
12
|
|
|
5
13
|
- Native builds follow the entry's import graph instead of copying the game folder: relative imports that leave the game root and workspace packages that resolve to source are staged with their relative layout and rewritten to relative paths, so ports inside monorepos compile.
|
package/native/df_native.c
CHANGED
|
@@ -227,6 +227,8 @@ static void mouse_relative(double *nx, double *ny) {
|
|
|
227
227
|
// Field 0 x, 1 y (normalized), 2 buttons (bit 0 left, bit 1 middle, bit 2 right).
|
|
228
228
|
double df_mouse(int32_t field) {
|
|
229
229
|
if (field == 2) {
|
|
230
|
+
// Clicks in other apps must not reach the game: buttons count only while the window has input focus.
|
|
231
|
+
if (!(SDL_GetWindowFlags(g_window) & SDL_WINDOW_INPUT_FOCUS)) return 0;
|
|
230
232
|
SDL_MouseButtonFlags flags = SDL_GetGlobalMouseState(NULL, NULL);
|
|
231
233
|
return (flags & SDL_BUTTON_LMASK ? 1 : 0) | (flags & SDL_BUTTON_MMASK ? 2 : 0) | (flags & SDL_BUTTON_RMASK ? 4 : 0);
|
|
232
234
|
}
|
package/package.json
CHANGED
|
@@ -39,6 +39,33 @@ scriptc compiles a subset of TypeScript. What tripped the templates:
|
|
|
39
39
|
|
|
40
40
|
Run `dotframe build macos` early and after each feature: one scriptc error is a quick fix, ninety are a port.
|
|
41
41
|
|
|
42
|
+
## Porting existing TypeScript
|
|
43
|
+
|
|
44
|
+
What a real port (Craft Ones, 66 errors) hit, and the fix for each:
|
|
45
|
+
|
|
46
|
+
| scriptc rejects | Do instead |
|
|
47
|
+
|---|---|
|
|
48
|
+
| `Object.assign(target, ...)` | Assign fields one by one |
|
|
49
|
+
| `ArrayLike<T>` / `Iterable<T>` parameters | Take `T[]` |
|
|
50
|
+
| `readonly T[]`, `.find`/`.filter` on readonly tuples, `for...of` over tuples | Plain arrays (`T[]`), indexed loops |
|
|
51
|
+
| Destructuring an `unknown` network payload | Narrow field by field with `typeof` checks |
|
|
52
|
+
| Non-literal index into a tuple | Make it an array |
|
|
53
|
+
| Dynamic keyed reads on a record whose entries have different shapes | Give every entry the same shape (optional fields), or a `switch` |
|
|
54
|
+
| `string[i]` | `s.charAt(i)` or `s.charCodeAt(i)` |
|
|
55
|
+
| `Number.parseInt` in a static build | `parseInt` or `Math.trunc(Number(s))` |
|
|
56
|
+
| `++`/`--` inside an expression | Its own statement |
|
|
57
|
+
| `satisfies Record<K, Record<string, T>>` | A plain type annotation |
|
|
58
|
+
|
|
59
|
+
Runtime traps (compile clean, fail when run):
|
|
60
|
+
|
|
61
|
+
- **Structural coercion copies.** Passing a class instance, or a wider record, to a parameter typed as a narrower structural type passes a copy: the callee's writes to scalar fields are lost (arrays inside still alias). In Craft Ones every projectile froze mid-flight. Pass the exact type, or return the updated record and copy it back. Reported to scriptc.
|
|
62
|
+
- **Out-of-range reads.** `rows[y - 1]?.[x]` typed as `string` but `undefined` at runtime crashes with "undefined is not representable in the target union". Bounds-check before reading.
|
|
63
|
+
- **`JSON.stringify` key order** differed from JavaScript in Craft Ones (not reproduced in a minimal case). Never build checksums or netplay comparisons from it.
|
|
64
|
+
|
|
65
|
+
## Debugging a native crash
|
|
66
|
+
|
|
67
|
+
`--optimization dev` builds failed to link in Craft Ones (undefined `_main`; a template game links fine). When that happens, debug the release binary with lldb: `lldb ./dist/macos/<name>`, `breakpoint set -n scr_error_new`, `run`, then `bt` shows the TypeScript call site of a runtime error.
|
|
68
|
+
|
|
42
69
|
## Other notes
|
|
43
70
|
|
|
44
71
|
- Benchmarks mean nothing while other processes load the machine. Check `top` first and say what else was running.
|
|
@@ -30,6 +30,8 @@ Run several seeds and a high latency (474 ms is what a transatlantic relay measu
|
|
|
30
30
|
|
|
31
31
|
## Rules
|
|
32
32
|
|
|
33
|
+
- Build checksums from numbers in a fixed order, never from `JSON.stringify`: native builds can order keys differently from JavaScript, so a web peer and a native peer would report false desyncs.
|
|
34
|
+
|
|
33
35
|
- The camera, effects, and HUD timers are simulation state if the simulation ever reads them. Update them once per step, never per drawn frame.
|
|
34
36
|
- Render must restore any random state it uses, or use a separate visual generator.
|
|
35
37
|
- Input delay (`--delay`, default 2) trades latency for fewer rollbacks.
|
|
@@ -1,27 +1,49 @@
|
|
|
1
|
-
// Shared by main.web.ts and main.native.ts: fixed 60 Hz steps
|
|
1
|
+
// Shared by main.web.ts and main.native.ts: keyboard and pointer to inputs, fixed 60 Hz steps, one render per frame.
|
|
2
2
|
import { createDraw2D } from "dotframe/src/draw2d";
|
|
3
3
|
import type { Frame, Setup } from "dotframe/src/gpu";
|
|
4
|
-
import { Key } from "dotframe/src/input";
|
|
4
|
+
import { Key, MouseButton } from "dotframe/src/input";
|
|
5
5
|
import type { Platform } from "dotframe/src/platform";
|
|
6
|
-
import { createGame, keyInput, PLAYERS, render, step, WINDOW } from "./game";
|
|
6
|
+
import { createGame, keyInput, PLAYERS, POINTER_BIT, render, step, WINDOW } from "./game";
|
|
7
7
|
|
|
8
8
|
const STEP = 1 / 60;
|
|
9
|
+
// A full charge takes one second of holding.
|
|
10
|
+
const FULL_CHARGE = 60;
|
|
9
11
|
|
|
10
12
|
export function createSetup(): Setup {
|
|
11
|
-
return ({ gpu, input }: Platform): Frame => {
|
|
13
|
+
return ({ gpu, input, audio }: Platform): Frame => {
|
|
12
14
|
const draw = createDraw2D(gpu, WINDOW.width, WINDOW.height);
|
|
13
15
|
const game = createGame(Math.floor(Math.random() * 1e9));
|
|
14
16
|
let simulated = -1;
|
|
17
|
+
// Hold to charge, release to act: the pattern most mouse and touch games need.
|
|
18
|
+
let charge = 0;
|
|
19
|
+
let wasDown = false;
|
|
15
20
|
return (time: number): boolean => {
|
|
16
21
|
if (simulated < 0) simulated = time;
|
|
17
|
-
// Fixed steps, so every target plays exactly what dotframe sim simulates.
|
|
18
22
|
for (let n = 0; simulated + STEP <= time && n < 5; n++) {
|
|
23
|
+
const pointer = input.pointer();
|
|
24
|
+
const inside = pointer.x >= 0 && pointer.x <= 1 && pointer.y >= 0 && pointer.y <= 1;
|
|
25
|
+
const down = inside && ((pointer.buttons & MouseButton.Left) !== 0 || input.touches().length > 0);
|
|
26
|
+
let released = false;
|
|
27
|
+
if (down) charge = Math.min(FULL_CHARGE, charge + 1);
|
|
28
|
+
else if (wasDown) {
|
|
29
|
+
released = true;
|
|
30
|
+
// Audio is presentation, never simulation state: the pitch says how charged the release was.
|
|
31
|
+
audio.tone(220 + (charge / FULL_CHARGE) * 660, 0.12, 0.25);
|
|
32
|
+
charge = 0;
|
|
33
|
+
}
|
|
34
|
+
wasDown = down;
|
|
19
35
|
const keys = [keyInput(input, [Key.A, Key.D, Key.W, Key.J]), keyInput(input, [Key.Left, Key.Right, Key.Up, Key.L])];
|
|
36
|
+
if (released) keys[0] = keys[0] | POINTER_BIT;
|
|
20
37
|
step(game, keys.slice(0, PLAYERS));
|
|
21
38
|
simulated += STEP;
|
|
22
39
|
}
|
|
23
40
|
draw.begin();
|
|
24
41
|
render(game, draw);
|
|
42
|
+
if (charge > 0) {
|
|
43
|
+
// Charge meter under the play area.
|
|
44
|
+
draw.setFillStyle("#ffffff");
|
|
45
|
+
draw.fillRect(20, WINDOW.height - 16, (WINDOW.width - 40) * (charge / FULL_CHARGE), 6);
|
|
46
|
+
}
|
|
25
47
|
draw.end({ r: 0, g: 0, b: 0 });
|
|
26
48
|
return true;
|
|
27
49
|
};
|
package/templates/blank/game.ts
CHANGED
|
@@ -4,6 +4,8 @@ import type { Draw2D } from "dotframe/src/draw2d";
|
|
|
4
4
|
import type { Input } from "dotframe/src/input";
|
|
5
5
|
|
|
6
6
|
export const WINDOW = { width: 960, height: 540, title: "__NAME__" };
|
|
7
|
+
// The input bit a released mouse or touch press sets for player 1 (fires the action bit); see src/setup.ts.
|
|
8
|
+
export const POINTER_BIT = 8;
|
|
7
9
|
export const PLAYERS = 2;
|
|
8
10
|
|
|
9
11
|
export interface Game {
|
|
@@ -11,6 +11,8 @@ const RIGHT = 2;
|
|
|
11
11
|
const JUMP = 4;
|
|
12
12
|
const ATTACK = 8;
|
|
13
13
|
const GROUND = 440;
|
|
14
|
+
// The input bit a released mouse or touch press sets for player 1 (attacks); see src/setup.ts.
|
|
15
|
+
export const POINTER_BIT = ATTACK;
|
|
14
16
|
const GRAVITY = 0.8;
|
|
15
17
|
const SPEED = 5;
|
|
16
18
|
const JUMP_SPEED = 15;
|
|
@@ -9,6 +9,8 @@ export const PLAYERS = 1;
|
|
|
9
9
|
const LEFT = 1;
|
|
10
10
|
const RIGHT = 2;
|
|
11
11
|
const JUMP = 4;
|
|
12
|
+
// The input bit a released mouse or touch press sets for player 1 (jumps); see src/setup.ts.
|
|
13
|
+
export const POINTER_BIT = JUMP;
|
|
12
14
|
const GRAVITY = 0.7;
|
|
13
15
|
const SPEED = 4.5;
|
|
14
16
|
const JUMP_SPEED = 13;
|