@ludra/core 0.3.0 → 0.4.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @ludra/core
2
2
 
3
- The runtime of [Ludra](https://github.com/ersingvn/ludra), the game engine built for AI agents. Most people want the [`ludra`](https://www.npmjs.com/package/ludra) CLI instead; use this package to embed the engine.
3
+ The runtime of [Ludra](https://github.com/ersingvn/ludra), the game engine built for AI agents. Most people want the [`@ludra/cli`](https://www.npmjs.com/package/@ludra/cli) command instead; use this package to embed the engine.
4
4
 
5
5
  - **Scene format and validation:** `validateScene(json)` returns diagnostics with a JSON path and a concrete fix.
6
6
  - **Deterministic simulation:** `new World(scene)` advances only through `world.step(heldActions)` at a fixed 60 Hz.
package/dist/browser.d.ts CHANGED
@@ -10,3 +10,22 @@ export interface GameOptions {
10
10
  * Resolves (after images load) to a function that stops the game.
11
11
  */
12
12
  export declare function startGame(canvas: HTMLCanvasElement, scene: SceneDef, options?: GameOptions): Promise<() => void>;
13
+ export interface CaptureOptions extends GameOptions {
14
+ /** Input script, e.g. `"right:1s, jump:0.4s"`. See `InputScript`. */
15
+ inputs?: string;
16
+ /** Simulation frames to capture (60 per second). Frame 0 is the starting state. */
17
+ frames: readonly number[];
18
+ }
19
+ export interface CapturedFrame {
20
+ frame: number;
21
+ time: number;
22
+ /** PNG as a data URL. */
23
+ png: string;
24
+ }
25
+ /**
26
+ * Plays a scene without a clock or keyboard and returns the requested frames
27
+ * as PNGs. Same renderer, effects and particles as `startGame`, stepped
28
+ * deterministically, so a frame always looks the same. Used by
29
+ * `ludra play --screenshot`.
30
+ */
31
+ export declare function captureFrames(canvas: HTMLCanvasElement, scene: SceneDef, options: CaptureOptions): Promise<CapturedFrame[]>;
package/dist/browser.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { SoundPlayer } from "./audio.js";
2
2
  import { parseFrames } from "./components.js";
3
3
  import { collectAssetPaths } from "./effects.js";
4
+ import { InputScript } from "./input.js";
4
5
  import { ParticleSystem } from "./particles.js";
5
6
  import { FIXED_DT } from "./time.js";
6
7
  import { World } from "./world.js";
@@ -92,6 +93,35 @@ export async function startGame(canvas, scene, options = {}) {
92
93
  window.removeEventListener("blur", onBlur);
93
94
  };
94
95
  }
96
+ /**
97
+ * Plays a scene without a clock or keyboard and returns the requested frames
98
+ * as PNGs. Same renderer, effects and particles as `startGame`, stepped
99
+ * deterministically, so a frame always looks the same. Used by
100
+ * `ludra play --screenshot`.
101
+ */
102
+ export async function captureFrames(canvas, scene, options) {
103
+ const ctx = canvas.getContext("2d");
104
+ if (!ctx)
105
+ throw new Error("Canvas 2D context is not available.");
106
+ const images = await loadImages(scene, options.assetBase ?? "");
107
+ const view = new View(scene, images);
108
+ const world = new World(scene);
109
+ const script = InputScript.parse(options.inputs ?? "");
110
+ const wanted = new Set(options.frames);
111
+ const last = Math.max(0, ...options.frames);
112
+ const captured = [];
113
+ for (let frame = 0;; frame++) {
114
+ if (wanted.has(frame)) {
115
+ view.draw(ctx, world, false);
116
+ captured.push({ frame, time: frame * FIXED_DT, png: canvas.toDataURL("image/png") });
117
+ }
118
+ if (frame >= last)
119
+ break;
120
+ world.step(script.at(frame));
121
+ view.step(world, () => { });
122
+ }
123
+ return captured;
124
+ }
95
125
  async function loadImages(scene, assetBase) {
96
126
  const images = new Map();
97
127
  const paths = new Set(collectAssetPaths(scene).filter((ref) => ref.kind === "image").map((ref) => ref.path));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ludra/core",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Ludra engine runtime: scene format, validation, deterministic simulation and rendering.",
5
5
  "license": "MIT",
6
6
  "repository": {
package/src/browser.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { SoundPlayer } from "./audio.ts";
2
2
  import { parseFrames, type Sprite } from "./components.ts";
3
3
  import { collectAssetPaths } from "./effects.ts";
4
+ import { InputScript } from "./input.ts";
4
5
  import { ParticleSystem } from "./particles.ts";
5
6
  import { FIXED_DT } from "./time.ts";
6
7
  import type { Action, SceneDef } from "./types.ts";
@@ -101,6 +102,49 @@ export async function startGame(canvas: HTMLCanvasElement, scene: SceneDef, opti
101
102
  };
102
103
  }
103
104
 
105
+ export interface CaptureOptions extends GameOptions {
106
+ /** Input script, e.g. `"right:1s, jump:0.4s"`. See `InputScript`. */
107
+ inputs?: string;
108
+ /** Simulation frames to capture (60 per second). Frame 0 is the starting state. */
109
+ frames: readonly number[];
110
+ }
111
+
112
+ export interface CapturedFrame {
113
+ frame: number;
114
+ time: number;
115
+ /** PNG as a data URL. */
116
+ png: string;
117
+ }
118
+
119
+ /**
120
+ * Plays a scene without a clock or keyboard and returns the requested frames
121
+ * as PNGs. Same renderer, effects and particles as `startGame`, stepped
122
+ * deterministically, so a frame always looks the same. Used by
123
+ * `ludra play --screenshot`.
124
+ */
125
+ export async function captureFrames(canvas: HTMLCanvasElement, scene: SceneDef, options: CaptureOptions): Promise<CapturedFrame[]> {
126
+ const ctx = canvas.getContext("2d");
127
+ if (!ctx) throw new Error("Canvas 2D context is not available.");
128
+ const images = await loadImages(scene, options.assetBase ?? "");
129
+ const view = new View(scene, images);
130
+ const world = new World(scene);
131
+ const script = InputScript.parse(options.inputs ?? "");
132
+ const wanted = new Set(options.frames);
133
+ const last = Math.max(0, ...options.frames);
134
+ const captured: CapturedFrame[] = [];
135
+
136
+ for (let frame = 0; ; frame++) {
137
+ if (wanted.has(frame)) {
138
+ view.draw(ctx, world, false);
139
+ captured.push({ frame, time: frame * FIXED_DT, png: canvas.toDataURL("image/png") });
140
+ }
141
+ if (frame >= last) break;
142
+ world.step(script.at(frame));
143
+ view.step(world, () => {});
144
+ }
145
+ return captured;
146
+ }
147
+
104
148
  async function loadImages(scene: SceneDef, assetBase: string): Promise<Map<string, HTMLImageElement>> {
105
149
  const images = new Map<string, HTMLImageElement>();
106
150
  const paths = new Set(collectAssetPaths(scene).filter((ref) => ref.kind === "image").map((ref) => ref.path));