@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 +1 -1
- package/dist/browser.d.ts +19 -0
- package/dist/browser.js +30 -0
- package/package.json +1 -1
- package/src/browser.ts +44 -0
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 [
|
|
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
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));
|