yuuna-engine 0.2.1 → 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 +133 -135
- package/lib/engine/types.d.ts +98 -34
- package/lib/index.cjs +446 -35
- package/lib/index.cjs.map +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.js +446 -35
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,135 +1,133 @@
|
|
|
1
|
-
<p align="center">
|
|
2
|
-
<img src="dist/resources/yuuna.png" alt="Yuuna" width="120" height="120" />
|
|
3
|
-
</p>
|
|
4
|
-
|
|
5
|
-
# Yuuna
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
color: "
|
|
54
|
-
position: { x:
|
|
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
|
-
yarn
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
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 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`, `MUSIC_END`, or a
|
|
87
|
+
`CUSTOM` event of a type you define yourself, for reporting things like
|
|
88
|
+
an async `fetch()` resolving back into your state machine.
|
|
89
|
+
- **Keyboard, camera, sprites & animation, sound effects & music,
|
|
90
|
+
canvas config, and mechanics pipelines** all follow the same idea:
|
|
91
|
+
small, focused props and functions `runEngine`/`nextState` take, that
|
|
92
|
+
compose with everything above instead of replacing it.
|
|
93
|
+
|
|
94
|
+
This README stays intentionally thin — the full concept-by-concept
|
|
95
|
+
reference, with every option and example, lives on the
|
|
96
|
+
[wiki](https://github.com/lucy-dot-exe/yuuna/wiki). The
|
|
97
|
+
[playground](https://lucy-dot-exe.github.io/yuuna/#playground) also has a
|
|
98
|
+
small, focused example for most of these you can run and edit directly.
|
|
99
|
+
|
|
100
|
+
## Templates
|
|
101
|
+
|
|
102
|
+
Prefer a working starting point over typing the quick start out by
|
|
103
|
+
hand? Grab one from [`templates/`](templates):
|
|
104
|
+
|
|
105
|
+
- **[blank](templates/blank)** — a single `index.html`, zero install —
|
|
106
|
+
open it in a browser and it runs.
|
|
107
|
+
- **[npm](templates/npm)** — TypeScript + a dev server with hot reload
|
|
108
|
+
(via Vite), for a real local project.
|
|
109
|
+
|
|
110
|
+
```sh
|
|
111
|
+
npx degit lucy-dot-exe/yuuna/templates/blank my-game
|
|
112
|
+
# or: npx degit lucy-dot-exe/yuuna/templates/npm my-game
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
[`degit`](https://github.com/Rich-Harris/degit) copies the folder without
|
|
116
|
+
its git history — no cloning or forking the whole engine repo needed.
|
|
117
|
+
Each template's own README has more on running it once copied.
|
|
118
|
+
|
|
119
|
+
## Development
|
|
120
|
+
|
|
121
|
+
```sh
|
|
122
|
+
yarn install
|
|
123
|
+
yarn build # builds lib/ (npm package) and dist/bundle.js (landing page)
|
|
124
|
+
yarn watch # rebuild on change
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`dist/index.html` is the landing page — it loads `dist/bundle.js` in the
|
|
128
|
+
browser via a global `Yuuna` object and embeds a live Monaco editor so
|
|
129
|
+
visitors can edit and run a game directly on the page.
|
|
130
|
+
|
|
131
|
+
## License
|
|
132
|
+
|
|
133
|
+
MIT © [lucy-dot-exe](https://github.com/lucy-dot-exe)
|
package/lib/engine/types.d.ts
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
|
-
|
|
1
|
+
type BaseRenderable = {
|
|
2
|
+
layer?: number;
|
|
3
|
+
scale?: {
|
|
4
|
+
x: number;
|
|
5
|
+
y: number;
|
|
6
|
+
};
|
|
7
|
+
modulate?: string;
|
|
8
|
+
children?: Renderable[];
|
|
9
|
+
screenSpace?: boolean;
|
|
10
|
+
id?: string;
|
|
11
|
+
isClickable?: boolean;
|
|
12
|
+
isHoverable?: boolean;
|
|
13
|
+
trackMouseMovement?: boolean;
|
|
14
|
+
};
|
|
15
|
+
export type RectangleRenderable = BaseRenderable & {
|
|
2
16
|
type: "RECTANGLE";
|
|
3
17
|
position: {
|
|
4
18
|
x: number;
|
|
@@ -9,12 +23,8 @@ export type RectangleRenderable = {
|
|
|
9
23
|
height: number;
|
|
10
24
|
};
|
|
11
25
|
color: string;
|
|
12
|
-
id?: string;
|
|
13
|
-
isClickable?: boolean;
|
|
14
|
-
isHoverable?: boolean;
|
|
15
|
-
trackMouseMovement?: boolean;
|
|
16
26
|
};
|
|
17
|
-
export type CircleRenderable = {
|
|
27
|
+
export type CircleRenderable = BaseRenderable & {
|
|
18
28
|
type: "CIRCLE";
|
|
19
29
|
position: {
|
|
20
30
|
x: number;
|
|
@@ -22,12 +32,8 @@ export type CircleRenderable = {
|
|
|
22
32
|
};
|
|
23
33
|
radius: number;
|
|
24
34
|
color: string;
|
|
25
|
-
id?: string;
|
|
26
|
-
isClickable?: boolean;
|
|
27
|
-
isHoverable?: boolean;
|
|
28
|
-
trackMouseMovement?: boolean;
|
|
29
35
|
};
|
|
30
|
-
export type TextRenderable = {
|
|
36
|
+
export type TextRenderable = BaseRenderable & {
|
|
31
37
|
type: "TEXT";
|
|
32
38
|
text: string;
|
|
33
39
|
color: string;
|
|
@@ -39,12 +45,9 @@ export type TextRenderable = {
|
|
|
39
45
|
x: "left" | "center" | "right";
|
|
40
46
|
y: "bottom" | "middle" | "top";
|
|
41
47
|
};
|
|
42
|
-
|
|
43
|
-
isClickable?: boolean;
|
|
44
|
-
isHoverable?: boolean;
|
|
45
|
-
trackMouseMovement?: boolean;
|
|
48
|
+
fontSize?: number;
|
|
46
49
|
};
|
|
47
|
-
export type SpriteRenderable = {
|
|
50
|
+
export type SpriteRenderable = BaseRenderable & {
|
|
48
51
|
type: "SPRITE";
|
|
49
52
|
position: {
|
|
50
53
|
x: number;
|
|
@@ -52,15 +55,10 @@ export type SpriteRenderable = {
|
|
|
52
55
|
};
|
|
53
56
|
resourceId: string;
|
|
54
57
|
frame: number;
|
|
55
|
-
scale?: number;
|
|
56
58
|
opacity?: number;
|
|
57
59
|
flipX?: boolean;
|
|
58
|
-
id?: string;
|
|
59
|
-
isClickable?: boolean;
|
|
60
|
-
isHoverable?: boolean;
|
|
61
|
-
trackMouseMovement?: boolean;
|
|
62
60
|
};
|
|
63
|
-
export type LineRenderable = {
|
|
61
|
+
export type LineRenderable = BaseRenderable & {
|
|
64
62
|
type: "LINE";
|
|
65
63
|
from: {
|
|
66
64
|
x: number;
|
|
@@ -72,12 +70,29 @@ export type LineRenderable = {
|
|
|
72
70
|
};
|
|
73
71
|
color: string;
|
|
74
72
|
width?: number;
|
|
75
|
-
id?: string;
|
|
76
|
-
isClickable?: boolean;
|
|
77
|
-
isHoverable?: boolean;
|
|
78
|
-
trackMouseMovement?: boolean;
|
|
79
73
|
};
|
|
80
|
-
export type
|
|
74
|
+
export type AnimatedSpriteRenderable = BaseRenderable & {
|
|
75
|
+
type: "ANIMATED_SPRITE";
|
|
76
|
+
position: {
|
|
77
|
+
x: number;
|
|
78
|
+
y: number;
|
|
79
|
+
};
|
|
80
|
+
resourceId: string;
|
|
81
|
+
animation: string;
|
|
82
|
+
timeScale?: number;
|
|
83
|
+
paused?: boolean;
|
|
84
|
+
opacity?: number;
|
|
85
|
+
flipX?: boolean;
|
|
86
|
+
id: string;
|
|
87
|
+
};
|
|
88
|
+
export type GroupRenderable = BaseRenderable & {
|
|
89
|
+
type: "GROUP";
|
|
90
|
+
position: {
|
|
91
|
+
x: number;
|
|
92
|
+
y: number;
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
export type Renderable = RectangleRenderable | CircleRenderable | SpriteRenderable | TextRenderable | LineRenderable | GroupRenderable | AnimatedSpriteRenderable;
|
|
81
96
|
export type TimeEvent = {
|
|
82
97
|
tag: "TIME";
|
|
83
98
|
delta: number;
|
|
@@ -89,6 +104,10 @@ export type ClickEvent = {
|
|
|
89
104
|
x: number;
|
|
90
105
|
y: number;
|
|
91
106
|
};
|
|
107
|
+
worldMouse: {
|
|
108
|
+
x: number;
|
|
109
|
+
y: number;
|
|
110
|
+
};
|
|
92
111
|
};
|
|
93
112
|
export type HoverInEvent = {
|
|
94
113
|
tag: "HOVER_IN";
|
|
@@ -97,6 +116,10 @@ export type HoverInEvent = {
|
|
|
97
116
|
x: number;
|
|
98
117
|
y: number;
|
|
99
118
|
};
|
|
119
|
+
worldMouse: {
|
|
120
|
+
x: number;
|
|
121
|
+
y: number;
|
|
122
|
+
};
|
|
100
123
|
};
|
|
101
124
|
export type HoverOutEvent = {
|
|
102
125
|
tag: "HOVER_OUT";
|
|
@@ -105,6 +128,10 @@ export type HoverOutEvent = {
|
|
|
105
128
|
x: number;
|
|
106
129
|
y: number;
|
|
107
130
|
};
|
|
131
|
+
worldMouse: {
|
|
132
|
+
x: number;
|
|
133
|
+
y: number;
|
|
134
|
+
};
|
|
108
135
|
};
|
|
109
136
|
export type MouseMoveEvent = {
|
|
110
137
|
tag: "MOUSE_MOVE";
|
|
@@ -113,26 +140,43 @@ export type MouseMoveEvent = {
|
|
|
113
140
|
x: number;
|
|
114
141
|
y: number;
|
|
115
142
|
};
|
|
143
|
+
worldMouse: {
|
|
144
|
+
x: number;
|
|
145
|
+
y: number;
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
export type MusicEndEvent = {
|
|
149
|
+
tag: "MUSIC_END";
|
|
150
|
+
id: string;
|
|
151
|
+
};
|
|
152
|
+
export type GameEvent = TimeEvent | ClickEvent | HoverInEvent | HoverOutEvent | MouseMoveEvent | MusicEndEvent;
|
|
153
|
+
export type CustomGameEvent<Custom> = {
|
|
154
|
+
tag: "CUSTOM";
|
|
155
|
+
event: Custom;
|
|
116
156
|
};
|
|
117
|
-
export type
|
|
118
|
-
export type NextStateProps<State> = {
|
|
157
|
+
export type NextStateProps<State, Custom = never> = {
|
|
119
158
|
state: State;
|
|
120
|
-
event: GameEvent
|
|
159
|
+
event: GameEvent | CustomGameEvent<Custom>;
|
|
121
160
|
keyboard: Record<KeyboardKeys, {
|
|
122
161
|
isPressed: boolean;
|
|
123
162
|
isJustPressed: boolean;
|
|
124
163
|
isJustReleased: boolean;
|
|
125
164
|
}>;
|
|
165
|
+
playSound: (id: string) => void;
|
|
166
|
+
playMusic: (id: string) => void;
|
|
167
|
+
pauseMusic: () => void;
|
|
168
|
+
resumeMusic: () => void;
|
|
169
|
+
setMusicVolume: (volume: number) => void;
|
|
126
170
|
};
|
|
127
171
|
export declare const STOP: "Yuuna.STOP";
|
|
128
|
-
export type NextStateFunction<State> = (props: NextStateProps<State>) => State | typeof STOP | undefined;
|
|
129
|
-
export type RunEngineProps<State> = {
|
|
172
|
+
export type NextStateFunction<State, Custom = never> = (props: NextStateProps<State, Custom>) => State | typeof STOP | undefined;
|
|
173
|
+
export type RunEngineProps<State, Custom = never> = {
|
|
130
174
|
initialState: State;
|
|
131
175
|
render: (state: State) => {
|
|
132
176
|
cursor?: "default" | "pointer";
|
|
133
177
|
renderables: Renderable[];
|
|
134
178
|
};
|
|
135
|
-
nextState: NextStateFunction<State> | NextStateFunction<State>[];
|
|
179
|
+
nextState: NextStateFunction<State, Custom> | NextStateFunction<State, Custom>[];
|
|
136
180
|
resources?: Record<string, {
|
|
137
181
|
src: string;
|
|
138
182
|
size: {
|
|
@@ -143,14 +187,33 @@ export type RunEngineProps<State> = {
|
|
|
143
187
|
vertical: number;
|
|
144
188
|
horizontal: number;
|
|
145
189
|
};
|
|
190
|
+
animations?: Record<string, {
|
|
191
|
+
frames: number[];
|
|
192
|
+
frameDuration: number;
|
|
193
|
+
loop: boolean;
|
|
194
|
+
}>;
|
|
195
|
+
}>;
|
|
196
|
+
sounds?: Record<string, {
|
|
197
|
+
src: string;
|
|
198
|
+
}>;
|
|
199
|
+
music?: Record<string, {
|
|
200
|
+
src: string;
|
|
201
|
+
loop?: boolean;
|
|
146
202
|
}>;
|
|
147
203
|
canvas?: {
|
|
148
204
|
width?: number;
|
|
149
205
|
height?: number;
|
|
150
206
|
backgroundColor?: string;
|
|
151
207
|
};
|
|
208
|
+
camera?: (state: State) => {
|
|
209
|
+
x: number;
|
|
210
|
+
y: number;
|
|
211
|
+
zoom: number;
|
|
212
|
+
};
|
|
152
213
|
};
|
|
153
|
-
export type RunEngineFunction = <State>(props: RunEngineProps<State>) => Promise<
|
|
214
|
+
export type RunEngineFunction = <State, Custom = never>(props: RunEngineProps<State, Custom>) => Promise<{
|
|
215
|
+
sendEvent: (event: Custom) => void;
|
|
216
|
+
}>;
|
|
154
217
|
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"];
|
|
155
218
|
export type KeyboardKeys = (typeof keyboardKeys)[number];
|
|
156
219
|
export type KeyboardState = Record<KeyboardKeys, boolean>;
|
|
@@ -158,3 +221,4 @@ export declare var Yuuna: {
|
|
|
158
221
|
runEngine: RunEngineFunction;
|
|
159
222
|
STOP: typeof STOP;
|
|
160
223
|
};
|
|
224
|
+
export {};
|