yuuna-engine 0.1.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/LICENSE +21 -0
- package/README.md +96 -0
- package/lib/engine/runEngine.d.ts +2 -0
- package/lib/engine/types.d.ts +135 -0
- package/lib/index.cjs +317 -0
- package/lib/index.cjs.map +1 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +315 -0
- package/lib/index.js.map +1 -0
- package/lib/utils/Position.d.ts +4 -0
- package/lib/utils/byId.d.ts +3 -0
- package/lib/utils/constants.d.ts +6 -0
- package/lib/utils/createRecord.d.ts +1 -0
- package/lib/utils/exhaust.d.ts +1 -0
- package/lib/utils/getKeys.d.ts +1 -0
- package/lib/utils/iterateRecord.d.ts +8 -0
- package/lib/utils/not.d.ts +1 -0
- package/lib/utils/unsafe.d.ts +1 -0
- package/package.json +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lucina de Oliveira (lucy-dot-exe)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
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" width="960" height="540"></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
|
+
render: (state) => ({
|
|
39
|
+
renderables: [
|
|
40
|
+
{
|
|
41
|
+
type: "TEXT",
|
|
42
|
+
text: `${state.cookies} cookies`,
|
|
43
|
+
color: "black",
|
|
44
|
+
position: { x: 100, y: 50 },
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
type: "CIRCLE",
|
|
48
|
+
id: "cookie",
|
|
49
|
+
isClickable: true,
|
|
50
|
+
color: "brown",
|
|
51
|
+
position: { x: 50, y: 50 },
|
|
52
|
+
radius: 25,
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
}),
|
|
56
|
+
|
|
57
|
+
nextState: ({ state, event }) => {
|
|
58
|
+
if (event.tag === "CLICK" && event.id === "cookie") {
|
|
59
|
+
return { cookies: state.cookies + 1 };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return state;
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Concepts
|
|
68
|
+
|
|
69
|
+
- **Renderables** — declarative shapes drawn each frame: `RECTANGLE`,
|
|
70
|
+
`CIRCLE`, `TEXT`, and `SPRITE`. Give one an `id` plus `isClickable` /
|
|
71
|
+
`isHoverable` / `trackMouseMovement` to make it interactive.
|
|
72
|
+
- **Events** — your `nextState` function receives one `GameEvent` per call:
|
|
73
|
+
`TIME` (frame tick with `delta`), `CLICK`, `HOVER_IN`, `HOVER_OUT`, or
|
|
74
|
+
`MOUSE_MOVE`.
|
|
75
|
+
- **Keyboard** — `nextState` also receives a `keyboard` map keyed by
|
|
76
|
+
`KeyCode`-style keys (e.g. `"KeyW"`, `"ArrowLeft"`, `"Space"`), each with
|
|
77
|
+
`isPressed` / `isJustPressed` / `isJustReleased`.
|
|
78
|
+
- **Sprites** — pass a `resources` map of `{ src, size, slices }` to
|
|
79
|
+
`runEngine` to load spritesheets, then reference them by id with a
|
|
80
|
+
`SPRITE` renderable's `resourceId` and `frame`.
|
|
81
|
+
|
|
82
|
+
## Development
|
|
83
|
+
|
|
84
|
+
```sh
|
|
85
|
+
yarn install
|
|
86
|
+
yarn build # builds lib/ (npm package) and dist/bundle.js (landing page)
|
|
87
|
+
yarn watch # rebuild on change
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`dist/index.html` is the landing page — it loads `dist/bundle.js` in the
|
|
91
|
+
browser via a global `Yuuna` object and embeds a live Monaco editor so
|
|
92
|
+
visitors can edit and run a game directly on the page.
|
|
93
|
+
|
|
94
|
+
## License
|
|
95
|
+
|
|
96
|
+
MIT © [lucy-dot-exe](https://github.com/lucy-dot-exe)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
export type RectangleRenderable = {
|
|
2
|
+
type: "RECTANGLE";
|
|
3
|
+
position: {
|
|
4
|
+
x: number;
|
|
5
|
+
y: number;
|
|
6
|
+
};
|
|
7
|
+
size: {
|
|
8
|
+
width: number;
|
|
9
|
+
height: number;
|
|
10
|
+
};
|
|
11
|
+
color: string;
|
|
12
|
+
id?: string;
|
|
13
|
+
isClickable?: boolean;
|
|
14
|
+
isHoverable?: boolean;
|
|
15
|
+
trackMouseMovement?: boolean;
|
|
16
|
+
};
|
|
17
|
+
export type CircleRenderable = {
|
|
18
|
+
type: "CIRCLE";
|
|
19
|
+
position: {
|
|
20
|
+
x: number;
|
|
21
|
+
y: number;
|
|
22
|
+
};
|
|
23
|
+
radius: number;
|
|
24
|
+
color: string;
|
|
25
|
+
id?: string;
|
|
26
|
+
isClickable?: boolean;
|
|
27
|
+
isHoverable?: boolean;
|
|
28
|
+
trackMouseMovement?: boolean;
|
|
29
|
+
};
|
|
30
|
+
export type TextRenderable = {
|
|
31
|
+
type: "TEXT";
|
|
32
|
+
text: string;
|
|
33
|
+
color: string;
|
|
34
|
+
position: {
|
|
35
|
+
x: number;
|
|
36
|
+
y: number;
|
|
37
|
+
};
|
|
38
|
+
align?: {
|
|
39
|
+
x: "left" | "center" | "right";
|
|
40
|
+
y: "bottom" | "middle" | "top";
|
|
41
|
+
};
|
|
42
|
+
id?: string;
|
|
43
|
+
isClickable?: boolean;
|
|
44
|
+
isHoverable?: boolean;
|
|
45
|
+
trackMouseMovement?: boolean;
|
|
46
|
+
};
|
|
47
|
+
export type SpriteRenderable = {
|
|
48
|
+
type: "SPRITE";
|
|
49
|
+
position: {
|
|
50
|
+
x: number;
|
|
51
|
+
y: number;
|
|
52
|
+
};
|
|
53
|
+
resourceId: string;
|
|
54
|
+
frame: number;
|
|
55
|
+
scale?: number;
|
|
56
|
+
opacity?: number;
|
|
57
|
+
id?: string;
|
|
58
|
+
isClickable?: boolean;
|
|
59
|
+
isHoverable?: boolean;
|
|
60
|
+
trackMouseMovement?: boolean;
|
|
61
|
+
};
|
|
62
|
+
export type Renderable = RectangleRenderable | CircleRenderable | SpriteRenderable | TextRenderable;
|
|
63
|
+
export type TimeEvent = {
|
|
64
|
+
tag: "TIME";
|
|
65
|
+
delta: number;
|
|
66
|
+
};
|
|
67
|
+
export type ClickEvent = {
|
|
68
|
+
tag: "CLICK";
|
|
69
|
+
id?: string;
|
|
70
|
+
mouse: {
|
|
71
|
+
x: number;
|
|
72
|
+
y: number;
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
export type HoverInEvent = {
|
|
76
|
+
tag: "HOVER_IN";
|
|
77
|
+
id?: string;
|
|
78
|
+
mouse: {
|
|
79
|
+
x: number;
|
|
80
|
+
y: number;
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
export type HoverOutEvent = {
|
|
84
|
+
tag: "HOVER_OUT";
|
|
85
|
+
id?: string;
|
|
86
|
+
mouse: {
|
|
87
|
+
x: number;
|
|
88
|
+
y: number;
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
export type MouseMoveEvent = {
|
|
92
|
+
tag: "MOUSE_MOVE";
|
|
93
|
+
id?: string;
|
|
94
|
+
mouse: {
|
|
95
|
+
x: number;
|
|
96
|
+
y: number;
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
export type GameEvent = TimeEvent | ClickEvent | HoverInEvent | HoverOutEvent | MouseMoveEvent;
|
|
100
|
+
type NextStateProps<State> = {
|
|
101
|
+
state: State;
|
|
102
|
+
event: GameEvent;
|
|
103
|
+
keyboard: Record<KeyboardKeys, {
|
|
104
|
+
isPressed: boolean;
|
|
105
|
+
isJustPressed: boolean;
|
|
106
|
+
isJustReleased: boolean;
|
|
107
|
+
}>;
|
|
108
|
+
};
|
|
109
|
+
export type RunEngineProps<State> = {
|
|
110
|
+
initialState: State;
|
|
111
|
+
render: (state: State) => {
|
|
112
|
+
cursor?: "default" | "pointer";
|
|
113
|
+
renderables: Renderable[];
|
|
114
|
+
};
|
|
115
|
+
nextState: (props: NextStateProps<State>) => State;
|
|
116
|
+
resources?: Record<string, {
|
|
117
|
+
src: string;
|
|
118
|
+
size: {
|
|
119
|
+
width: number;
|
|
120
|
+
height: number;
|
|
121
|
+
};
|
|
122
|
+
slices: {
|
|
123
|
+
vertical: number;
|
|
124
|
+
horizontal: number;
|
|
125
|
+
};
|
|
126
|
+
}>;
|
|
127
|
+
};
|
|
128
|
+
export type RunEngineFunction = <State>(props: RunEngineProps<State>) => Promise<void>;
|
|
129
|
+
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"];
|
|
130
|
+
export type KeyboardKeys = (typeof keyboardKeys)[number];
|
|
131
|
+
export type KeyboardState = Record<KeyboardKeys, boolean>;
|
|
132
|
+
export declare var Yuuna: {
|
|
133
|
+
runEngine: RunEngineFunction;
|
|
134
|
+
};
|
|
135
|
+
export {};
|
package/lib/index.cjs
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function exhaust(value) {
|
|
4
|
+
throw new Error(`${value} was expected to be never.`);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const CONSTANTS = {
|
|
8
|
+
WINDOW: {
|
|
9
|
+
WIDTH: 960,
|
|
10
|
+
HEIGHT: 540,
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function unsafe(input) {
|
|
15
|
+
//@ts-ignore
|
|
16
|
+
return input;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getKeys(object) {
|
|
20
|
+
return unsafe(Object.keys(object));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function iterateRecordAsync(record, fn) {
|
|
24
|
+
const mapped = unsafe({});
|
|
25
|
+
const $mappings = getKeys(record).map(async (key) => {
|
|
26
|
+
mapped[key] = await fn({ key, value: record[key] });
|
|
27
|
+
});
|
|
28
|
+
await Promise.all($mappings);
|
|
29
|
+
return mapped;
|
|
30
|
+
}
|
|
31
|
+
function iterateRecord(record, fn) {
|
|
32
|
+
const mapped = unsafe({});
|
|
33
|
+
getKeys(record).forEach((key) => {
|
|
34
|
+
mapped[key] = fn({ key, value: record[key] });
|
|
35
|
+
});
|
|
36
|
+
return mapped;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const createRecord = (keys, fn) => {
|
|
40
|
+
const mapped = unsafe({});
|
|
41
|
+
keys.forEach((key) => {
|
|
42
|
+
mapped[key] = fn(key);
|
|
43
|
+
});
|
|
44
|
+
return mapped;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const keyboardKeys = [
|
|
48
|
+
"ControlLeft",
|
|
49
|
+
"ControlRight",
|
|
50
|
+
"AltLeft",
|
|
51
|
+
"AltRight",
|
|
52
|
+
"CapsLock",
|
|
53
|
+
"End",
|
|
54
|
+
"Delete",
|
|
55
|
+
"Tab",
|
|
56
|
+
"Space",
|
|
57
|
+
"Enter",
|
|
58
|
+
"ArrowLeft",
|
|
59
|
+
"ArrowRight",
|
|
60
|
+
"ArrowUp",
|
|
61
|
+
"ArrowDown",
|
|
62
|
+
"Numpad0",
|
|
63
|
+
"Numpad1",
|
|
64
|
+
"Numpad2",
|
|
65
|
+
"Numpad3",
|
|
66
|
+
"Numpad4",
|
|
67
|
+
"Numpad5",
|
|
68
|
+
"Numpad6",
|
|
69
|
+
"Numpad7",
|
|
70
|
+
"Numpad8",
|
|
71
|
+
"Numpad9",
|
|
72
|
+
"Digit0",
|
|
73
|
+
"Digit1",
|
|
74
|
+
"Digit2",
|
|
75
|
+
"Digit3",
|
|
76
|
+
"Digit4",
|
|
77
|
+
"Digit5",
|
|
78
|
+
"Digit6",
|
|
79
|
+
"Digit7",
|
|
80
|
+
"Digit8",
|
|
81
|
+
"Digit9",
|
|
82
|
+
"KeyA",
|
|
83
|
+
"KeyB",
|
|
84
|
+
"KeyC",
|
|
85
|
+
"KeyD",
|
|
86
|
+
"KeyE",
|
|
87
|
+
"KeyF",
|
|
88
|
+
"KeyG",
|
|
89
|
+
"KeyH",
|
|
90
|
+
"KeyI",
|
|
91
|
+
"KeyJ",
|
|
92
|
+
"KeyK",
|
|
93
|
+
"KeyL",
|
|
94
|
+
"KeyM",
|
|
95
|
+
"KeyN",
|
|
96
|
+
"KeyO",
|
|
97
|
+
"KeyP",
|
|
98
|
+
"KeyQ",
|
|
99
|
+
"KeyR",
|
|
100
|
+
"KeyS",
|
|
101
|
+
"KeyT",
|
|
102
|
+
"KeyU",
|
|
103
|
+
"KeyV",
|
|
104
|
+
"KeyW",
|
|
105
|
+
"KeyX",
|
|
106
|
+
"KeyY",
|
|
107
|
+
"KeyZ",
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
let resetCanvas = null;
|
|
111
|
+
const runEngine = async (props) => {
|
|
112
|
+
resetCanvas?.();
|
|
113
|
+
const canvas = window.document.getElementById("yuuna");
|
|
114
|
+
if (canvas === null) {
|
|
115
|
+
throw new Error('No HTML element found with id "yuuna"');
|
|
116
|
+
}
|
|
117
|
+
if (!(canvas instanceof HTMLCanvasElement)) {
|
|
118
|
+
throw new Error('No Canvas element found with id "yuuna"');
|
|
119
|
+
}
|
|
120
|
+
const context = canvas.getContext("2d");
|
|
121
|
+
if (context === null) {
|
|
122
|
+
throw new Error("Failed to get context from canvas");
|
|
123
|
+
}
|
|
124
|
+
let state = props.initialState;
|
|
125
|
+
const resources = props.resources ?? {};
|
|
126
|
+
const resourceById = await iterateRecordAsync(resources, async ({ value }) => new Promise((resolve) => {
|
|
127
|
+
const image = new Image();
|
|
128
|
+
image.src = value.src;
|
|
129
|
+
image.onload = function () {
|
|
130
|
+
resolve({
|
|
131
|
+
image,
|
|
132
|
+
size: {
|
|
133
|
+
width: value.size.width / value.slices.horizontal,
|
|
134
|
+
height: value.size.height / value.slices.vertical,
|
|
135
|
+
},
|
|
136
|
+
slices: value.slices,
|
|
137
|
+
});
|
|
138
|
+
};
|
|
139
|
+
}));
|
|
140
|
+
context.imageSmoothingEnabled = false;
|
|
141
|
+
function getFocusedElement(position, r) {
|
|
142
|
+
const isNonInteractable = r.type === "TEXT" ||
|
|
143
|
+
((r.isHoverable === undefined || !r.isHoverable) &&
|
|
144
|
+
(r.isClickable === undefined || !r.isClickable));
|
|
145
|
+
if (isNonInteractable) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
if (r.type === "CIRCLE") {
|
|
149
|
+
const delta = {
|
|
150
|
+
x: Math.abs(position.x - r.position.x),
|
|
151
|
+
y: Math.abs(position.y - r.position.y),
|
|
152
|
+
};
|
|
153
|
+
const distance = Math.sqrt(delta.x * delta.x + delta.y * delta.y);
|
|
154
|
+
const isHovered = distance < r.radius;
|
|
155
|
+
return isHovered;
|
|
156
|
+
}
|
|
157
|
+
if (r.type === "RECTANGLE") {
|
|
158
|
+
const topLeft = { x: r.position.x, y: r.position.y };
|
|
159
|
+
const bottomRight = {
|
|
160
|
+
x: r.position.x + r.size.width,
|
|
161
|
+
y: r.position.y + r.size.height,
|
|
162
|
+
};
|
|
163
|
+
const isInsideX = position.x > topLeft.x && position.x < bottomRight.x;
|
|
164
|
+
const isInsideY = position.y > topLeft.y && position.y < bottomRight.y;
|
|
165
|
+
const isHovered = isInsideX && isInsideY;
|
|
166
|
+
return isHovered;
|
|
167
|
+
}
|
|
168
|
+
if (r.type === "SPRITE") {
|
|
169
|
+
const topLeft = { x: r.position.x, y: r.position.y };
|
|
170
|
+
const resource = resourceById[r.resourceId];
|
|
171
|
+
const { scale = 1 } = r;
|
|
172
|
+
const bottomRight = {
|
|
173
|
+
x: r.position.x + resource.size.width * scale,
|
|
174
|
+
y: r.position.y + resource.size.height * scale,
|
|
175
|
+
};
|
|
176
|
+
const isInsideX = position.x > topLeft.x && position.x < bottomRight.x;
|
|
177
|
+
const isInsideY = position.y > topLeft.y && position.y < bottomRight.y;
|
|
178
|
+
const isHovered = isInsideX && isInsideY;
|
|
179
|
+
return isHovered;
|
|
180
|
+
}
|
|
181
|
+
exhaust(r);
|
|
182
|
+
}
|
|
183
|
+
const updateState = (updateFn) => {
|
|
184
|
+
state = updateFn(state);
|
|
185
|
+
};
|
|
186
|
+
let lastFrame = Date.now();
|
|
187
|
+
let hoveredId = null;
|
|
188
|
+
const events = [];
|
|
189
|
+
canvas.addEventListener("click", (ev) => {
|
|
190
|
+
const mouse = { x: ev.offsetX, y: ev.offsetY };
|
|
191
|
+
if (hoveredId === null)
|
|
192
|
+
return;
|
|
193
|
+
const { renderables } = props.render(state);
|
|
194
|
+
const hovered = renderables.find((e) => e.id === hoveredId);
|
|
195
|
+
if (hovered !== undefined && hovered.isClickable) {
|
|
196
|
+
events.push({ tag: "CLICK", id: hovered.id, mouse });
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
const initialState = {
|
|
200
|
+
keyboardState: createRecord(keyboardKeys, () => false),
|
|
201
|
+
};
|
|
202
|
+
let previousState = {
|
|
203
|
+
keyboardState: { ...initialState.keyboardState },
|
|
204
|
+
};
|
|
205
|
+
const currentState = {
|
|
206
|
+
keyboardState: { ...initialState.keyboardState },
|
|
207
|
+
};
|
|
208
|
+
document.addEventListener("keydown", (event) => {
|
|
209
|
+
const pressedKey = keyboardKeys.find((key) => key === event.code);
|
|
210
|
+
if (pressedKey !== undefined) {
|
|
211
|
+
currentState.keyboardState[pressedKey] = true;
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
document.addEventListener("keyup", (event) => {
|
|
215
|
+
const releasedKey = keyboardKeys.find((key) => key === event.code);
|
|
216
|
+
if (releasedKey !== undefined) {
|
|
217
|
+
currentState.keyboardState[releasedKey] = false;
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
canvas.addEventListener("mousemove", (ev) => {
|
|
221
|
+
const mouse = { x: ev.offsetX, y: ev.offsetY };
|
|
222
|
+
const { renderables } = props.render(state);
|
|
223
|
+
const hovered = [...renderables]
|
|
224
|
+
.reverse()
|
|
225
|
+
.find((r) => getFocusedElement(mouse, r));
|
|
226
|
+
if (hovered !== undefined && hovered.isHoverable) {
|
|
227
|
+
events.push({ tag: "HOVER_IN", id: hovered.id, mouse });
|
|
228
|
+
}
|
|
229
|
+
if (hoveredId !== null) {
|
|
230
|
+
const lastHovered = renderables.find((r) => r.id === hoveredId);
|
|
231
|
+
if (lastHovered !== undefined && lastHovered.id !== hovered?.id) {
|
|
232
|
+
events.push({ tag: "HOVER_OUT", id: lastHovered.id, mouse });
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
hoveredId = hovered === undefined ? null : hovered.id ?? null;
|
|
236
|
+
});
|
|
237
|
+
canvas.addEventListener("mousemove", (ev) => {
|
|
238
|
+
const mouse = { x: ev.offsetX, y: ev.offsetY };
|
|
239
|
+
if (hoveredId === null) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const { renderables } = props.render(state);
|
|
243
|
+
const hovered = renderables.find((r) => r.id === hoveredId);
|
|
244
|
+
if (hovered !== undefined && hovered.trackMouseMovement) {
|
|
245
|
+
events.push({ tag: "MOUSE_MOVE", mouse, id: hovered.id });
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
context.imageSmoothingEnabled = false;
|
|
249
|
+
const intervalId = setInterval(() => {
|
|
250
|
+
const now = Date.now();
|
|
251
|
+
const delta = now - lastFrame;
|
|
252
|
+
events.push({ tag: "TIME", delta: delta });
|
|
253
|
+
for (const event of events) {
|
|
254
|
+
updateState((state) => props.nextState({
|
|
255
|
+
state,
|
|
256
|
+
event,
|
|
257
|
+
keyboard: iterateRecord(previousState.keyboardState, ({ key, value: previouslyPressed }) => {
|
|
258
|
+
const isPressed = currentState.keyboardState[key];
|
|
259
|
+
return {
|
|
260
|
+
isPressed,
|
|
261
|
+
isJustPressed: isPressed && !previouslyPressed,
|
|
262
|
+
isJustReleased: !isPressed && previouslyPressed,
|
|
263
|
+
};
|
|
264
|
+
}),
|
|
265
|
+
}));
|
|
266
|
+
}
|
|
267
|
+
events.splice(0, events.length);
|
|
268
|
+
context.clearRect(0, 0, CONSTANTS.WINDOW.WIDTH, CONSTANTS.WINDOW.HEIGHT);
|
|
269
|
+
const { cursor, renderables } = props.render(state);
|
|
270
|
+
canvas.style.cursor = cursor ?? "default";
|
|
271
|
+
for (const renderable of renderables) {
|
|
272
|
+
if (renderable.type === "RECTANGLE") {
|
|
273
|
+
context.fillStyle = renderable.color;
|
|
274
|
+
context.fillRect(renderable.position.x, renderable.position.y, renderable.size.width, renderable.size.height);
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
if (renderable.type === "CIRCLE") {
|
|
278
|
+
context.fillStyle = renderable.color;
|
|
279
|
+
context.beginPath();
|
|
280
|
+
context.arc(renderable.position.x, renderable.position.y, renderable.radius, 0, 2 * Math.PI);
|
|
281
|
+
context.fill();
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
if (renderable.type === "TEXT") {
|
|
285
|
+
const { color, position: { x, y }, text, align, } = renderable;
|
|
286
|
+
context.fillStyle = color;
|
|
287
|
+
context.font = "30px Arial";
|
|
288
|
+
context.textAlign = align?.x ?? "left";
|
|
289
|
+
context.textBaseline = align?.y ?? "top";
|
|
290
|
+
context.fillText(text, x, y);
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
if (renderable.type === "SPRITE") {
|
|
294
|
+
const { scale = 1, opacity = 1 } = renderable;
|
|
295
|
+
const resource = resourceById[renderable.resourceId];
|
|
296
|
+
const frame = {
|
|
297
|
+
x: renderable.frame % resource.slices.horizontal,
|
|
298
|
+
y: Math.floor(renderable.frame / resource.slices.vertical) %
|
|
299
|
+
resource.slices.vertical,
|
|
300
|
+
};
|
|
301
|
+
context.globalAlpha = opacity;
|
|
302
|
+
context.drawImage(resource.image, frame.x * resource.size.width, frame.y * resource.size.height, resource.size.width, resource.size.height, renderable.position.x, renderable.position.y, resource.size.width * scale, resource.size.height * scale);
|
|
303
|
+
context.globalAlpha = 1;
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
exhaust(renderable);
|
|
307
|
+
}
|
|
308
|
+
lastFrame = now;
|
|
309
|
+
previousState.keyboardState = { ...currentState.keyboardState };
|
|
310
|
+
}, 0);
|
|
311
|
+
resetCanvas = () => {
|
|
312
|
+
clearInterval(intervalId);
|
|
313
|
+
};
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
exports.runEngine = runEngine;
|
|
317
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
package/lib/index.d.ts
ADDED
package/lib/index.js
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
function exhaust(value) {
|
|
2
|
+
throw new Error(`${value} was expected to be never.`);
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
const CONSTANTS = {
|
|
6
|
+
WINDOW: {
|
|
7
|
+
WIDTH: 960,
|
|
8
|
+
HEIGHT: 540,
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function unsafe(input) {
|
|
13
|
+
//@ts-ignore
|
|
14
|
+
return input;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getKeys(object) {
|
|
18
|
+
return unsafe(Object.keys(object));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function iterateRecordAsync(record, fn) {
|
|
22
|
+
const mapped = unsafe({});
|
|
23
|
+
const $mappings = getKeys(record).map(async (key) => {
|
|
24
|
+
mapped[key] = await fn({ key, value: record[key] });
|
|
25
|
+
});
|
|
26
|
+
await Promise.all($mappings);
|
|
27
|
+
return mapped;
|
|
28
|
+
}
|
|
29
|
+
function iterateRecord(record, fn) {
|
|
30
|
+
const mapped = unsafe({});
|
|
31
|
+
getKeys(record).forEach((key) => {
|
|
32
|
+
mapped[key] = fn({ key, value: record[key] });
|
|
33
|
+
});
|
|
34
|
+
return mapped;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const createRecord = (keys, fn) => {
|
|
38
|
+
const mapped = unsafe({});
|
|
39
|
+
keys.forEach((key) => {
|
|
40
|
+
mapped[key] = fn(key);
|
|
41
|
+
});
|
|
42
|
+
return mapped;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const keyboardKeys = [
|
|
46
|
+
"ControlLeft",
|
|
47
|
+
"ControlRight",
|
|
48
|
+
"AltLeft",
|
|
49
|
+
"AltRight",
|
|
50
|
+
"CapsLock",
|
|
51
|
+
"End",
|
|
52
|
+
"Delete",
|
|
53
|
+
"Tab",
|
|
54
|
+
"Space",
|
|
55
|
+
"Enter",
|
|
56
|
+
"ArrowLeft",
|
|
57
|
+
"ArrowRight",
|
|
58
|
+
"ArrowUp",
|
|
59
|
+
"ArrowDown",
|
|
60
|
+
"Numpad0",
|
|
61
|
+
"Numpad1",
|
|
62
|
+
"Numpad2",
|
|
63
|
+
"Numpad3",
|
|
64
|
+
"Numpad4",
|
|
65
|
+
"Numpad5",
|
|
66
|
+
"Numpad6",
|
|
67
|
+
"Numpad7",
|
|
68
|
+
"Numpad8",
|
|
69
|
+
"Numpad9",
|
|
70
|
+
"Digit0",
|
|
71
|
+
"Digit1",
|
|
72
|
+
"Digit2",
|
|
73
|
+
"Digit3",
|
|
74
|
+
"Digit4",
|
|
75
|
+
"Digit5",
|
|
76
|
+
"Digit6",
|
|
77
|
+
"Digit7",
|
|
78
|
+
"Digit8",
|
|
79
|
+
"Digit9",
|
|
80
|
+
"KeyA",
|
|
81
|
+
"KeyB",
|
|
82
|
+
"KeyC",
|
|
83
|
+
"KeyD",
|
|
84
|
+
"KeyE",
|
|
85
|
+
"KeyF",
|
|
86
|
+
"KeyG",
|
|
87
|
+
"KeyH",
|
|
88
|
+
"KeyI",
|
|
89
|
+
"KeyJ",
|
|
90
|
+
"KeyK",
|
|
91
|
+
"KeyL",
|
|
92
|
+
"KeyM",
|
|
93
|
+
"KeyN",
|
|
94
|
+
"KeyO",
|
|
95
|
+
"KeyP",
|
|
96
|
+
"KeyQ",
|
|
97
|
+
"KeyR",
|
|
98
|
+
"KeyS",
|
|
99
|
+
"KeyT",
|
|
100
|
+
"KeyU",
|
|
101
|
+
"KeyV",
|
|
102
|
+
"KeyW",
|
|
103
|
+
"KeyX",
|
|
104
|
+
"KeyY",
|
|
105
|
+
"KeyZ",
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
let resetCanvas = null;
|
|
109
|
+
const runEngine = async (props) => {
|
|
110
|
+
resetCanvas?.();
|
|
111
|
+
const canvas = window.document.getElementById("yuuna");
|
|
112
|
+
if (canvas === null) {
|
|
113
|
+
throw new Error('No HTML element found with id "yuuna"');
|
|
114
|
+
}
|
|
115
|
+
if (!(canvas instanceof HTMLCanvasElement)) {
|
|
116
|
+
throw new Error('No Canvas element found with id "yuuna"');
|
|
117
|
+
}
|
|
118
|
+
const context = canvas.getContext("2d");
|
|
119
|
+
if (context === null) {
|
|
120
|
+
throw new Error("Failed to get context from canvas");
|
|
121
|
+
}
|
|
122
|
+
let state = props.initialState;
|
|
123
|
+
const resources = props.resources ?? {};
|
|
124
|
+
const resourceById = await iterateRecordAsync(resources, async ({ value }) => new Promise((resolve) => {
|
|
125
|
+
const image = new Image();
|
|
126
|
+
image.src = value.src;
|
|
127
|
+
image.onload = function () {
|
|
128
|
+
resolve({
|
|
129
|
+
image,
|
|
130
|
+
size: {
|
|
131
|
+
width: value.size.width / value.slices.horizontal,
|
|
132
|
+
height: value.size.height / value.slices.vertical,
|
|
133
|
+
},
|
|
134
|
+
slices: value.slices,
|
|
135
|
+
});
|
|
136
|
+
};
|
|
137
|
+
}));
|
|
138
|
+
context.imageSmoothingEnabled = false;
|
|
139
|
+
function getFocusedElement(position, r) {
|
|
140
|
+
const isNonInteractable = r.type === "TEXT" ||
|
|
141
|
+
((r.isHoverable === undefined || !r.isHoverable) &&
|
|
142
|
+
(r.isClickable === undefined || !r.isClickable));
|
|
143
|
+
if (isNonInteractable) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
if (r.type === "CIRCLE") {
|
|
147
|
+
const delta = {
|
|
148
|
+
x: Math.abs(position.x - r.position.x),
|
|
149
|
+
y: Math.abs(position.y - r.position.y),
|
|
150
|
+
};
|
|
151
|
+
const distance = Math.sqrt(delta.x * delta.x + delta.y * delta.y);
|
|
152
|
+
const isHovered = distance < r.radius;
|
|
153
|
+
return isHovered;
|
|
154
|
+
}
|
|
155
|
+
if (r.type === "RECTANGLE") {
|
|
156
|
+
const topLeft = { x: r.position.x, y: r.position.y };
|
|
157
|
+
const bottomRight = {
|
|
158
|
+
x: r.position.x + r.size.width,
|
|
159
|
+
y: r.position.y + r.size.height,
|
|
160
|
+
};
|
|
161
|
+
const isInsideX = position.x > topLeft.x && position.x < bottomRight.x;
|
|
162
|
+
const isInsideY = position.y > topLeft.y && position.y < bottomRight.y;
|
|
163
|
+
const isHovered = isInsideX && isInsideY;
|
|
164
|
+
return isHovered;
|
|
165
|
+
}
|
|
166
|
+
if (r.type === "SPRITE") {
|
|
167
|
+
const topLeft = { x: r.position.x, y: r.position.y };
|
|
168
|
+
const resource = resourceById[r.resourceId];
|
|
169
|
+
const { scale = 1 } = r;
|
|
170
|
+
const bottomRight = {
|
|
171
|
+
x: r.position.x + resource.size.width * scale,
|
|
172
|
+
y: r.position.y + resource.size.height * scale,
|
|
173
|
+
};
|
|
174
|
+
const isInsideX = position.x > topLeft.x && position.x < bottomRight.x;
|
|
175
|
+
const isInsideY = position.y > topLeft.y && position.y < bottomRight.y;
|
|
176
|
+
const isHovered = isInsideX && isInsideY;
|
|
177
|
+
return isHovered;
|
|
178
|
+
}
|
|
179
|
+
exhaust(r);
|
|
180
|
+
}
|
|
181
|
+
const updateState = (updateFn) => {
|
|
182
|
+
state = updateFn(state);
|
|
183
|
+
};
|
|
184
|
+
let lastFrame = Date.now();
|
|
185
|
+
let hoveredId = null;
|
|
186
|
+
const events = [];
|
|
187
|
+
canvas.addEventListener("click", (ev) => {
|
|
188
|
+
const mouse = { x: ev.offsetX, y: ev.offsetY };
|
|
189
|
+
if (hoveredId === null)
|
|
190
|
+
return;
|
|
191
|
+
const { renderables } = props.render(state);
|
|
192
|
+
const hovered = renderables.find((e) => e.id === hoveredId);
|
|
193
|
+
if (hovered !== undefined && hovered.isClickable) {
|
|
194
|
+
events.push({ tag: "CLICK", id: hovered.id, mouse });
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
const initialState = {
|
|
198
|
+
keyboardState: createRecord(keyboardKeys, () => false),
|
|
199
|
+
};
|
|
200
|
+
let previousState = {
|
|
201
|
+
keyboardState: { ...initialState.keyboardState },
|
|
202
|
+
};
|
|
203
|
+
const currentState = {
|
|
204
|
+
keyboardState: { ...initialState.keyboardState },
|
|
205
|
+
};
|
|
206
|
+
document.addEventListener("keydown", (event) => {
|
|
207
|
+
const pressedKey = keyboardKeys.find((key) => key === event.code);
|
|
208
|
+
if (pressedKey !== undefined) {
|
|
209
|
+
currentState.keyboardState[pressedKey] = true;
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
document.addEventListener("keyup", (event) => {
|
|
213
|
+
const releasedKey = keyboardKeys.find((key) => key === event.code);
|
|
214
|
+
if (releasedKey !== undefined) {
|
|
215
|
+
currentState.keyboardState[releasedKey] = false;
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
canvas.addEventListener("mousemove", (ev) => {
|
|
219
|
+
const mouse = { x: ev.offsetX, y: ev.offsetY };
|
|
220
|
+
const { renderables } = props.render(state);
|
|
221
|
+
const hovered = [...renderables]
|
|
222
|
+
.reverse()
|
|
223
|
+
.find((r) => getFocusedElement(mouse, r));
|
|
224
|
+
if (hovered !== undefined && hovered.isHoverable) {
|
|
225
|
+
events.push({ tag: "HOVER_IN", id: hovered.id, mouse });
|
|
226
|
+
}
|
|
227
|
+
if (hoveredId !== null) {
|
|
228
|
+
const lastHovered = renderables.find((r) => r.id === hoveredId);
|
|
229
|
+
if (lastHovered !== undefined && lastHovered.id !== hovered?.id) {
|
|
230
|
+
events.push({ tag: "HOVER_OUT", id: lastHovered.id, mouse });
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
hoveredId = hovered === undefined ? null : hovered.id ?? null;
|
|
234
|
+
});
|
|
235
|
+
canvas.addEventListener("mousemove", (ev) => {
|
|
236
|
+
const mouse = { x: ev.offsetX, y: ev.offsetY };
|
|
237
|
+
if (hoveredId === null) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const { renderables } = props.render(state);
|
|
241
|
+
const hovered = renderables.find((r) => r.id === hoveredId);
|
|
242
|
+
if (hovered !== undefined && hovered.trackMouseMovement) {
|
|
243
|
+
events.push({ tag: "MOUSE_MOVE", mouse, id: hovered.id });
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
context.imageSmoothingEnabled = false;
|
|
247
|
+
const intervalId = setInterval(() => {
|
|
248
|
+
const now = Date.now();
|
|
249
|
+
const delta = now - lastFrame;
|
|
250
|
+
events.push({ tag: "TIME", delta: delta });
|
|
251
|
+
for (const event of events) {
|
|
252
|
+
updateState((state) => props.nextState({
|
|
253
|
+
state,
|
|
254
|
+
event,
|
|
255
|
+
keyboard: iterateRecord(previousState.keyboardState, ({ key, value: previouslyPressed }) => {
|
|
256
|
+
const isPressed = currentState.keyboardState[key];
|
|
257
|
+
return {
|
|
258
|
+
isPressed,
|
|
259
|
+
isJustPressed: isPressed && !previouslyPressed,
|
|
260
|
+
isJustReleased: !isPressed && previouslyPressed,
|
|
261
|
+
};
|
|
262
|
+
}),
|
|
263
|
+
}));
|
|
264
|
+
}
|
|
265
|
+
events.splice(0, events.length);
|
|
266
|
+
context.clearRect(0, 0, CONSTANTS.WINDOW.WIDTH, CONSTANTS.WINDOW.HEIGHT);
|
|
267
|
+
const { cursor, renderables } = props.render(state);
|
|
268
|
+
canvas.style.cursor = cursor ?? "default";
|
|
269
|
+
for (const renderable of renderables) {
|
|
270
|
+
if (renderable.type === "RECTANGLE") {
|
|
271
|
+
context.fillStyle = renderable.color;
|
|
272
|
+
context.fillRect(renderable.position.x, renderable.position.y, renderable.size.width, renderable.size.height);
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (renderable.type === "CIRCLE") {
|
|
276
|
+
context.fillStyle = renderable.color;
|
|
277
|
+
context.beginPath();
|
|
278
|
+
context.arc(renderable.position.x, renderable.position.y, renderable.radius, 0, 2 * Math.PI);
|
|
279
|
+
context.fill();
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
if (renderable.type === "TEXT") {
|
|
283
|
+
const { color, position: { x, y }, text, align, } = renderable;
|
|
284
|
+
context.fillStyle = color;
|
|
285
|
+
context.font = "30px Arial";
|
|
286
|
+
context.textAlign = align?.x ?? "left";
|
|
287
|
+
context.textBaseline = align?.y ?? "top";
|
|
288
|
+
context.fillText(text, x, y);
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
if (renderable.type === "SPRITE") {
|
|
292
|
+
const { scale = 1, opacity = 1 } = renderable;
|
|
293
|
+
const resource = resourceById[renderable.resourceId];
|
|
294
|
+
const frame = {
|
|
295
|
+
x: renderable.frame % resource.slices.horizontal,
|
|
296
|
+
y: Math.floor(renderable.frame / resource.slices.vertical) %
|
|
297
|
+
resource.slices.vertical,
|
|
298
|
+
};
|
|
299
|
+
context.globalAlpha = opacity;
|
|
300
|
+
context.drawImage(resource.image, frame.x * resource.size.width, frame.y * resource.size.height, resource.size.width, resource.size.height, renderable.position.x, renderable.position.y, resource.size.width * scale, resource.size.height * scale);
|
|
301
|
+
context.globalAlpha = 1;
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
exhaust(renderable);
|
|
305
|
+
}
|
|
306
|
+
lastFrame = now;
|
|
307
|
+
previousState.keyboardState = { ...currentState.keyboardState };
|
|
308
|
+
}, 0);
|
|
309
|
+
resetCanvas = () => {
|
|
310
|
+
clearInterval(intervalId);
|
|
311
|
+
};
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
export { runEngine };
|
|
315
|
+
//# sourceMappingURL=index.js.map
|
package/lib/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const createRecord: <Key extends string, T>(keys: readonly Key[], fn: (key: Key) => T) => Record<Key, T>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function exhaust(value: never): never;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function getKeys<Keys extends string>(object: Record<Keys, any>): Keys[];
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function iterateRecordAsync<Key extends string, T, Item>(record: Record<Key, T>, fn: (item: {
|
|
2
|
+
key: Key;
|
|
3
|
+
value: T;
|
|
4
|
+
}) => Item | Promise<Item>): Promise<Record<Key, Item>>;
|
|
5
|
+
export declare function iterateRecord<Key extends string, T, Item>(record: Record<Key, T>, fn: (item: {
|
|
6
|
+
key: Key;
|
|
7
|
+
value: T;
|
|
8
|
+
}) => Item): Record<Key, Item>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
declare function not<T>(fn: (value: T) => boolean): (value: T) => boolean;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function unsafe<Input, Output>(input: Input): Output;
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "yuuna-engine",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A lightweight, state-machine-based TypeScript game engine for quick prototypes. Runs directly in the browser.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./lib/index.cjs",
|
|
7
|
+
"module": "./lib/index.js",
|
|
8
|
+
"types": "./lib/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./lib/index.d.ts",
|
|
12
|
+
"import": "./lib/index.js",
|
|
13
|
+
"require": "./lib/index.cjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"lib"
|
|
18
|
+
],
|
|
19
|
+
"sideEffects": false,
|
|
20
|
+
"keywords": [
|
|
21
|
+
"game-engine",
|
|
22
|
+
"canvas",
|
|
23
|
+
"typescript",
|
|
24
|
+
"state-machine",
|
|
25
|
+
"2d",
|
|
26
|
+
"prototyping",
|
|
27
|
+
"browser"
|
|
28
|
+
],
|
|
29
|
+
"author": "lucy-dot-exe <lucinadotexe@gmail.com>",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/lucy-dot-exe/yuuna.git"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://github.com/lucy-dot-exe/yuuna#readme",
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/lucy-dot-exe/yuuna/issues"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "node scripts/generate-playground-assets.mjs && rollup -c",
|
|
41
|
+
"watch": "node scripts/generate-playground-assets.mjs && rollup -c -w",
|
|
42
|
+
"prepublishOnly": "npm run build"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"nodemon": "^3.1.0",
|
|
46
|
+
"rollup": "^4.17.2",
|
|
47
|
+
"rollup-plugin-typescript2": "^0.36.0",
|
|
48
|
+
"ts-node": "^10.9.2",
|
|
49
|
+
"typescript": "^5.4.5"
|
|
50
|
+
}
|
|
51
|
+
}
|