yuuna-engine 0.6.0 → 0.8.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,156 +1,177 @@
1
- <p align="center">
2
- <img src="dist/resources/yuuna.png" alt="Yuuna" width="120" height="120" />
3
- </p>
4
-
5
- # Yuuna
6
-
7
- [Live demo & playground](https://lucy-dot-exe.github.io/yuuna/) · [GitHub](https://github.com/lucy-dot-exe/yuuna)
8
-
9
- A lightweight, state-machine-based TypeScript game engine built for quick
10
- prototypes — drop it into a page and it's running, no editor or build step
11
- required. You describe your game as a `state`, a `render(state)` function,
12
- and a `nextState({ state, event, keyboard })` function — Yuuna owns the
13
- render loop, input handling, and canvas drawing. It's scratch paper for game
14
- ideas, not a replacement for Godot or Unity.
15
-
16
- ## Install
17
-
18
- ```sh
19
- npm install yuuna-engine
20
- ```
21
-
22
- ## Quick start
23
-
24
- Add a canvas with `id="yuuna"` to your page:
25
-
26
- ```html
27
- <canvas id="yuuna"></canvas>
28
- ```
29
-
30
- Then describe your game as state + render + nextState:
31
-
32
- ```ts
33
- import { runEngine } from "yuuna-engine";
34
-
35
- // The shape of your game's data — whatever it takes to fully describe
36
- // what's on screen and how it behaves
37
- type GameState = { cookies: number };
38
-
39
- // What that state looks like before anything has happened yet
40
- const initialState: GameState = { cookies: 0 };
41
-
42
- runEngine<GameState>({
43
- initialState,
44
-
45
- // Given the current state, what should be drawn this frame? Called
46
- // every frame — always derive the picture from state, instead of
47
- // reaching for the canvas directly.
48
- render: (state) => ({
49
- renderables: [
50
- {
51
- type: "TEXT",
52
- text: `${state.cookies} cookies`,
53
- color: "black",
54
- position: { x: 100, y: 50 },
55
- },
56
- {
57
- type: "CIRCLE",
58
- id: "cookie",
59
- isClickable: true,
60
- color: "brown",
61
- position: { x: 50, y: 50 },
62
- radius: 25,
63
- },
64
- ],
65
- }),
66
-
67
- // Given the current state and something that just happened, what's the
68
- // next state? Called once per event (a click, a frame tick, ...) — the
69
- // only place game logic lives.
70
- nextState: ({ state, event }) => {
71
- if (event.tag === "CLICK" && event.id === "cookie") {
72
- return { cookies: state.cookies + 1 };
73
- }
74
-
75
- return state;
76
- },
77
- });
78
- ```
79
-
80
- ## Concepts
81
-
82
- - **Renderables** — declarative shapes drawn each frame: `RECTANGLE`,
83
- `CIRCLE`, `TEXT`, `SPRITE`, `ANIMATED_SPRITE`, `LINE`, and `GROUP`. Give
84
- one an `id` plus `isClickable`/`isHoverable` to make it interactive.
85
- - **Events** — `nextState` receives one `GameEvent` per call: `TIME`,
86
- `CLICK`, `HOVER_IN`, `HOVER_OUT`, `MOUSE_MOVE`, `MOUSE_LEAVE`,
87
- `MUSIC_END`, or a `CUSTOM` event of a type you define yourself, for
88
- reporting things like an async `fetch()` resolving back into your
89
- state machine.
90
- - **Keyboard, camera, sprites & animation, sound effects & music,
91
- canvas config, and mechanics pipelines** all follow the same idea:
92
- small, focused props and functions `runEngine`/`nextState` take, that
93
- compose with everything above instead of replacing it.
94
-
95
- This README stays intentionally thin — the full concept-by-concept
96
- reference, with every option and example, lives on the
97
- [wiki](https://github.com/lucy-dot-exe/yuuna/wiki). The
98
- [playground](https://lucy-dot-exe.github.io/yuuna/#playground) also has a
99
- small, focused example for most of these you can run and edit directly.
100
-
101
- ## Templates
102
-
103
- Prefer a working starting point over typing the quick start out by
104
- hand? Grab one from [`templates/`](templates):
105
-
106
- - **[blank](templates/blank)** — a single `index.html`, zero install —
107
- open it in a browser and it runs.
108
- - **[npm](templates/npm)** — TypeScript + a dev server with hot reload
109
- (via Vite), for a real local project.
110
- - **[neutralino-desktop](templates/neutralino-desktop)** — the `npm`
111
- template wrapped in [Neutralino](https://neutralino.js.org) to run as a
112
- native desktop window instead of a browser tab.
113
-
114
- ```sh
115
- npx degit lucy-dot-exe/yuuna/templates/blank my-game
116
- # or: npx degit lucy-dot-exe/yuuna/templates/npm my-game
117
- # or: npx degit lucy-dot-exe/yuuna/templates/neutralino-desktop my-game
118
- ```
119
-
120
- [`degit`](https://github.com/Rich-Harris/degit) copies the folder without
121
- its git history — no cloning or forking the whole engine repo needed.
122
- Each template's own README has more on running it once copied.
123
-
124
- ## Development
125
-
126
- ```sh
127
- yarn install
128
- yarn build # builds lib/ (npm package) and dist/bundle.js (landing page)
129
- yarn watch # rebuild on change
130
- ```
131
-
132
- `dist/index.html` is the landing page — it loads `dist/bundle.js` in the
133
- browser via a global `Yuuna` object and embeds a live Monaco editor so
134
- visitors can edit and run a game directly on the page.
135
-
136
- ## Assets
137
-
138
- The examples' art/sound/music lives in `dist/resources/`, gitignored
139
- rather than committed — this repo being open source doesn't make every
140
- asset in it free to redistribute. `runEngine()` falls back to a
141
- generated placeholder for any image that isn't there (and simply plays
142
- nothing for missing audio) instead of failing, so the examples still
143
- run without them — just with placeholder art in place of the real
144
- thing. Drop the real files in locally (or restore them from wherever
145
- you got this repo from) to see them for real.
146
-
147
- Currently used:
148
-
149
- - **[Free Pixel Food!](https://henrysoftware.itch.io/pixel-food)** by
150
- [Henry Software](https://henrysoftware.itch.io/) — the food icons in
151
- the Food Clicker and Sprites examples. CC0; credited here by choice,
152
- not requirement.
153
-
154
- ## License
155
-
156
- MIT © [lucy-dot-exe](https://github.com/lucy-dot-exe)
1
+ <p align="center">
2
+ <img src="dist/resources/yuuna.png" alt="Yuuna" width="120" height="120" />
3
+ </p>
4
+
5
+ # Yuuna
6
+
7
+ [Live demo & playground](https://lucy-dot-exe.github.io/yuuna/) · [GitHub](https://github.com/lucy-dot-exe/yuuna)
8
+
9
+ A lightweight, state-machine-based TypeScript game engine — drop it into a
10
+ page and it's running, no editor or build step required. You describe your
11
+ game as a `state`, a `render(state)` function, and a
12
+ `nextState({ state, event, keyboard })` function — the reducer pattern
13
+ (`(state, event) => nextState`, same shape as a Redux reducer or
14
+ `useReducer`) — and Yuuna owns the render loop, input handling, and canvas
15
+ drawing.
16
+
17
+ ## Install
18
+
19
+ ```sh
20
+ npm install yuuna-engine
21
+ ```
22
+
23
+ ## Quick start
24
+
25
+ Add a canvas with `id="yuuna"` to your page:
26
+
27
+ ```html
28
+ <canvas id="yuuna"></canvas>
29
+ ```
30
+
31
+ Then describe your game as state + render + nextState:
32
+
33
+ ```ts
34
+ import { runEngine } from "yuuna-engine";
35
+
36
+ // The shape of your game's data — whatever it takes to fully describe
37
+ // what's on screen and how it behaves
38
+ type GameState = { cookies: number };
39
+
40
+ // What that state looks like before anything has happened yet
41
+ const initialState: GameState = { cookies: 0 };
42
+
43
+ runEngine<GameState>({
44
+ initialState,
45
+
46
+ // Given the current state, what should be drawn this frame? Called
47
+ // every frame — always derive the picture from state, instead of
48
+ // reaching for the canvas directly.
49
+ render: (state) => ({
50
+ renderables: [
51
+ {
52
+ type: "TEXT",
53
+ text: `${state.cookies} cookies`,
54
+ color: "black",
55
+ position: { x: 100, y: 50 },
56
+ },
57
+ {
58
+ type: "CIRCLE",
59
+ id: "cookie",
60
+ isClickable: true,
61
+ color: "brown",
62
+ position: { x: 50, y: 50 },
63
+ radius: 25,
64
+ },
65
+ ],
66
+ }),
67
+
68
+ // Given the current state and something that just happened, what's the
69
+ // next state? Called once per event (a click, a frame tick, ...) — the
70
+ // only place game logic lives.
71
+ nextState: ({ state, event }) => {
72
+ if (event.tag === "CLICK" && event.id === "cookie") {
73
+ return { cookies: state.cookies + 1 };
74
+ }
75
+
76
+ return state;
77
+ },
78
+ });
79
+ ```
80
+
81
+ ## Concepts
82
+
83
+ - **Renderables** — declarative shapes drawn each frame: `RECTANGLE`,
84
+ `CIRCLE`, `TEXT`, `SPRITE`, `ANIMATED_SPRITE`, `LINE`, and `GROUP`. Give
85
+ one an `id` plus `isClickable`/`isHoverable` to make it interactive.
86
+ - **Events** — `nextState` receives one `GameEvent` per call: `TIME`,
87
+ `CLICK`, `HOVER_IN`, `HOVER_OUT`, `MOUSE_MOVE`, `MOUSE_LEAVE`,
88
+ `MUSIC_END`, or a `CUSTOM` event of a type you define yourself, for
89
+ reporting things like an async `fetch()` resolving back into your
90
+ state machine.
91
+ - **Keyboard, camera, sprites & animation, sound effects & music,
92
+ canvas config, and mechanics pipelines** all follow the same idea:
93
+ small, focused props and functions `runEngine`/`nextState` take, that
94
+ compose with everything above instead of replacing it.
95
+ - **Mechanics pipelines** — `nextState` can be an array of small reducers
96
+ instead of one big function; each runs in order per event, the way
97
+ Redux middleware chains do, and any of them can return `STOP` to end
98
+ the pipeline early for that event.
99
+
100
+ This README stays intentionally thin — the full concept-by-concept
101
+ reference, with every option and example, lives on the
102
+ [wiki](https://github.com/lucy-dot-exe/yuuna/wiki). The
103
+ [playground](https://lucy-dot-exe.github.io/yuuna/#playground) also has a
104
+ small, focused example for most of these you can run and edit directly.
105
+
106
+ ## Templates
107
+
108
+ Prefer a working starting point over typing the quick start out by
109
+ hand? Grab one from [`templates/`](templates):
110
+
111
+ - **[blank](templates/blank)** — a single `index.html`, zero install —
112
+ open it in a browser and it runs.
113
+ - **[npm](templates/npm)** — TypeScript + a dev server with hot reload
114
+ (via Vite), for a real local project.
115
+ - **[neutralino-desktop](templates/neutralino-desktop)** — the `npm`
116
+ template wrapped in [Neutralino](https://neutralino.js.org) to run as a
117
+ native desktop window.
118
+
119
+ ```sh
120
+ npx degit lucy-dot-exe/yuuna/templates/blank my-game
121
+ # or: npx degit lucy-dot-exe/yuuna/templates/npm my-game
122
+ # or: npx degit lucy-dot-exe/yuuna/templates/neutralino-desktop my-game
123
+ ```
124
+
125
+ [`degit`](https://github.com/Rich-Harris/degit) copies the folder without
126
+ its git history — no cloning or forking the whole engine repo needed.
127
+ Each template's own README has more on running it once copied.
128
+
129
+ ## Made with Yuuna
130
+
131
+ - **[Yuuna's Heroes](https://lucinaexe.itch.io/yuunas-td)** — a game made
132
+ using Yuuna.
133
+
134
+ ## Development
135
+
136
+ ```sh
137
+ yarn install
138
+ yarn build # builds lib/ (npm package) and dist/bundle.js (landing page)
139
+ yarn watch # rebuild on change
140
+ ```
141
+
142
+ `dist/index.html` is the landing page — it loads `dist/bundle.js` in the
143
+ browser via a global `Yuuna` object and embeds a live Monaco editor so
144
+ visitors can edit and run a game directly on the page.
145
+
146
+ ## Assets
147
+
148
+ The examples' art/sound/music lives in `dist/resources/`, gitignored
149
+ rather than committed — this repo being open source doesn't make every
150
+ asset in it free to redistribute. `runEngine()` falls back to a
151
+ generated placeholder for any image that isn't there (and simply plays
152
+ nothing for missing audio) instead of failing, so the examples still
153
+ run without them — just with placeholder art in place of the real
154
+ thing. Drop the real files in locally (or restore them from wherever
155
+ you got this repo from) to see them for real.
156
+
157
+ Only MIT- or CC0-licensed assets are used, so nothing here needs
158
+ attribution to run or redistribute — the credits below are given by
159
+ choice, not requirement.
160
+
161
+ Currently used:
162
+
163
+ - **[Free Pixel Food!](https://henrysoftware.itch.io/pixel-food)** by
164
+ [Henry Software](https://henrysoftware.itch.io/) — the food icons in
165
+ the Food Clicker and Sprites examples. CC0.
166
+ - **[Sunny Land Pixel Game Art](https://ansimuz.itch.io/sunny-land-pixel-game-art)**
167
+ by [ansimuz](https://ansimuz.itch.io/) — the fox (idle/walk/jump), the
168
+ ground/platform tile, and the parallax sky background in the
169
+ Platformer example. CC0.
170
+ - **[Mini Pixel Pack 3](https://grafxkid.itch.io/mini-pixel-pack-3)** by
171
+ [GrafxKid](https://grafxkid.itch.io/) — the ship, the charged-beam
172
+ bullet, Alan (the enemy), and the parallax starfield in the Shoot Em
173
+ Up example. CC0.
174
+
175
+ ## License
176
+
177
+ MIT © [lucy-dot-exe](https://github.com/lucy-dot-exe)
@@ -0,0 +1,8 @@
1
+ import { AnimatedSpriteRenderable, CircleRenderable, GroupRenderable, LineRenderable, RectangleRenderable, SpriteRenderable, TextRenderable } from "./types";
2
+ export declare const rectangle: (props: Omit<RectangleRenderable, "type">) => RectangleRenderable;
3
+ export declare const circle: (props: Omit<CircleRenderable, "type">) => CircleRenderable;
4
+ export declare const text: (props: Omit<TextRenderable, "type">) => TextRenderable;
5
+ export declare const sprite: (props: Omit<SpriteRenderable, "type">) => SpriteRenderable;
6
+ export declare const animatedSprite: (props: Omit<AnimatedSpriteRenderable, "type">) => AnimatedSpriteRenderable;
7
+ export declare const line: (props: Omit<LineRenderable, "type">) => LineRenderable;
8
+ export declare const group: (props: Omit<GroupRenderable, "type">) => GroupRenderable;
@@ -54,7 +54,7 @@ export type SpriteRenderable = BaseRenderable & {
54
54
  y: number;
55
55
  };
56
56
  resourceId: string;
57
- frame: number;
57
+ frame?: number;
58
58
  opacity?: number;
59
59
  flipX?: boolean;
60
60
  swapColors?: {
@@ -191,6 +191,11 @@ export type NextStateProps<State, Custom = never> = {
191
191
  isJustPressed: boolean;
192
192
  isJustReleased: boolean;
193
193
  }>;
194
+ mouseButton: {
195
+ isPressed: boolean;
196
+ isJustPressed: boolean;
197
+ isJustReleased: boolean;
198
+ };
194
199
  playSound: (id: string) => void;
195
200
  playMusic: (id: string) => void;
196
201
  pauseMusic: () => void;
@@ -199,6 +204,22 @@ export type NextStateProps<State, Custom = never> = {
199
204
  };
200
205
  export declare const STOP: "Yuuna.STOP";
201
206
  export type NextStateFunction<State, Custom = never> = (props: NextStateProps<State, Custom>) => State | typeof STOP | undefined;
207
+ export type ResourceConfig = {
208
+ src: string;
209
+ size?: {
210
+ width: number;
211
+ height: number;
212
+ };
213
+ slices?: {
214
+ vertical: number;
215
+ horizontal: number;
216
+ };
217
+ animations?: Record<string, {
218
+ frames: number[];
219
+ frameDuration: number;
220
+ loop: boolean;
221
+ }>;
222
+ };
202
223
  export type RunEngineProps<State, Custom = never> = {
203
224
  initialState: State;
204
225
  render: (state: State) => {
@@ -206,22 +227,7 @@ export type RunEngineProps<State, Custom = never> = {
206
227
  renderables: Renderable[];
207
228
  };
208
229
  nextState: NextStateFunction<State, Custom> | NextStateFunction<State, Custom>[];
209
- resources?: Record<string, {
210
- src: string;
211
- size: {
212
- width: number;
213
- height: number;
214
- };
215
- slices: {
216
- vertical: number;
217
- horizontal: number;
218
- };
219
- animations?: Record<string, {
220
- frames: number[];
221
- frameDuration: number;
222
- loop: boolean;
223
- }>;
224
- }>;
230
+ resources?: Record<string, ResourceConfig>;
225
231
  sounds?: Record<string, {
226
232
  src: string;
227
233
  }>;
@@ -234,17 +240,20 @@ export type RunEngineProps<State, Custom = never> = {
234
240
  height?: number;
235
241
  backgroundColor?: string;
236
242
  resize?: "none" | "fit" | "stretch";
243
+ pixelRatio?: number | true;
237
244
  };
238
245
  camera?: (state: State) => {
239
246
  x: number;
240
247
  y: number;
241
248
  zoom: number;
242
249
  };
250
+ timeScale?: (state: State) => number;
243
251
  };
244
252
  export type RunEngineFunction = <State, Custom = never>(props: RunEngineProps<State, Custom>) => Promise<{
245
253
  sendEvent: (event: Custom) => void;
246
254
  requestFullscreen: () => Promise<void>;
247
255
  exitFullscreen: () => Promise<void>;
256
+ addResource: (id: string, resource: ResourceConfig) => Promise<void>;
248
257
  }>;
249
258
  export declare const keyboardKeys: readonly ["ControlLeft", "ControlRight", "AltLeft", "AltRight", "CapsLock", "End", "Delete", "Tab", "Space", "Enter", "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Numpad0", "Numpad1", "Numpad2", "Numpad3", "Numpad4", "Numpad5", "Numpad6", "Numpad7", "Numpad8", "Numpad9", "Digit0", "Digit1", "Digit2", "Digit3", "Digit4", "Digit5", "Digit6", "Digit7", "Digit8", "Digit9", "KeyA", "KeyB", "KeyC", "KeyD", "KeyE", "KeyF", "KeyG", "KeyH", "KeyI", "KeyJ", "KeyK", "KeyL", "KeyM", "KeyN", "KeyO", "KeyP", "KeyQ", "KeyR", "KeyS", "KeyT", "KeyU", "KeyV", "KeyW", "KeyX", "KeyY", "KeyZ"];
250
259
  export type KeyboardKeys = (typeof keyboardKeys)[number];
@@ -252,5 +261,12 @@ export type KeyboardState = Record<KeyboardKeys, boolean>;
252
261
  export declare var Yuuna: {
253
262
  runEngine: RunEngineFunction;
254
263
  STOP: typeof STOP;
264
+ rectangle: (props: Omit<RectangleRenderable, "type">) => RectangleRenderable;
265
+ circle: (props: Omit<CircleRenderable, "type">) => CircleRenderable;
266
+ text: (props: Omit<TextRenderable, "type">) => TextRenderable;
267
+ sprite: (props: Omit<SpriteRenderable, "type">) => SpriteRenderable;
268
+ animatedSprite: (props: Omit<AnimatedSpriteRenderable, "type">) => AnimatedSpriteRenderable;
269
+ line: (props: Omit<LineRenderable, "type">) => LineRenderable;
270
+ group: (props: Omit<GroupRenderable, "type">) => GroupRenderable;
255
271
  };
256
272
  export {};