@origingame/create-game 0.1.0 → 0.2.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.
Files changed (58) hide show
  1. package/README.md +15 -7
  2. package/archetypes/README.md +4 -2
  3. package/archetypes/idle/gitignore +8 -0
  4. package/archetypes/idle/package.json +1 -1
  5. package/archetypes/idle/project.ogt +1 -1
  6. package/archetypes/match3/gitignore +8 -0
  7. package/archetypes/match3/package.json +1 -1
  8. package/archetypes/match3/project.ogt +1 -1
  9. package/archetypes/rhythm/gitignore +8 -0
  10. package/archetypes/rhythm/package.json +1 -1
  11. package/archetypes/rhythm/project.ogt +1 -1
  12. package/archetypes/roguelike/gitignore +8 -0
  13. package/archetypes/roguelike/package.json +1 -1
  14. package/archetypes/roguelike/project.ogt +1 -1
  15. package/archetypes/runner/gitignore +8 -0
  16. package/archetypes/runner/package.json +1 -1
  17. package/archetypes/runner/project.ogt +1 -1
  18. package/archetypes/third-person/gitignore +8 -0
  19. package/archetypes/third-person/package.json +1 -1
  20. package/archetypes/third-person/project.ogt +1 -1
  21. package/archetypes/topdown/gitignore +8 -0
  22. package/archetypes/topdown/package.json +1 -1
  23. package/archetypes/topdown/project.ogt +1 -1
  24. package/archetypes/tower-defense/gitignore +8 -0
  25. package/archetypes/tower-defense/package.json +1 -1
  26. package/archetypes/tower-defense/project.ogt +1 -1
  27. package/bin/create.ts +80 -16
  28. package/package.json +5 -4
  29. package/template/gitignore +8 -0
  30. package/template/package.json +1 -1
  31. package/template/project.ogt +1 -1
  32. package/template-2d/.origin/setup +14 -0
  33. package/template-2d/LICENSES.md +14 -0
  34. package/template-2d/README.md +39 -0
  35. package/template-2d/assets/audio/coin.wav +0 -0
  36. package/template-2d/assets/audio/jump.wav +0 -0
  37. package/template-2d/assets/fonts/OFL.txt +93 -0
  38. package/template-2d/assets/fonts/ui.ttf +0 -0
  39. package/template-2d/assets/fonts/ui.ttf.ogt +11 -0
  40. package/template-2d/assets/looks/origin-game-2d.ogt +12 -0
  41. package/template-2d/assets/maps/level.ogt +112 -0
  42. package/template-2d/assets/sprites/coin.ogt +11 -0
  43. package/template-2d/assets/sprites/coin.png +0 -0
  44. package/template-2d/assets/sprites/coin.png.ogt +7 -0
  45. package/template-2d/assets/sprites/courier.ogt +34 -0
  46. package/template-2d/assets/sprites/courier.png +0 -0
  47. package/template-2d/assets/sprites/courier.png.ogt +7 -0
  48. package/template-2d/assets/strings/en.ogt +11 -0
  49. package/template-2d/assets/tiles/city.ogt +17 -0
  50. package/template-2d/assets/tiles/city.png +0 -0
  51. package/template-2d/assets/tiles/city.png.ogt +7 -0
  52. package/template-2d/gitattributes +22 -0
  53. package/template-2d/gitignore +8 -0
  54. package/template-2d/package.json +18 -0
  55. package/template-2d/project.ogt +21 -0
  56. package/template-2d/src/main.ts +305 -0
  57. package/template-2d/tests/smoke.og.ts +60 -0
  58. package/template-2d/tsconfig.json +17 -0
@@ -0,0 +1,305 @@
1
+ // The 2D starter's entry: its default export is og.game(...). `og run .`
2
+ // runs it headless with the probe; `og dev .` reloads it on every save;
3
+ // `og build .` bundles it for the play page.
4
+ //
5
+ // A courier runs and jumps the rooftops of one level: a hand-written
6
+ // tilemap (assets/maps/level.ogt) whose roofs collide and whose ledges are
7
+ // one-way on their own collider layer, an og.PlatformerController moved by
8
+ // the arrows, WASD, Space or a pad, the courier's flipbook clips chosen by
9
+ // what the controller did, coins to take, a pit that costs a life, a goal
10
+ // at the far end, a camera that follows, and the UI kit's platformer HUD.
11
+ // Everything the starter shows is in og-game §8 (sprites, flipbooks,
12
+ // tilemaps), §13 (the controller) and og-feel §12 (its tunables).
13
+ //
14
+ // The frame's look is data: GameDesc.look names a file under assets/looks/
15
+ // that extends a preset of the style catalogue, applied before start runs.
16
+
17
+ import { og, defineComponent, defineResource, defineSystem, bool, f32, u32, SHAPE_BOX } from "@origingame/engine";
18
+ import type { FlipbookInfo, Result, TileObject } from "@origingame/engine";
19
+
20
+ /** The courier's clips, in the sheet's file order (og-game §8): `start` checks them. */
21
+ const CLIP = { idle: 0, run: 1, jump: 2, fall: 3 } as const;
22
+ /** The camera's height in world units: 14 cells of 16 px, the 16bit-pixel look's 224-pixel frame. */
23
+ const VIEW_H = 14;
24
+ /** The ledges' collider layer (the map's `colliderLayers 2`), one-way for the courier. */
25
+ const LEDGES = 2;
26
+ /** The courier's box (og.Collider): its feet are its position. */
27
+ const BOX = { w: 0.9, h: 1.9 } as const;
28
+ /** Below this the courier fell into the pit. */
29
+ const FALL_Y = -3;
30
+ const LIVES = 3;
31
+
32
+ /** The courier. */
33
+ export const Hero = defineComponent("Hero", { spawnX: f32(0, { unit: "m" }), spawnY: f32(0, { unit: "m" }) });
34
+
35
+ /** A coin of the map's `coin` objects; taken once. */
36
+ export const Coin = defineComponent("Coin", { taken: bool() });
37
+
38
+ /** A voice: the sound bit (Sound) it plays when Game.sfx asks for it. */
39
+ export const Voice = defineComponent("Voice", { bit: u32() });
40
+
41
+ const Sound = { Jump: 1, Coin: 2 } as const;
42
+
43
+ /** The run: what the HUD shows and the systems share. */
44
+ export const Game = defineResource("Game", {
45
+ coins: u32(),
46
+ total: u32(),
47
+ lives: u32(LIVES),
48
+ won: bool(),
49
+ /** The last life was lost: Pickups starts the level again. */
50
+ restart: bool(),
51
+ goalX: f32(0, { unit: "m" }),
52
+ heroX: f32(0, { unit: "m" }),
53
+ heroY: f32(0, { unit: "m" }),
54
+ /** Sounds asked for this tick (Sound bits), played by Voices. */
55
+ sfx: u32(),
56
+ /** The pause menu is open: the courier and time hold. */
57
+ paused: bool(),
58
+ /** The HUD's touch controls this tick (a phone): the run and the held jump. */
59
+ touchX: f32(0, { min: -1, max: 1 }),
60
+ touchJump: bool(),
61
+ });
62
+
63
+ function must<T>(r: Result<T>): T {
64
+ if (!r.ok) throw new Error(r.reason);
65
+ return r.value;
66
+ }
67
+
68
+ /**
69
+ * Input to the controller, on the press's tick (og-feel §1): the run, the
70
+ * held jump (released early, the jump is lower), Down to drop through a
71
+ * ledge. The jump's sound starts on the press while the controller can
72
+ * jump (og-feel §12). A fall into the pit costs a life and puts the
73
+ * courier back at the spawn; the last life starts the level again.
74
+ */
75
+ export const Control = defineSystem(
76
+ "Control",
77
+ { reads: [Hero, og.Input, og.Gamepad], writes: [Game, og.PlatformerController, og.Transform], after: ["Hud"] },
78
+ (q, _time, world) => {
79
+ const g = must(world.resource(Game));
80
+ const input = must(world.resource(og.Input));
81
+ const pad = must(world.resource(og.Gamepad));
82
+ const stick = pad.axis("leftX", 0);
83
+ const run = Math.max(-1, Math.min(1, input.axis("ArrowLeft", "ArrowRight") + input.axis("KeyA", "KeyD") + (Math.abs(stick) > 0.25 ? stick : 0) + g.touchX));
84
+ const jump = input.down("Space") || input.down("ArrowUp") || input.down("KeyW") || pad.down("a", 0) || g.touchJump;
85
+ const pressed = input.pressed("Space") || input.pressed("ArrowUp") || input.pressed("KeyW") || pad.pressed("a", 0);
86
+ const down = input.down("ArrowDown") || input.down("KeyS") || pad.down("down", 0);
87
+ for (const a of q.archetypes()) {
88
+ const hero = a.column(Hero);
89
+ const pc = a.column(og.PlatformerController);
90
+ const p = a.column(og.Transform).position;
91
+ for (let i = 0; i < a.count; i++) {
92
+ const playing = !g.won && !g.paused;
93
+ pc.moveX[i] = playing ? run : 0;
94
+ pc.jump[i] = playing && jump ? 1 : 0;
95
+ pc.down[i] = playing && down ? 1 : 0;
96
+ if (playing && pressed && (pc.grounded[i] === 1 || pc.coyote[i]! > 0)) g.sfx |= Sound.Jump;
97
+ if (p[i * 3 + 1]! < FALL_Y) {
98
+ g.lives -= 1;
99
+ // The last life: Pickups puts every coin back.
100
+ if (g.lives === 0) g.restart = true;
101
+ p[i * 3] = hero.spawnX[i]!;
102
+ p[i * 3 + 1] = hero.spawnY[i]!;
103
+ pc.velocity[i * 3] = pc.velocity[i * 3 + 1] = 0;
104
+ }
105
+ g.heroX = p[i * 3]!;
106
+ g.heroY = p[i * 3 + 1]!;
107
+ }
108
+ }
109
+ },
110
+ );
111
+
112
+ /** A coin the courier's box touches is taken; reaching the goal clears the level; the last life lost puts every coin back. */
113
+ export const Pickups = defineSystem("Pickups", { writes: [Game, Coin, og.Sprite], reads: [og.Transform], after: ["Control"] }, (q, _time, world) => {
114
+ const g = must(world.resource(Game));
115
+ const restart = g.restart;
116
+ if (restart) {
117
+ // The last life was lost: every coin back, the lives full.
118
+ g.restart = false;
119
+ g.coins = 0;
120
+ g.lives = LIVES;
121
+ }
122
+ for (const a of q.archetypes()) {
123
+ const coin = a.column(Coin);
124
+ const sprite = a.column(og.Sprite);
125
+ const p = a.column(og.Transform).position;
126
+ for (let i = 0; i < a.count; i++) {
127
+ if (restart) {
128
+ coin.taken[i] = 0;
129
+ sprite.visible[i] = 1;
130
+ }
131
+ if (coin.taken[i] === 1) continue;
132
+ const dx = Math.abs(p[i * 3]! - g.heroX);
133
+ const dy = p[i * 3 + 1]! - g.heroY;
134
+ if (dx < BOX.w / 2 + 0.35 && dy > -0.35 && dy < BOX.h + 0.35) {
135
+ coin.taken[i] = 1;
136
+ sprite.visible[i] = 0;
137
+ g.coins += 1;
138
+ g.sfx |= Sound.Coin;
139
+ }
140
+ }
141
+ }
142
+ if (!g.won && g.heroX >= g.goalX) {
143
+ g.won = true;
144
+ g.sfx |= Sound.Coin;
145
+ }
146
+ });
147
+
148
+ /** The courier's clip from what its controller did, and its facing. */
149
+ export const Animate = defineSystem("Animate", { reads: [Hero, og.PlatformerController], writes: [og.Flipbook, og.Sprite], after: ["Control"] }, (q) => {
150
+ for (const a of q.archetypes()) {
151
+ const pc = a.column(og.PlatformerController);
152
+ const fb = a.column(og.Flipbook);
153
+ const sprite = a.column(og.Sprite);
154
+ for (let i = 0; i < a.count; i++) {
155
+ const vx = pc.velocity[i * 3]!;
156
+ const vy = pc.velocity[i * 3 + 1]!;
157
+ const clip = pc.grounded[i] !== 1 ? (vy > 0 ? CLIP.jump : CLIP.fall) : Math.abs(vx) > 0.2 ? CLIP.run : CLIP.idle;
158
+ if (fb.clip[i] !== clip) fb.clip[i] = clip;
159
+ sprite.flipX[i] = pc.facing[i]! < 0 ? 1 : 0;
160
+ }
161
+ }
162
+ });
163
+
164
+ /** Plays each sound asked for once: a voice still playing is stopped this tick and played again the next. */
165
+ export const Voices = defineSystem("Voices", { reads: [Voice], writes: [Game, og.AudioSource], after: ["Pickups"] }, (q, _time, world) => {
166
+ const g = must(world.resource(Game));
167
+ if (g.sfx === 0) return;
168
+ let left = g.sfx;
169
+ for (const a of q.archetypes()) {
170
+ const voice = a.column(Voice);
171
+ const source = a.column(og.AudioSource);
172
+ for (let i = 0; i < a.count; i++) {
173
+ const bit = voice.bit[i]!;
174
+ if ((g.sfx & bit) === 0) continue;
175
+ if (source.playing[i]) source.playing[i] = 0;
176
+ else {
177
+ source.playing[i] = 1;
178
+ left &= ~bit;
179
+ }
180
+ }
181
+ }
182
+ g.sfx = left;
183
+ });
184
+
185
+ /**
186
+ * The UI kit's platformer HUD (og-ui §6): lives, coins, the level's name,
187
+ * the pause button and, on a phone, the touch controls (taken by Control
188
+ * this tick: Hud runs first). Pause (the button, Escape or the pad's Start)
189
+ * opens the kit's pause menu and holds time until Resume.
190
+ */
191
+ export const Hud = defineSystem("Hud", { writes: [Game] }, (_q, _time, world) => {
192
+ const g = must(world.resource(Game));
193
+ const hud = og.ui.platformerHud("Hud", {
194
+ lives: g.lives,
195
+ maxLives: LIVES,
196
+ coins: g.coins,
197
+ score: g.coins * 100,
198
+ level: og.i18n.t(world, g.won ? "hud.clear" : "hud.level"),
199
+ timeLeft: null,
200
+ // A d-pad and Jump where the pointer is a finger (og-ui §5).
201
+ touch: og.ui.frameViewport().coarsePointer,
202
+ });
203
+ g.touchX = g.paused ? 0 : hud.moveX;
204
+ g.touchJump = !g.paused && hud.held.includes("jump");
205
+ const menu = og.ui.pauseMenu("Pause", { open: g.paused, items: [{ id: "resume", label: og.i18n.t(world, "pause.resume") }] });
206
+ if (g.paused && menu.selected === "resume") {
207
+ g.paused = false;
208
+ must(og.time.setScale(world, 1));
209
+ } else if (!g.paused && hud.paused) {
210
+ g.paused = true;
211
+ must(og.time.setScale(world, 0));
212
+ }
213
+ });
214
+
215
+ function checkClips(info: FlipbookInfo): void {
216
+ for (const [name, index] of Object.entries(CLIP)) {
217
+ const at = og.anim.clip(info, name);
218
+ if (at !== index) throw new Error(`sprites/courier.ogt: clip ${name} is ${at}, the code expects ${index}`);
219
+ }
220
+ }
221
+
222
+ /** A map object's point in world units: the map's top-left corner is at (0, height). */
223
+ function at(o: TileObject, height: number): { x: number; y: number } {
224
+ return { x: o.position.x, y: height + o.position.y };
225
+ }
226
+
227
+ export default og.game({
228
+ // The art direction as data (assets/looks/origin-game-2d.ogt; og-taste §12).
229
+ look: "looks/origin-game-2d.ogt",
230
+ tick: 60,
231
+ seed: 1,
232
+ resources: [Game],
233
+ systems: [Hud, Control, Pickups, Animate, Voices],
234
+ start(world) {
235
+ const map = must(og.load.tilemap(world, "maps/level.ogt"));
236
+ const courier = must(og.load.flipbook(world, "sprites/courier.ogt"));
237
+ checkClips(courier);
238
+ const coin = must(og.load.flipbook(world, "sprites/coin.ogt"));
239
+ const height = map.height * map.tileHeight;
240
+
241
+ const camera = must(world.resource(og.Camera));
242
+ camera.orthographic = true;
243
+ camera.height = VIEW_H;
244
+ camera.position = { x: 8, y: height / 2, z: 50 };
245
+ camera.background = { r: 0.0042, g: 0.0048, b: 0.018 };
246
+
247
+ // The map draws from its top-left corner and gives its collision layers static colliders.
248
+ const level = must(world.spawn({ name: "level", position: { x: 0, y: height, z: 0 } }));
249
+ must(world.insert(level, og.Tilemap, { map: map.map, layer: -10 }));
250
+
251
+ const g = must(world.resource(Game));
252
+ for (const o of map.objects) {
253
+ const p = at(o, height);
254
+ if (o.kind === "spawn") {
255
+ const hero = must(world.spawn({ name: "hero", position: { x: p.x, y: p.y, z: 0 } }));
256
+ must(world.insert(hero, Hero, { spawnX: p.x, spawnY: p.y }));
257
+ must(world.insert(hero, og.Sprite, { texture: courier.texture, width: 2, height: 2.5, anchor: { x: 0.5, y: 0 }, layer: 2 }));
258
+ must(world.insert(hero, og.Flipbook, { sheet: courier.sheet, clip: CLIP.idle }));
259
+ must(world.insert(hero, og.Collider, { shape: SHAPE_BOX, size: { x: BOX.w, y: BOX.h, z: 1 }, offset: { x: 0, y: BOX.h / 2, z: 0 } }));
260
+ // og-feel §12's platformer tunables; the ledges' layer is one-way.
261
+ must(world.insert(hero, og.PlatformerController, { maxSpeed: 8, jumpHeight: 3.6, timeToApex: 0.4, oneWayLayers: LEDGES, wallJumps: false }));
262
+ must(world.insert(hero, og.Follow2D, { offset: { x: 0, y: 1.5 }, deadZone: { x: 1, y: 0.8 }, smoothTime: 0.12, lookAhead: 0.2, lookAheadMax: { x: 2, y: 1 } }));
263
+ } else if (o.kind === "coin") {
264
+ const e = must(world.spawn({ name: o.name, position: { x: p.x, y: p.y, z: 0 } }));
265
+ must(world.insert(e, Coin));
266
+ must(world.insert(e, og.Sprite, { texture: coin.texture, width: 0.7, height: 0.7, layer: 1 }));
267
+ must(world.insert(e, og.Flipbook, { sheet: coin.sheet, clip: 0 }));
268
+ g.total += 1;
269
+ } else if (o.kind === "goal") {
270
+ g.goalX = p.x;
271
+ }
272
+ }
273
+
274
+ for (const [path, bit] of [["audio/jump.wav", Sound.Jump], ["audio/coin.wav", Sound.Coin]] as const) {
275
+ const sound = og.load.sound(world, path);
276
+ if (!sound.ok) continue;
277
+ const v = must(world.spawn({ name: `voice-${bit}` }));
278
+ must(world.insert(v, Voice, { bit }));
279
+ must(world.insert(v, og.AudioSource, { sound: sound.value, playing: false, volume: 0.5 }));
280
+ }
281
+
282
+ // Every string the HUD shows (og-i18n §1), and its font.
283
+ must(og.i18n.load(world, "strings/en.ogt"));
284
+ const font = og.load.font(world, "fonts/ui.ttf");
285
+ if (font.ok) og.ui.configure(world, { font: font.value });
286
+ },
287
+ commands: {
288
+ /** `warp:<x>:<y>`: put the courier's feet there. */
289
+ warp: (world, args) => {
290
+ const hero = world.find("hero");
291
+ const x = Number(args[0]);
292
+ const y = Number(args[1]);
293
+ if (hero === null || !Number.isFinite(x) || !Number.isFinite(y)) throw new Error("write warp:<x>:<y>");
294
+ must(world.set(hero, og.Transform, { position: { x, y, z: 0 } }));
295
+ },
296
+ },
297
+ commandHelp: {
298
+ warp: { usage: "<x>:<y>", doc: "Put the courier's feet at a world point." },
299
+ },
300
+ facts: (world) => {
301
+ const g = world.resource(Game);
302
+ if (!g.ok) return {};
303
+ return { coins: g.value.coins, total: g.value.total, lives: g.value.lives, won: g.value.won, x: g.value.heroX, y: g.value.heroY };
304
+ },
305
+ });
@@ -0,0 +1,60 @@
1
+ // The starter's playtest, run by `og test` through `og run --headless
2
+ // --commands`: the courier runs right over two coins, jumps up through the
3
+ // one-way ledge above it and lands on top, Escape opens the pause menu and
4
+ // holds time, Escape again resumes, and a fall into the pit costs a life
5
+ // and puts the courier back at the spawn.
6
+ import { playtest } from "@origingame/engine/playtest";
7
+
8
+ interface Got<T> {
9
+ value: T;
10
+ }
11
+
12
+ interface Game {
13
+ coins: number;
14
+ lives: number;
15
+ paused: boolean;
16
+ heroX: number;
17
+ heroY: number;
18
+ }
19
+
20
+ interface EntityDump {
21
+ components: Record<string, Record<string, number | boolean | string | number[]>>;
22
+ }
23
+
24
+ interface UiDump {
25
+ elements: Array<{ id: string; text: string; width: number; height: number }>;
26
+ }
27
+
28
+ export default playtest({
29
+ name: "The courier runs over two coins, jumps up through a one-way ledge onto it, pauses and resumes, and the pit costs a life",
30
+ commands:
31
+ "0.5:key_down:ArrowRight;2.0:key_up:ArrowRight;" +
32
+ "2.3:key_down:Space;2.9:key_up:Space;" +
33
+ "3.5:screenshot:ledge.png;3.55:dump_entity:hero:hero.json;3.6:get:Game:ledge.json;3.65:dump_ui:ui.json;" +
34
+ "3.7:key_down:Escape;3.75:key_up:Escape;4.2:get:Game:paused.json;" +
35
+ "4.3:key_down:Escape;4.35:key_up:Escape;4.5:get:Game:resumed.json;" +
36
+ "4.6:warp:21:-1;5.0:get:Game:fell.json;5.1:quit",
37
+ check(run, t) {
38
+ t.ok(run.quit, "the script reached quit");
39
+ t.frame("ledge.png", () => {
40
+ const frame = run.png("ledge.png");
41
+ t.ok(frame.width > 0 && frame.height > 0, `a ${frame.width}x${frame.height} frame was captured`);
42
+ });
43
+ const ledge = run.json<Got<Game>>("ledge.json").value;
44
+ t.ok(ledge.coins === 2, `the run took the two coins on the roof (${ledge.coins})`);
45
+ // The ledge (cells 11-15 of row 9) has its top at y 5: the courier passed up through it and stands on it.
46
+ t.ok(ledge.heroX > 11 && ledge.heroX < 16, `the courier is under the ledge's span (x ${ledge.heroX.toFixed(2)})`);
47
+ t.ok(Math.abs(ledge.heroY - 5) < 0.05, `and stands on the one-way ledge (y ${ledge.heroY.toFixed(3)})`);
48
+ const hero = run.json<EntityDump>("hero.json");
49
+ t.ok(hero.components["og.PlatformerController"]!["grounded"] === true, "the controller is grounded on the ledge");
50
+ const ui = run.json<UiDump>("ui.json");
51
+ const count = ui.elements.find((e) => e.id === "Count");
52
+ t.ok(count !== undefined && count.text === "2", `the platformer HUD counts the coins ("${count?.text}")`);
53
+ const paused = run.json<Got<Game>>("paused.json").value;
54
+ t.ok(paused.paused, "Escape opened the pause menu");
55
+ t.ok(!run.json<Got<Game>>("resumed.json").value.paused, "Escape again resumed");
56
+ const fell = run.json<Got<Game>>("fell.json").value;
57
+ t.ok(fell.lives === 2, `the pit cost a life (${fell.lives} left)`);
58
+ t.ok(Math.abs(fell.heroX - 2.5) < 0.5 && fell.heroY < 2.5, `the courier is back at the spawn (${fell.heroX.toFixed(2)}, ${fell.heroY.toFixed(2)})`);
59
+ },
60
+ });
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "lib": ["ES2022"],
7
+ "types": [],
8
+ "strict": true,
9
+ "exactOptionalPropertyTypes": true,
10
+ "noFallthroughCasesInSwitch": true,
11
+ "isolatedModules": true,
12
+ "verbatimModuleSyntax": true,
13
+ "allowImportingTsExtensions": true,
14
+ "noEmit": true
15
+ },
16
+ "include": ["src/**/*.ts", "tests/**/*.ts"]
17
+ }