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 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
- A lightweight, state-machine-based TypeScript game engine built for quick
8
- prototypes — drop it into a page and it's running, no editor or build step
9
- required. You describe your game as a `state`, a `render(state)` function,
10
- and a `nextState({ state, event, keyboard })` function — Yuuna owns the
11
- render loop, input handling, and canvas drawing. It's scratch paper for game
12
- ideas, not a replacement for Godot or Unity.
13
-
14
- ## Install
15
-
16
- ```sh
17
- npm install yuuna-engine
18
- ```
19
-
20
- ## Quick start
21
-
22
- Add a canvas with `id="yuuna"` to your page:
23
-
24
- ```html
25
- <canvas id="yuuna"></canvas>
26
- ```
27
-
28
- Then describe your game as state + render + nextState:
29
-
30
- ```ts
31
- import { runEngine } from "yuuna-engine";
32
-
33
- type GameState = { cookies: number };
34
-
35
- runEngine<GameState>({
36
- initialState: { cookies: 0 },
37
-
38
- // Optional — size and color the canvas from code instead of HTML/CSS
39
- canvas: { width: 960, height: 540, backgroundColor: "#0d1831" },
40
-
41
- render: (state) => ({
42
- renderables: [
43
- {
44
- type: "TEXT",
45
- text: `${state.cookies} cookies`,
46
- color: "black",
47
- position: { x: 100, y: 50 },
48
- },
49
- {
50
- type: "CIRCLE",
51
- id: "cookie",
52
- isClickable: true,
53
- color: "brown",
54
- position: { x: 50, y: 50 },
55
- radius: 25,
56
- },
57
- ],
58
- }),
59
-
60
- nextState: ({ state, event }) => {
61
- if (event.tag === "CLICK" && event.id === "cookie") {
62
- return { cookies: state.cookies + 1 };
63
- }
64
-
65
- return state;
66
- },
67
- });
68
- ```
69
-
70
- ## Concepts
71
-
72
- - **Renderables** — declarative shapes drawn each frame: `RECTANGLE`,
73
- `CIRCLE`, `TEXT`, `SPRITE`, and `LINE`. Give one an `id` plus
74
- `isClickable` / `isHoverable` / `trackMouseMovement` to make it
75
- interactive.
76
- - **Events** — your `nextState` function receives one `GameEvent` per call:
77
- `TIME` (frame tick with `delta`), `CLICK`, `HOVER_IN`, `HOVER_OUT`, or
78
- `MOUSE_MOVE`.
79
- - **Keyboard** — `nextState` also receives a `keyboard` map keyed by
80
- `KeyCode`-style keys (e.g. `"KeyW"`, `"ArrowLeft"`, `"Space"`), each with
81
- `isPressed` / `isJustPressed` / `isJustReleased`.
82
- - **Sprites** — pass a `resources` map of `{ src, size, slices }` to
83
- `runEngine` to load spritesheets, then reference them by id with a
84
- `SPRITE` renderable's `resourceId` and `frame`. Set `flipX: true` to
85
- mirror a sprite horizontally — useful when the art is drawn facing one
86
- direction but needs to move the other way.
87
- - **Canvas** — pass `canvas: { width, height, backgroundColor }` to
88
- `runEngine` to size and color the canvas from code. All three are
89
- optional; anything you don't set falls back to the canvas element's
90
- existing HTML/CSS.
91
- - **Mechanics** — `nextState` can also be an array of small
92
- `NextStateFunction`s instead of one big function. Each one is run in
93
- order for every event, and can return:
94
- - a new state, to update to
95
- - `undefined` (or no `return` at all) — no change, but the rest of the
96
- list still runs, so a guard can just be `if (...) return;`
97
- - `STOP` (imported from `yuuna-engine`) — no change, and the rest of
98
- the list is skipped for this event, so a shared rule (like "nothing
99
- happens once the game is over") only needs to be written once
100
-
101
- ```ts
102
- import { runEngine, STOP, type NextStateFunction } from "yuuna-engine";
103
-
104
- const freezeOnGameOver: NextStateFunction<GameState> = ({ state }) => {
105
- if (state.lives <= 0) return STOP;
106
- };
107
-
108
- const moveEnemies: NextStateFunction<GameState> = ({ state, event }) => {
109
- if (event.tag === "TIME") {
110
- return { ...state, enemies: move(state.enemies, event.delta) };
111
- }
112
- };
113
-
114
- runEngine<GameState>({
115
- initialState,
116
- render,
117
- nextState: [freezeOnGameOver, moveEnemies /* ... */],
118
- });
119
- ```
120
-
121
- ## Development
122
-
123
- ```sh
124
- yarn install
125
- yarn build # builds lib/ (npm package) and dist/bundle.js (landing page)
126
- yarn watch # rebuild on change
127
- ```
128
-
129
- `dist/index.html` is the landing page — it loads `dist/bundle.js` in the
130
- browser via a global `Yuuna` object and embeds a live Monaco editor so
131
- visitors can edit and run a game directly on the page.
132
-
133
- ## License
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)
@@ -1,4 +1,18 @@
1
- export type RectangleRenderable = {
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
- id?: string;
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 Renderable = RectangleRenderable | CircleRenderable | SpriteRenderable | TextRenderable | LineRenderable;
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 GameEvent = TimeEvent | ClickEvent | HoverInEvent | HoverOutEvent | MouseMoveEvent;
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<void>;
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 {};