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 +177 -156
- package/lib/engine/renderables.d.ts +8 -0
- package/lib/engine/types.d.ts +33 -17
- package/lib/index.cjs +298 -41
- package/lib/index.cjs.map +1 -1
- package/lib/index.d.ts +3 -2
- package/lib/index.js +292 -42
- package/lib/index.js.map +1 -1
- package/package.json +4 -2
- package/lib/utils/constants.d.ts +0 -6
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
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
`
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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;
|
package/lib/engine/types.d.ts
CHANGED
|
@@ -54,7 +54,7 @@ export type SpriteRenderable = BaseRenderable & {
|
|
|
54
54
|
y: number;
|
|
55
55
|
};
|
|
56
56
|
resourceId: string;
|
|
57
|
-
frame
|
|
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 {};
|