gorlorn 1.0.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/dist/index.mjs ADDED
@@ -0,0 +1,1432 @@
1
+ // src/GorlornGame.tsx
2
+ import { useEffect, useRef, useState } from "react";
3
+
4
+ // src/Constants.ts
5
+ var Constants = {
6
+ IsDebugMode: false,
7
+ AutoClearStats: false,
8
+ DieInOneHit: false,
9
+ EnemyDiameter: 0.058,
10
+ HeroDiameter: 0.056,
11
+ BulletDiameter: 0.026,
12
+ HeartDiameter: 0.035,
13
+ ButtonDiameter: 0.1,
14
+ HealthBarLength: 0.35,
15
+ HealthBarThickness: 0.06,
16
+ EnemySpeed: 0.7,
17
+ HeroSpeed: 0.9,
18
+ HeroAcceleration: 8,
19
+ BulletSpeed: 0.8,
20
+ HeartSpeed: 0.3,
21
+ PlayerFrozenOnHitMs: 2e3,
22
+ PlayerBlinksOnHitMs: 3e3,
23
+ ChainBulletLifeTimeMs: 300,
24
+ StartingEnemySpawnIntervalMs: 400,
25
+ EnemySpawnRateAcceleration: 0.995,
26
+ EnemySpeedIncrement: 8e-4,
27
+ MaxComboSize: 20,
28
+ EnemyDamage: 20,
29
+ PlayerHealth: 140,
30
+ HeartHealthRestore: 40,
31
+ MinShotIntervalMs: 250,
32
+ StartingChainCountToSpawnHeart: 4
33
+ };
34
+
35
+ // src/Images.ts
36
+ function makeSprite(src, width, height) {
37
+ const img = new window.Image();
38
+ img.src = src;
39
+ return { img, width, height };
40
+ }
41
+ var _Images = class _Images {
42
+ static load(screenWidth, screenHeight, onComplete, assetBasePath = "/gorlorn") {
43
+ if (_Images._isLoaded) {
44
+ onComplete();
45
+ return;
46
+ }
47
+ const base = assetBasePath.replace(/\/+$/, "");
48
+ const xPct = (p) => Math.round(screenWidth * p);
49
+ const yPct = (p) => Math.round(screenHeight * p);
50
+ const avg = (screenWidth + screenHeight) / 2;
51
+ const byWidth = (p) => Math.round(avg * p);
52
+ const btnH = yPct(0.1);
53
+ const btnW = btnH * 5;
54
+ const sprites = [
55
+ ["Hero", `${base}/hero.png`, byWidth(Constants.HeroDiameter), byWidth(Constants.HeroDiameter)],
56
+ ["Background", `${base}/space.png`, xPct(1.1), yPct(1.1)],
57
+ ["Title", `${base}/title.png`, xPct(0.7), yPct(0.7 / 4.347826086956522)],
58
+ ["Bullet", `${base}/bullet.png`, byWidth(Constants.BulletDiameter), byWidth(Constants.BulletDiameter)],
59
+ ["BulletCombo", `${base}/bullet_combo.png`, byWidth(Constants.BulletDiameter), byWidth(Constants.BulletDiameter)],
60
+ ["DeathText", `${base}/death_text.png`, yPct(0.2 * 5.58333), yPct(0.2)],
61
+ ["Enemy", `${base}/enemy.png`, byWidth(Constants.EnemyDiameter), byWidth(Constants.EnemyDiameter)],
62
+ ["Heart", `${base}/heart.png`, byWidth(Constants.HeartDiameter), byWidth(Constants.HeartDiameter)],
63
+ ["LeftButton", `${base}/left_button.png`, xPct(Constants.ButtonDiameter), xPct(Constants.ButtonDiameter)],
64
+ ["RightButton", `${base}/right_button.png`, xPct(Constants.ButtonDiameter), xPct(Constants.ButtonDiameter)],
65
+ ["ButtonPlay", `${base}/button_play.png`, btnW, btnH],
66
+ ["ButtonStatistics", `${base}/button_statistics.png`, btnW, btnH],
67
+ ["ButtonAbout", `${base}/button_about.png`, btnW, btnH],
68
+ ["ButtonQuit", `${base}/button_quit.png`, btnW, btnH]
69
+ ];
70
+ let remaining = sprites.length;
71
+ const done = () => {
72
+ if (--remaining === 0) {
73
+ _Images._isLoaded = true;
74
+ onComplete();
75
+ }
76
+ };
77
+ for (const [key, src, w, h] of sprites) {
78
+ const sprite = makeSprite(src, w, h);
79
+ sprite.img.onload = done;
80
+ sprite.img.onerror = done;
81
+ _Images[key] = sprite;
82
+ }
83
+ }
84
+ };
85
+ _Images._isLoaded = false;
86
+ var Images = _Images;
87
+
88
+ // src/UI/Background.ts
89
+ var NUM_POINTS = 10;
90
+ var Background = class {
91
+ constructor(gorlorn) {
92
+ this.horizontalLineStarts = [];
93
+ this.horizontalLinesOffset = 0;
94
+ this.time = 0;
95
+ this.backgroundXOffset = 0;
96
+ this.backgroundYOffset = 0;
97
+ this.gridAnimationDurationMs = 300;
98
+ this.isRetractingGrid = false;
99
+ this.isExpandingGrid = false;
100
+ this.gorlorn = gorlorn;
101
+ this.targetGridTopY = gorlorn.getYFromPercent(0.45);
102
+ this.gridTopY = this.targetGridTopY;
103
+ for (let i = 0; i < NUM_POINTS; i++) {
104
+ this.horizontalLineStarts.push(gorlorn.getXFromPercent(1 / NUM_POINTS * i));
105
+ }
106
+ }
107
+ retractGrid(durationMs) {
108
+ this.gridAnimationDurationMs = durationMs;
109
+ this.isRetractingGrid = true;
110
+ }
111
+ extendGrid(durationMs) {
112
+ this.gridAnimationDurationMs = durationMs;
113
+ this.isExpandingGrid = true;
114
+ }
115
+ update(dt) {
116
+ const speed = this.gorlorn.screenWidth * 0.1;
117
+ this.time += dt;
118
+ this.backgroundXOffset = this.gorlorn.getXFromPercent(0.05) * Math.cos(this.time / 2) - this.gorlorn.getXFromPercent(0.05);
119
+ this.backgroundYOffset = this.gorlorn.getYFromPercent(0.05) * Math.sin(this.time / 2) - this.gorlorn.getYFromPercent(0.05);
120
+ this.horizontalLinesOffset += speed * dt;
121
+ if (this.isRetractingGrid) {
122
+ this.gridTopY += this.targetGridTopY / (this.gridAnimationDurationMs / 1e3) * dt;
123
+ if (this.gridTopY > this.gorlorn.screenHeight) this.isRetractingGrid = false;
124
+ } else if (this.isExpandingGrid) {
125
+ this.gridTopY -= this.targetGridTopY / (this.gridAnimationDurationMs / 1e3) * dt;
126
+ if (this.gridTopY <= this.targetGridTopY) {
127
+ this.gridTopY = this.targetGridTopY;
128
+ this.isExpandingGrid = false;
129
+ }
130
+ }
131
+ }
132
+ draw(ctx) {
133
+ const bg = Images.Background;
134
+ ctx.drawImage(bg.img, this.backgroundXOffset, this.backgroundYOffset, bg.width, bg.height);
135
+ ctx.save();
136
+ ctx.strokeStyle = "rgb(247,33,155)";
137
+ ctx.lineWidth = this.gorlorn.getYFromPercent(5e-3);
138
+ const centerX = this.gorlorn.screenWidth * 0.5;
139
+ const bottomY = this.gorlorn.screenHeight + (this.gridTopY - this.targetGridTopY);
140
+ for (let i = 0; i < NUM_POINTS; i++) {
141
+ const lineStart = (this.horizontalLineStarts[i] + this.horizontalLinesOffset) % this.gorlorn.screenWidth;
142
+ const distFromCenter = centerX - lineStart;
143
+ const bottomX = centerX - distFromCenter * 3.2;
144
+ ctx.beginPath();
145
+ ctx.moveTo(lineStart, this.gridTopY);
146
+ ctx.lineTo(bottomX, bottomY);
147
+ ctx.stroke();
148
+ }
149
+ let y = this.gridTopY;
150
+ let yInterval = this.gorlorn.getYFromPercent(0.05);
151
+ while (y < this.gorlorn.screenHeight) {
152
+ ctx.beginPath();
153
+ ctx.moveTo(0, y);
154
+ ctx.lineTo(this.gorlorn.screenWidth, y);
155
+ ctx.stroke();
156
+ y += yInterval;
157
+ yInterval *= 1.3;
158
+ }
159
+ ctx.restore();
160
+ }
161
+ };
162
+
163
+ // src/Entities/Points.ts
164
+ var Points = class {
165
+ constructor(gorlorn, points, chainCount, x, y) {
166
+ this.gorlorn = gorlorn;
167
+ this.pointString = String(points);
168
+ this.timeCreatedMs = Date.now();
169
+ this.x = x;
170
+ this.y = y;
171
+ this.textSize = Math.round(gorlorn.getYFromPercent(0.03 + chainCount * 5e-3));
172
+ if (points > Math.pow(2, 5)) {
173
+ this.color = "rgb(255,0,0)";
174
+ this.lifeTimeMs = 1250;
175
+ this.speed = gorlorn.screenWidth * 0.15;
176
+ } else {
177
+ this.color = "rgb(255,255,255)";
178
+ this.lifeTimeMs = 800;
179
+ this.speed = gorlorn.screenHeight * 0.1;
180
+ }
181
+ }
182
+ update(dt) {
183
+ if (Date.now() - this.timeCreatedMs > this.lifeTimeMs) return true;
184
+ this.y += this.speed * dt;
185
+ return false;
186
+ }
187
+ draw(ctx) {
188
+ ctx.font = `${this.textSize}px sans-serif`;
189
+ ctx.fillStyle = this.color;
190
+ ctx.textAlign = "left";
191
+ ctx.fillText(this.pointString, this.x, this.y);
192
+ }
193
+ };
194
+
195
+ // src/UI/HealthBar.ts
196
+ var HealthBar = class {
197
+ constructor(x, y, thickness, length) {
198
+ this.x = x;
199
+ this.y = y;
200
+ this.thickness = thickness;
201
+ this.length = length;
202
+ }
203
+ draw(ctx, healthPercent) {
204
+ const padding = 2;
205
+ const fillLength = Math.round((this.length - padding * 2) * healthPercent);
206
+ ctx.fillStyle = "rgb(55,55,55)";
207
+ ctx.fillRect(this.x, this.y, this.length, this.thickness);
208
+ ctx.fillStyle = "rgb(255,0,0)";
209
+ ctx.fillRect(this.x + padding, this.y + padding, fillLength, this.thickness - padding * 2);
210
+ ctx.fillStyle = "rgb(22,22,22)";
211
+ ctx.fillRect(this.x + padding + fillLength, this.y + padding, this.length - padding * 2 - fillLength, this.thickness - padding * 2);
212
+ }
213
+ };
214
+
215
+ // src/UI/HUD.ts
216
+ var HUD = class {
217
+ constructor(gorlorn) {
218
+ this.points = [];
219
+ this.pointers = [];
220
+ this.isPointerDown = false;
221
+ this.keyLeftDown = false;
222
+ this.keyRightDown = false;
223
+ // Public state read by Hero, Button, etc.
224
+ this.isLeftPressed = false;
225
+ this.isRightPressed = false;
226
+ this.isClickedFlag = false;
227
+ this.isEscapeDown = false;
228
+ this.clickX = 0;
229
+ this.clickY = 0;
230
+ this.gorlorn = gorlorn;
231
+ this.buttonHitBoxDiameter = Math.round(gorlorn.getXFromPercent(Constants.ButtonDiameter) * 1.2);
232
+ this.leftBoxRight = this.buttonHitBoxDiameter;
233
+ this.leftBoxBottom = gorlorn.screenHeight;
234
+ this.rightBoxLeft = gorlorn.screenWidth - this.buttonHitBoxDiameter;
235
+ this.rightBoxBottom = gorlorn.screenHeight;
236
+ const healthBarLength = gorlorn.getXFromPercent(Constants.HealthBarLength);
237
+ const healthBarThickness = gorlorn.getYFromPercent(Constants.HealthBarThickness);
238
+ this.healthBar = new HealthBar(
239
+ gorlorn.getXFromPercent(0.99) - healthBarLength,
240
+ gorlorn.getYFromPercent(0.01),
241
+ healthBarThickness,
242
+ healthBarLength
243
+ );
244
+ this.scoreTextSize = gorlorn.getYFromPercent(0.08);
245
+ this.highScoreTextSize = gorlorn.getYFromPercent(0.05);
246
+ }
247
+ handlePointerEvent(e) {
248
+ if (this.isClickedFlag) this.isClickedFlag = false;
249
+ if (e.action === "down") {
250
+ this.pointers.push({ id: e.id, x: e.x, y: e.y });
251
+ if (this.pointers.length === 1 && !this.isPointerDown) {
252
+ this.isClickedFlag = true;
253
+ this.clickX = e.x;
254
+ this.clickY = e.y;
255
+ }
256
+ } else if (e.action === "up") {
257
+ this.pointers = this.pointers.filter((p) => p.id !== e.id);
258
+ } else if (e.action === "move") {
259
+ for (const p of this.pointers) {
260
+ if (p.id === e.id) {
261
+ p.x = e.x;
262
+ p.y = e.y;
263
+ }
264
+ }
265
+ }
266
+ this.isPointerDown = this.pointers.length === 1;
267
+ this.recomputeDirections();
268
+ }
269
+ handleKeyDown(key) {
270
+ if (key === "ArrowLeft") this.keyLeftDown = true;
271
+ if (key === "ArrowRight") this.keyRightDown = true;
272
+ if (key === "Escape") this.isEscapeDown = true;
273
+ this.recomputeDirections();
274
+ }
275
+ handleKeyUp(key) {
276
+ if (key === "ArrowLeft") this.keyLeftDown = false;
277
+ if (key === "ArrowRight") this.keyRightDown = false;
278
+ if (key === "Escape") this.isEscapeDown = false;
279
+ this.recomputeDirections();
280
+ }
281
+ recomputeDirections() {
282
+ let pointerLeft = false;
283
+ let pointerRight = false;
284
+ if (this.pointers.length > 0) {
285
+ const latest = this.pointers[this.pointers.length - 1];
286
+ pointerLeft = latest.x <= this.leftBoxRight && latest.y >= this.gorlorn.screenHeight - this.buttonHitBoxDiameter;
287
+ pointerRight = latest.x >= this.rightBoxLeft && latest.y >= this.gorlorn.screenHeight - this.buttonHitBoxDiameter;
288
+ }
289
+ this.isLeftPressed = pointerLeft || this.keyLeftDown;
290
+ this.isRightPressed = pointerRight || this.keyRightDown;
291
+ }
292
+ addPoints(x, y, chainCount) {
293
+ const pts = Math.pow(2, chainCount);
294
+ this.gorlorn.gameStats.score += pts;
295
+ this.points.push(new Points(this.gorlorn, pts, chainCount, x, y));
296
+ }
297
+ update(dt) {
298
+ this.points = this.points.filter((p) => !p.update(dt));
299
+ }
300
+ draw(ctx) {
301
+ ctx.save();
302
+ ctx.font = `${this.highScoreTextSize}px sans-serif`;
303
+ ctx.fillStyle = "white";
304
+ ctx.textAlign = "left";
305
+ ctx.fillText(`High Score: ${this.gorlorn.getHighScore()}`, this.gorlorn.getXFromPercent(0.01), this.gorlorn.getYFromPercent(0.065));
306
+ ctx.font = `${this.scoreTextSize}px sans-serif`;
307
+ ctx.fillText(`Score: ${this.gorlorn.gameStats.score}`, this.gorlorn.getXFromPercent(0.01), this.gorlorn.getYFromPercent(0.14));
308
+ const lb = Images.LeftButton;
309
+ const lbx = Math.round((this.leftBoxRight - lb.width) / 2);
310
+ const lby = Math.round(this.gorlorn.screenHeight - this.buttonHitBoxDiameter + (this.buttonHitBoxDiameter - lb.height) / 2);
311
+ ctx.drawImage(lb.img, lbx, lby, lb.width, lb.height);
312
+ const rb = Images.RightButton;
313
+ const rbx = Math.round(this.rightBoxLeft + (this.buttonHitBoxDiameter - rb.width) / 2);
314
+ const rby = Math.round(this.gorlorn.screenHeight - this.buttonHitBoxDiameter + (this.buttonHitBoxDiameter - rb.height) / 2);
315
+ ctx.drawImage(rb.img, rbx, rby, rb.width, rb.height);
316
+ for (const p of this.points) {
317
+ p.draw(ctx);
318
+ }
319
+ this.healthBar.draw(ctx, this.gorlorn.hero.getHealthPercent());
320
+ ctx.restore();
321
+ }
322
+ };
323
+
324
+ // src/Entities/Entity.ts
325
+ function rectsIntersect(a, b) {
326
+ return a.left < b.right && a.right > b.left && a.top < b.bottom && a.bottom > b.top;
327
+ }
328
+ function rectContainsRect(outer, inner) {
329
+ return inner.left >= outer.left && inner.right <= outer.right && inner.top >= outer.top && inner.bottom <= outer.bottom;
330
+ }
331
+ var Entity = class {
332
+ constructor(sprite) {
333
+ this.x = 0;
334
+ this.y = 0;
335
+ this.opacity = 1;
336
+ this.vx = 0;
337
+ this.vy = 0;
338
+ this.maxV = Number.MAX_VALUE;
339
+ this.ax = 0;
340
+ this.ay = 0;
341
+ this.hitBox = { left: 0, top: 0, right: 0, bottom: 0 };
342
+ this.isBlinking = false;
343
+ this.timeStartedBlinkingMs = 0;
344
+ this.blinkDurationMs = 0;
345
+ this.blinkOpacityDelta = 0;
346
+ this.sprite = sprite;
347
+ this.width = sprite.width;
348
+ this.height = sprite.width;
349
+ this.timeCreatedMs = Date.now();
350
+ }
351
+ update(dt) {
352
+ this.x += this.vx * dt;
353
+ this.y += this.vy * dt;
354
+ this.vx = Math.max(-this.maxV, Math.min(this.maxV, this.vx + this.ax * dt));
355
+ this.vy = Math.max(-this.maxV, Math.min(this.maxV, this.vy + this.ay * dt));
356
+ this.hitBox = {
357
+ left: this.x - this.width * 0.48,
358
+ top: this.y - this.height * 0.48,
359
+ right: this.x + this.width * 0.48,
360
+ bottom: this.y + this.height * 0.48
361
+ };
362
+ if (this.isBlinking) {
363
+ if (Date.now() >= this.timeStartedBlinkingMs + this.blinkDurationMs) {
364
+ this.opacity = 1;
365
+ this.isBlinking = false;
366
+ } else {
367
+ this.opacity += dt * this.blinkOpacityDelta;
368
+ if (this.opacity >= 1) {
369
+ this.opacity = 1;
370
+ this.blinkOpacityDelta *= -1;
371
+ } else if (this.opacity <= 0) {
372
+ this.opacity = 0;
373
+ this.blinkOpacityDelta *= -1;
374
+ }
375
+ }
376
+ }
377
+ return false;
378
+ }
379
+ draw(ctx) {
380
+ const prevAlpha = ctx.globalAlpha;
381
+ if (this.opacity !== 1) ctx.globalAlpha = this.opacity;
382
+ ctx.drawImage(
383
+ this.sprite.img,
384
+ this.x - this.width / 2,
385
+ this.y - this.height / 2,
386
+ this.width,
387
+ this.height
388
+ );
389
+ if (this.opacity !== 1) ctx.globalAlpha = prevAlpha;
390
+ }
391
+ getTimeCreatedMs() {
392
+ return this.timeCreatedMs;
393
+ }
394
+ isOlderThan(other) {
395
+ return this.timeCreatedMs < other.getTimeCreatedMs();
396
+ }
397
+ startBlinking(durationMs) {
398
+ this.timeStartedBlinkingMs = Date.now();
399
+ this.blinkDurationMs = durationMs;
400
+ this.isBlinking = true;
401
+ this.blinkOpacityDelta = -17;
402
+ }
403
+ getIsBlinking() {
404
+ return this.isBlinking;
405
+ }
406
+ testHitEntity(other) {
407
+ return rectsIntersect(this.hitBox, other.hitBox);
408
+ }
409
+ };
410
+
411
+ // src/Entities/Hero.ts
412
+ var Hero = class extends Entity {
413
+ constructor(gorlorn) {
414
+ super(Images.Hero);
415
+ this.lastShotFiredMs = 0;
416
+ this.direction = "None";
417
+ this.gorlorn = gorlorn;
418
+ this.speed = gorlorn.screenWidth * Constants.HeroSpeed;
419
+ this.acceleration = gorlorn.screenWidth * Constants.HeroAcceleration;
420
+ this.health = Constants.PlayerHealth;
421
+ this.maxV = this.speed;
422
+ this.x = gorlorn.screenWidth * 0.5;
423
+ this.y = gorlorn.getYFromPercent(0.995 - Constants.HeroDiameter);
424
+ }
425
+ dealDamage(damage) {
426
+ if (this.getIsBlinking()) return;
427
+ this.health -= damage;
428
+ this.startBlinking(Constants.PlayerBlinksOnHitMs);
429
+ if (this.health <= 1e-3 || Constants.DieInOneHit) {
430
+ this.health = 0;
431
+ this.gorlorn.die();
432
+ }
433
+ this.gorlorn.bulletManager.fireBulletSpray(this.x, this.y, 6, 0, Math.PI * 1.1, Math.PI * 1.9);
434
+ }
435
+ restoreHealth() {
436
+ this.health = Math.min(Constants.PlayerHealth, this.health + Constants.HeartHealthRestore);
437
+ }
438
+ getHealthPercent() {
439
+ return this.health / Constants.PlayerHealth;
440
+ }
441
+ update(dt) {
442
+ const hud = this.gorlorn.hud;
443
+ const canMoveAndShoot = !this.getIsBlinking() || Date.now() > this.timeStartedBlinkingMs + Constants.PlayerFrozenOnHitMs;
444
+ if (canMoveAndShoot && hud.isLeftPressed) {
445
+ if (this.direction === "Right") this.vx = 0;
446
+ this.ax = -this.acceleration;
447
+ this.direction = "Left";
448
+ } else if (canMoveAndShoot && hud.isRightPressed) {
449
+ if (this.direction === "Left") this.vx = 0;
450
+ this.ax = this.acceleration;
451
+ this.direction = "Right";
452
+ } else {
453
+ this.vx = 0;
454
+ this.ax = 0;
455
+ this.direction = "None";
456
+ }
457
+ super.update(dt);
458
+ this.x = Math.min(Math.max(this.x, this.width * 0.5), this.gorlorn.screenWidth - this.width * 0.5);
459
+ if (canMoveAndShoot) {
460
+ const now = Date.now();
461
+ if (now - this.lastShotFiredMs > Constants.MinShotIntervalMs) {
462
+ this.lastShotFiredMs = now;
463
+ this.gorlorn.bulletManager.fireBullet(this.x, this.y, Math.PI * 1.5);
464
+ this.gorlorn.gameStats.shotsFired++;
465
+ }
466
+ }
467
+ return false;
468
+ }
469
+ };
470
+
471
+ // src/Entities/Bullet.ts
472
+ var Bullet = class extends Entity {
473
+ constructor(gorlorn, sprite, x, y, speed, angle, chainCount, lifetimeMs) {
474
+ super(sprite);
475
+ this.gorlorn = gorlorn;
476
+ this.lifeTimeMs = lifetimeMs;
477
+ this.chainCount = chainCount;
478
+ this.createdTimeMs = Date.now();
479
+ this.x = x;
480
+ this.y = y;
481
+ this.vx = speed * Math.cos(angle);
482
+ this.vy = speed * Math.sin(angle);
483
+ }
484
+ isChainBullet() {
485
+ return this.chainCount > 1;
486
+ }
487
+ getChainCount() {
488
+ return this.chainCount;
489
+ }
490
+ update(dt) {
491
+ if (this.lifeTimeMs !== 0 && Date.now() - this.createdTimeMs > this.lifeTimeMs) {
492
+ return true;
493
+ }
494
+ super.update(dt);
495
+ const gameArea = { left: 0, top: 0, right: this.gorlorn.screenWidth, bottom: this.gorlorn.screenHeight };
496
+ return !rectContainsRect(gameArea, this.hitBox);
497
+ }
498
+ };
499
+
500
+ // src/BulletManager.ts
501
+ var BulletManager = class {
502
+ constructor(gorlorn) {
503
+ this.bullets = [];
504
+ this.chainCountToSpawnHeart = Constants.StartingChainCountToSpawnHeart;
505
+ this.gorlorn = gorlorn;
506
+ this.speed = (gorlorn.screenWidth + gorlorn.screenHeight) / 2 * Constants.BulletSpeed;
507
+ }
508
+ fireBullet(originX, originY, angle, chainCount = 1, lifeTimeMs = 0) {
509
+ const sprite = chainCount === 1 ? Images.Bullet : Images.BulletCombo;
510
+ this.bullets.push(new Bullet(this.gorlorn, sprite, originX, originY, this.speed, angle, chainCount, lifeTimeMs));
511
+ }
512
+ fireBulletSpray(originX, originY, numBullets, chainCount, minAngle = 0, maxAngle = Math.PI * 2) {
513
+ const newChainCount = Math.min(Constants.MaxComboSize, chainCount + 1);
514
+ for (let i = 0; i < numBullets; i++) {
515
+ const angle = minAngle + Math.random() * (maxAngle - minAngle);
516
+ this.fireBullet(originX, originY, angle, newChainCount, Constants.ChainBulletLifeTimeMs);
517
+ }
518
+ }
519
+ update(dt) {
520
+ const dead = [];
521
+ const enemyKillers = [];
522
+ for (const bullet of this.bullets) {
523
+ if (bullet.update(dt)) {
524
+ dead.push(bullet);
525
+ continue;
526
+ }
527
+ const killedEnemy = this.gorlorn.enemyManager.tryKillEnemy(bullet);
528
+ if (killedEnemy !== null) {
529
+ this.gorlorn.hud.addPoints(bullet.x, bullet.y, bullet.getChainCount());
530
+ dead.push(bullet);
531
+ enemyKillers.push(bullet);
532
+ if (bullet.getChainCount() > this.chainCountToSpawnHeart) {
533
+ this.gorlorn.heartManager.spawnHeart(bullet.x, bullet.y);
534
+ this.chainCountToSpawnHeart++;
535
+ }
536
+ } else if (bullet.y - bullet.height < 0) {
537
+ dead.push(bullet);
538
+ }
539
+ }
540
+ this.bullets = this.bullets.filter((b) => !dead.includes(b));
541
+ for (const b of enemyKillers) {
542
+ this.fireBulletSpray(b.x, b.y, 3, b.getChainCount());
543
+ this.gorlorn.gameStats.highestCombo = Math.max(b.getChainCount() + 1, this.gorlorn.gameStats.highestCombo);
544
+ }
545
+ }
546
+ draw(ctx) {
547
+ for (const bullet of this.bullets) {
548
+ bullet.draw(ctx);
549
+ }
550
+ }
551
+ };
552
+
553
+ // src/Entities/Enemy.ts
554
+ var Enemy = class extends Entity {
555
+ constructor(gorlorn, sprite) {
556
+ super(sprite);
557
+ this.hasEnteredScreen = false;
558
+ this.gorlorn = gorlorn;
559
+ }
560
+ update(dt) {
561
+ super.update(dt);
562
+ if (this.gorlorn.hero.testHitEntity(this)) {
563
+ this.gorlorn.hero.dealDamage(Constants.EnemyDamage);
564
+ return true;
565
+ }
566
+ if (this.x <= this.width * 0.5 || this.x >= this.gorlorn.screenWidth - this.width * 0.5) {
567
+ this.vx *= -1;
568
+ }
569
+ if (this.hasEnteredScreen) {
570
+ if (this.y <= this.height * 0.5 || this.y >= this.gorlorn.screenHeight - this.height * 0.5) {
571
+ this.vy *= -1;
572
+ }
573
+ this.x = Math.min(Math.max(this.x, this.width * 0.5), this.gorlorn.screenWidth - this.width * 0.5);
574
+ this.y = Math.min(Math.max(this.y, this.height * 0.5), this.gorlorn.screenHeight - this.height * 0.5);
575
+ } else if (this.y > this.height) {
576
+ this.hasEnteredScreen = true;
577
+ }
578
+ return false;
579
+ }
580
+ };
581
+
582
+ // src/EnemyManager.ts
583
+ var EnemyManager = class {
584
+ constructor(gorlorn) {
585
+ this.enemies = [];
586
+ this.timeLastEnemySpawnedMs = 0;
587
+ this.enemySpawnIntervalMs = Constants.StartingEnemySpawnIntervalMs;
588
+ this.enemySpeed = Constants.EnemySpeed;
589
+ this.timeLastRateUpdateMs = 0;
590
+ this.gorlorn = gorlorn;
591
+ }
592
+ tryKillEnemy(bullet) {
593
+ for (const enemy of this.enemies) {
594
+ if (!(bullet.isChainBullet() && bullet.isOlderThan(enemy)) && enemy.testHitEntity(bullet)) {
595
+ this.gorlorn.gameStats.enemiesVanquished++;
596
+ this.enemies = this.enemies.filter((e) => e !== enemy);
597
+ return enemy;
598
+ }
599
+ }
600
+ return null;
601
+ }
602
+ update(dt) {
603
+ const now = Date.now();
604
+ if (now - this.timeLastEnemySpawnedMs > this.enemySpawnIntervalMs) {
605
+ this.timeLastEnemySpawnedMs = now;
606
+ this.spawnEnemy();
607
+ }
608
+ const dead = [];
609
+ for (const enemy of this.enemies) {
610
+ if (enemy.update(dt)) dead.push(enemy);
611
+ }
612
+ this.enemies = this.enemies.filter((e) => !dead.includes(e));
613
+ if (now - this.timeLastRateUpdateMs > 1e3) {
614
+ this.enemySpawnIntervalMs *= Constants.EnemySpawnRateAcceleration;
615
+ this.enemySpeed += Constants.EnemySpeedIncrement;
616
+ this.timeLastRateUpdateMs = now;
617
+ }
618
+ }
619
+ draw(ctx) {
620
+ for (const enemy of this.enemies) {
621
+ enemy.draw(ctx);
622
+ }
623
+ }
624
+ spawnEnemy() {
625
+ const angle = this.getEnemyAngle();
626
+ const speed = (this.gorlorn.screenWidth + this.gorlorn.screenHeight) / 2 * this.enemySpeed;
627
+ const minX = this.gorlorn.screenWidth * 0.2;
628
+ const enemy = new Enemy(this.gorlorn, Images.Enemy);
629
+ enemy.x = minX + Math.random() * this.gorlorn.screenWidth * 0.6;
630
+ enemy.y = this.gorlorn.getYFromPercent(0.025);
631
+ enemy.vx = speed * Math.cos(angle);
632
+ enemy.vy = speed * Math.sin(angle);
633
+ this.enemies.push(enemy);
634
+ }
635
+ getEnemyAngle() {
636
+ const left = Math.random() < 0.5;
637
+ if (left) {
638
+ return Math.PI * 0.2 + Math.random() * Math.PI * 0.2;
639
+ } else {
640
+ return Math.PI * 0.6 + Math.random() * Math.PI * 0.2;
641
+ }
642
+ }
643
+ };
644
+
645
+ // src/Entities/Heart.ts
646
+ var Heart = class extends Entity {
647
+ constructor(gorlorn, sprite, x, y) {
648
+ super(sprite);
649
+ this.timeEnteredPhaseMs = 0;
650
+ this.phase = "Descending";
651
+ this.gorlorn = gorlorn;
652
+ this.x = x;
653
+ this.y = y;
654
+ this.vy = gorlorn.getSpeed(Constants.HeartSpeed);
655
+ }
656
+ update(dt) {
657
+ super.update(dt);
658
+ if (this.gorlorn.hero.testHitEntity(this)) {
659
+ this.gorlorn.gameStats.heartsCollected++;
660
+ this.gorlorn.hero.restoreHealth();
661
+ return true;
662
+ }
663
+ const now = Date.now();
664
+ if (this.phase === "Descending") {
665
+ if (this.y > this.gorlorn.getYFromPercent(0.96)) {
666
+ this.y = this.gorlorn.getYFromPercent(0.96);
667
+ this.timeEnteredPhaseMs = now;
668
+ this.vy = 0;
669
+ this.phase = "Paused";
670
+ }
671
+ } else if (this.phase === "Paused") {
672
+ if (now - this.timeEnteredPhaseMs > 3e3) {
673
+ this.timeEnteredPhaseMs = now;
674
+ this.startBlinking(2e3);
675
+ this.phase = "Blinking";
676
+ }
677
+ } else if (this.phase === "Blinking") {
678
+ if (now - this.timeEnteredPhaseMs > 2e3) {
679
+ return true;
680
+ }
681
+ }
682
+ return false;
683
+ }
684
+ };
685
+
686
+ // src/HeartManager.ts
687
+ var HeartManager = class {
688
+ constructor(gorlorn) {
689
+ this.hearts = [];
690
+ this.gorlorn = gorlorn;
691
+ }
692
+ spawnHeart(x, y) {
693
+ this.hearts.push(new Heart(this.gorlorn, Images.Heart, x, y));
694
+ }
695
+ update(dt) {
696
+ this.hearts = this.hearts.filter((h) => !h.update(dt));
697
+ }
698
+ draw(ctx) {
699
+ for (const heart of this.hearts) {
700
+ heart.draw(ctx);
701
+ }
702
+ }
703
+ };
704
+
705
+ // src/GorlornStats.ts
706
+ var GorlornStats = class _GorlornStats {
707
+ constructor() {
708
+ this.score = 0;
709
+ this.highScore = 0;
710
+ this.shotsFired = 0;
711
+ this.enemiesVanquished = 0;
712
+ this.highestCombo = 0;
713
+ this.gamesPlayed = 0;
714
+ this.heartsCollected = 0;
715
+ this.timePlayedMs = 0;
716
+ }
717
+ add(stats) {
718
+ this.score += stats.score;
719
+ this.highScore = Math.max(this.highScore, stats.score);
720
+ this.highestCombo = Math.max(this.highestCombo, stats.highestCombo);
721
+ this.shotsFired += stats.shotsFired;
722
+ this.enemiesVanquished += stats.enemiesVanquished;
723
+ this.gamesPlayed++;
724
+ this.heartsCollected += stats.heartsCollected;
725
+ this.timePlayedMs += stats.timePlayedMs;
726
+ }
727
+ save() {
728
+ try {
729
+ localStorage.setItem("GorlornStatistics", JSON.stringify(this));
730
+ } catch (e) {
731
+ }
732
+ }
733
+ static load() {
734
+ try {
735
+ const json = localStorage.getItem("GorlornStatistics");
736
+ if (json) {
737
+ const stats = new _GorlornStats();
738
+ Object.assign(stats, JSON.parse(json));
739
+ return stats;
740
+ }
741
+ } catch (e) {
742
+ }
743
+ return new _GorlornStats();
744
+ }
745
+ };
746
+
747
+ // src/Screens/ScreenBase.ts
748
+ var ScreenBase = class {
749
+ constructor(gorlorn) {
750
+ this.gorlorn = gorlorn;
751
+ }
752
+ };
753
+
754
+ // src/UI/Button.ts
755
+ var Button = class {
756
+ constructor(gorlorn, sprite, centerX, centerY) {
757
+ this.isClickedFlag = false;
758
+ this.gorlorn = gorlorn;
759
+ this.sprite = sprite;
760
+ this.x = centerX - sprite.width / 2;
761
+ this.y = centerY - sprite.height / 2;
762
+ }
763
+ isClicked() {
764
+ return this.isClickedFlag;
765
+ }
766
+ update() {
767
+ const hud = this.gorlorn.hud;
768
+ this.isClickedFlag = hud.isClickedFlag && hud.clickX >= this.x && hud.clickX <= this.x + this.sprite.width && hud.clickY >= this.y && hud.clickY <= this.y + this.sprite.height;
769
+ }
770
+ draw(ctx) {
771
+ ctx.drawImage(this.sprite.img, this.x, this.y, this.sprite.width, this.sprite.height);
772
+ }
773
+ };
774
+
775
+ // src/Screens/MenuScreen.ts
776
+ var MenuScreen = class extends ScreenBase {
777
+ constructor(gorlorn) {
778
+ super(gorlorn);
779
+ this.playButton = new Button(gorlorn, Images.ButtonPlay, gorlorn.getXFromPercent(0.5), gorlorn.getYFromPercent(0.46));
780
+ this.statisticsButton = new Button(gorlorn, Images.ButtonStatistics, gorlorn.getXFromPercent(0.5), gorlorn.getYFromPercent(0.6));
781
+ this.aboutButton = new Button(gorlorn, Images.ButtonAbout, gorlorn.getXFromPercent(0.5), gorlorn.getYFromPercent(0.74));
782
+ this.quitButton = new Button(gorlorn, Images.ButtonQuit, gorlorn.getXFromPercent(0.5), gorlorn.getYFromPercent(0.88));
783
+ }
784
+ show(_previousScreen) {
785
+ }
786
+ leave() {
787
+ return true;
788
+ }
789
+ update(_dt) {
790
+ this.playButton.update();
791
+ this.statisticsButton.update();
792
+ this.aboutButton.update();
793
+ this.quitButton.update();
794
+ if (this.playButton.isClicked()) {
795
+ this.gorlorn.startGame();
796
+ } else if (this.statisticsButton.isClicked()) {
797
+ this.gorlorn.showStatistics();
798
+ } else if (this.aboutButton.isClicked()) {
799
+ this.gorlorn.showAboutScreen();
800
+ }
801
+ return false;
802
+ }
803
+ draw(ctx) {
804
+ this.gorlorn.background.draw(ctx);
805
+ const title = Images.Title;
806
+ ctx.drawImage(title.img, this.gorlorn.getXFromPercent(0.15), this.gorlorn.getYFromPercent(0.15), title.width, title.height);
807
+ this.playButton.draw(ctx);
808
+ this.statisticsButton.draw(ctx);
809
+ this.aboutButton.draw(ctx);
810
+ this.quitButton.draw(ctx);
811
+ }
812
+ };
813
+
814
+ // src/UI/HeroSummonEffect.ts
815
+ var HeroSummonEffect = class {
816
+ constructor(gorlorn, comingFromMenu) {
817
+ this.titleOpacity = 1;
818
+ this.heroOpacity = 0;
819
+ this.beamY = 0;
820
+ this.gorlorn = gorlorn;
821
+ this.phase = comingFromMenu ? "FadeOutTitle" : "BeamDescending";
822
+ this.beamSpeed = gorlorn.getYFromPercent(6);
823
+ this.beamWidth = gorlorn.getXFromPercent(0.1);
824
+ this.beamX = (gorlorn.screenWidth - this.beamWidth) / 2;
825
+ }
826
+ update(dt) {
827
+ switch (this.phase) {
828
+ case "FadeOutTitle":
829
+ this.titleOpacity = Math.max(0, this.titleOpacity - dt);
830
+ if (this.titleOpacity <= 0) this.phase = "BeamDescending";
831
+ break;
832
+ case "BeamDescending":
833
+ this.beamY += this.beamSpeed * dt;
834
+ if (this.beamY >= this.gorlorn.screenHeight) {
835
+ this.beamY = this.gorlorn.screenHeight;
836
+ this.phase = "FadeInHero";
837
+ }
838
+ break;
839
+ case "FadeInHero":
840
+ this.heroOpacity = Math.min(1, this.heroOpacity + dt);
841
+ if (this.heroOpacity >= 1) this.phase = "BeamAscending";
842
+ break;
843
+ case "BeamAscending":
844
+ this.beamY -= this.beamSpeed * dt;
845
+ if (this.beamY <= 0) return true;
846
+ break;
847
+ }
848
+ return false;
849
+ }
850
+ draw(ctx) {
851
+ ctx.save();
852
+ if (this.phase === "FadeOutTitle") {
853
+ const title = Images.Title;
854
+ ctx.globalAlpha = this.titleOpacity;
855
+ ctx.drawImage(title.img, this.gorlorn.getXFromPercent(0.15), this.gorlorn.getYFromPercent(0.15), title.width, title.height);
856
+ }
857
+ ctx.globalAlpha = 140 / 255;
858
+ ctx.fillStyle = "rgb(247,255,109)";
859
+ ctx.fillRect(this.beamX, 0, this.beamWidth, this.beamY);
860
+ const hero = this.gorlorn.hero;
861
+ ctx.globalAlpha = this.heroOpacity;
862
+ ctx.drawImage(
863
+ Images.Hero.img,
864
+ hero.x - hero.width / 2,
865
+ hero.y - hero.height / 2,
866
+ hero.width,
867
+ hero.height
868
+ );
869
+ ctx.restore();
870
+ }
871
+ };
872
+
873
+ // src/Screens/GameScreen.ts
874
+ var GameScreen = class extends ScreenBase {
875
+ constructor(gorlorn) {
876
+ super(gorlorn);
877
+ this.heroSummonEffect = null;
878
+ }
879
+ show(previousScreen) {
880
+ const comingFromMenu = previousScreen instanceof MenuScreen;
881
+ this.heroSummonEffect = new HeroSummonEffect(this.gorlorn, comingFromMenu);
882
+ }
883
+ leave() {
884
+ return true;
885
+ }
886
+ update(dt) {
887
+ if (this.heroSummonEffect !== null) {
888
+ if (this.heroSummonEffect.update(dt)) {
889
+ this.heroSummonEffect = null;
890
+ }
891
+ } else {
892
+ this.gorlorn.enemyManager.update(dt);
893
+ this.gorlorn.bulletManager.update(dt);
894
+ this.gorlorn.hero.update(dt);
895
+ this.gorlorn.hud.update(dt);
896
+ this.gorlorn.heartManager.update(dt);
897
+ }
898
+ return false;
899
+ }
900
+ draw(ctx) {
901
+ this.gorlorn.background.draw(ctx);
902
+ this.gorlorn.bulletManager.draw(ctx);
903
+ this.gorlorn.enemyManager.draw(ctx);
904
+ if (this.heroSummonEffect !== null) {
905
+ this.heroSummonEffect.draw(ctx);
906
+ } else {
907
+ this.gorlorn.hud.draw(ctx);
908
+ this.gorlorn.hero.draw(ctx);
909
+ }
910
+ this.gorlorn.heartManager.draw(ctx);
911
+ }
912
+ };
913
+
914
+ // src/UI/FloatingNumbers.ts
915
+ var FloatingNumbers = class {
916
+ constructor(gorlorn, number, y, letterWidthXPercent, letterHeightYPercent, r, g, b) {
917
+ this.letters = [];
918
+ this.age = 0;
919
+ this.gorlorn = gorlorn;
920
+ this.textSize = gorlorn.getYFromPercent(letterHeightYPercent);
921
+ this.color = `rgb(${r},${g},${b})`;
922
+ const s = number.toLocaleString();
923
+ const letterWidth = gorlorn.getXFromPercent(letterWidthXPercent);
924
+ const totalWidth = letterWidth * s.length;
925
+ let x = (gorlorn.screenWidth - totalWidth) * 0.5;
926
+ for (let i = 0; i < s.length; i++) {
927
+ const ch = s[i];
928
+ this.letters.push({ x, baseY: y, y, index: i, char: ch });
929
+ x += ch === "," ? letterWidth * 0.3333 : letterWidth;
930
+ }
931
+ }
932
+ update(dt) {
933
+ this.age += dt;
934
+ const offsetAmt = this.gorlorn.getYFromPercent(0.015);
935
+ for (const letter of this.letters) {
936
+ const adjustedAge = this.age - 50 * letter.index;
937
+ letter.y = letter.baseY + offsetAmt * Math.cos(adjustedAge * 3);
938
+ }
939
+ }
940
+ draw(ctx) {
941
+ ctx.save();
942
+ ctx.font = `${this.textSize}px sans-serif`;
943
+ ctx.fillStyle = this.color;
944
+ ctx.textAlign = "left";
945
+ for (const letter of this.letters) {
946
+ ctx.fillText(letter.char, letter.x, letter.y);
947
+ }
948
+ ctx.restore();
949
+ }
950
+ };
951
+
952
+ // src/Screens/DeathScreen.ts
953
+ var RED_FLASH_DURATION_MS = 90;
954
+ var DeathScreen = class extends ScreenBase {
955
+ constructor(gorlorn, newHighScore) {
956
+ super(gorlorn);
957
+ this.phase = "BackgroundFadeIn";
958
+ this.timeEnteredPhaseMs = 0;
959
+ this.backgroundOpacity = 0;
960
+ this.highScoreFloatingNumbers = null;
961
+ this.highScoreTextSize = gorlorn.getYFromPercent(0.1);
962
+ if (newHighScore) {
963
+ this.highScoreFloatingNumbers = new FloatingNumbers(
964
+ gorlorn,
965
+ gorlorn.gameStats.score,
966
+ gorlorn.getYFromPercent(0.225),
967
+ 0.04,
968
+ 0.12,
969
+ 95,
970
+ 152,
971
+ 234
972
+ );
973
+ }
974
+ }
975
+ show(_previousScreen) {
976
+ this.enterPhase("BackgroundFadeIn");
977
+ }
978
+ leave() {
979
+ return true;
980
+ }
981
+ update(dt) {
982
+ if (this.phase === "BackgroundFadeIn") {
983
+ this.backgroundOpacity = Math.min(1, this.backgroundOpacity + 0.5 * dt);
984
+ if (this.backgroundOpacity >= 1) this.enterPhase("RedFlash");
985
+ } else if (this.phase === "RedFlash") {
986
+ if (Date.now() >= this.timeEnteredPhaseMs + RED_FLASH_DURATION_MS) {
987
+ this.enterPhase("Done");
988
+ }
989
+ } else if (this.phase === "Done") {
990
+ if (this.gorlorn.hud.isClickedFlag) {
991
+ this.gorlorn.startGame();
992
+ }
993
+ if (this.highScoreFloatingNumbers) {
994
+ this.highScoreFloatingNumbers.update(dt);
995
+ }
996
+ }
997
+ return false;
998
+ }
999
+ draw(ctx) {
1000
+ this.gorlorn.background.draw(ctx);
1001
+ this.gorlorn.bulletManager.draw(ctx);
1002
+ this.gorlorn.enemyManager.draw(ctx);
1003
+ this.gorlorn.hero.draw(ctx);
1004
+ ctx.fillStyle = `rgba(0,0,0,${this.backgroundOpacity})`;
1005
+ ctx.fillRect(0, 0, this.gorlorn.screenWidth, this.gorlorn.screenHeight);
1006
+ if (this.phase === "RedFlash") {
1007
+ const flashPercent = Math.min(1, (Date.now() - this.timeEnteredPhaseMs) / RED_FLASH_DURATION_MS);
1008
+ ctx.fillStyle = `rgba(255,0,0,${200 / 255 * flashPercent})`;
1009
+ ctx.fillRect(0, 0, this.gorlorn.screenWidth, this.gorlorn.screenHeight);
1010
+ }
1011
+ if (this.phase === "RedFlash" || this.phase === "Done") {
1012
+ if (this.phase === "Done") {
1013
+ const dt = Images.DeathText;
1014
+ ctx.drawImage(
1015
+ dt.img,
1016
+ (this.gorlorn.screenWidth - dt.width) * 0.5,
1017
+ (this.gorlorn.screenHeight - dt.height) * 0.5,
1018
+ dt.width,
1019
+ dt.height
1020
+ );
1021
+ if (this.highScoreFloatingNumbers) {
1022
+ ctx.save();
1023
+ ctx.font = `${this.highScoreTextSize}px sans-serif`;
1024
+ ctx.fillStyle = "rgb(95,152,234)";
1025
+ ctx.textAlign = "center";
1026
+ ctx.fillText("New High Score!", this.gorlorn.getXFromPercent(0.5), this.gorlorn.getYFromPercent(0.1));
1027
+ ctx.restore();
1028
+ this.highScoreFloatingNumbers.draw(ctx);
1029
+ }
1030
+ }
1031
+ }
1032
+ }
1033
+ enterPhase(phase) {
1034
+ this.phase = phase;
1035
+ this.timeEnteredPhaseMs = Date.now();
1036
+ }
1037
+ };
1038
+
1039
+ // src/Screens/AboutScreen.ts
1040
+ var GRID_ANIMATION_DURATION_MS = 300;
1041
+ var AboutScreen = class extends ScreenBase {
1042
+ constructor(gorlorn) {
1043
+ super(gorlorn);
1044
+ this.timeEnteredMs = 0;
1045
+ this.timeLeftMs = 0;
1046
+ this.versionTextSize = gorlorn.getYFromPercent(0.04);
1047
+ this.messageTextSize = gorlorn.getYFromPercent(0.04);
1048
+ this.backBtnH = gorlorn.getYFromPercent(0.1);
1049
+ this.backBtnW = this.backBtnH * 5;
1050
+ this.backBtnX = gorlorn.getXFromPercent(0.5) - this.backBtnW / 2;
1051
+ this.backBtnY = gorlorn.getYFromPercent(0.82) - this.backBtnH / 2;
1052
+ }
1053
+ show(_previousScreen) {
1054
+ this.timeEnteredMs = Date.now();
1055
+ this.gorlorn.background.retractGrid(GRID_ANIMATION_DURATION_MS);
1056
+ }
1057
+ leave() {
1058
+ this.gorlorn.background.extendGrid(GRID_ANIMATION_DURATION_MS);
1059
+ this.timeLeftMs = Date.now();
1060
+ return false;
1061
+ }
1062
+ update(_dt) {
1063
+ if (this.timeLeftMs === 0 && Date.now() - this.timeEnteredMs > GRID_ANIMATION_DURATION_MS) {
1064
+ const hud = this.gorlorn.hud;
1065
+ const backClicked = hud.isClickedFlag && hud.clickX >= this.backBtnX && hud.clickX <= this.backBtnX + this.backBtnW && hud.clickY >= this.backBtnY && hud.clickY <= this.backBtnY + this.backBtnH;
1066
+ if (backClicked) {
1067
+ this.gorlorn.showMenu();
1068
+ }
1069
+ }
1070
+ return this.timeLeftMs !== 0 && Date.now() - this.timeLeftMs > GRID_ANIMATION_DURATION_MS;
1071
+ }
1072
+ draw(ctx) {
1073
+ this.gorlorn.background.draw(ctx);
1074
+ const title = Images.Title;
1075
+ ctx.drawImage(title.img, this.gorlorn.getXFromPercent(0.15), this.gorlorn.getYFromPercent(0.15), title.width, title.height);
1076
+ if (Date.now() - this.timeEnteredMs > GRID_ANIMATION_DURATION_MS && this.timeLeftMs === 0) {
1077
+ ctx.save();
1078
+ ctx.fillStyle = "white";
1079
+ ctx.textAlign = "left";
1080
+ ctx.font = `${this.versionTextSize}px sans-serif`;
1081
+ ctx.fillText("Version 1.0", this.gorlorn.getXFromPercent(0.15), this.gorlorn.getYFromPercent(0.19) + title.height);
1082
+ ctx.font = `${this.messageTextSize}px sans-serif`;
1083
+ ctx.textAlign = "center";
1084
+ const cx = this.gorlorn.getXFromPercent(0.5);
1085
+ ctx.fillText("This simple game was created for fun and to learn Android", cx, this.gorlorn.getYFromPercent(0.5));
1086
+ ctx.fillText("development. To check out some REAL (and free!) games, go to:", cx, this.gorlorn.getYFromPercent(0.57));
1087
+ ctx.fillText(" www.smileysmazehunt.com", cx, this.gorlorn.getYFromPercent(0.64));
1088
+ ctx.fillStyle = "rgba(20, 30, 50, 0.85)";
1089
+ ctx.strokeStyle = "rgba(150, 200, 255, 0.8)";
1090
+ ctx.lineWidth = Math.max(1, this.backBtnH * 0.05);
1091
+ ctx.fillRect(this.backBtnX, this.backBtnY, this.backBtnW, this.backBtnH);
1092
+ ctx.strokeRect(this.backBtnX, this.backBtnY, this.backBtnW, this.backBtnH);
1093
+ ctx.fillStyle = "white";
1094
+ ctx.font = `${this.backBtnH * 0.45}px sans-serif`;
1095
+ ctx.textAlign = "center";
1096
+ ctx.textBaseline = "middle";
1097
+ ctx.fillText("BACK", this.backBtnX + this.backBtnW / 2, this.backBtnY + this.backBtnH / 2);
1098
+ ctx.restore();
1099
+ }
1100
+ }
1101
+ };
1102
+
1103
+ // src/Screens/StatisticsScreen.ts
1104
+ var BACKGROUND_RETRACT_MS = 300;
1105
+ var StatisticsScreen = class extends ScreenBase {
1106
+ constructor(gorlorn, stats) {
1107
+ super(gorlorn);
1108
+ this.phase = "Entering";
1109
+ this.timeEnteredPhaseMs = 0;
1110
+ this.statsSizePercent = 0;
1111
+ this.stats = stats;
1112
+ }
1113
+ show(_previousScreen) {
1114
+ this.enterPhase("Entering");
1115
+ this.gorlorn.background.retractGrid(BACKGROUND_RETRACT_MS);
1116
+ }
1117
+ leave() {
1118
+ this.gorlorn.background.extendGrid(BACKGROUND_RETRACT_MS);
1119
+ this.enterPhase("Leaving");
1120
+ return false;
1121
+ }
1122
+ update(dt) {
1123
+ const rate = 1e3 / BACKGROUND_RETRACT_MS * dt * 0.9;
1124
+ if (this.phase === "Entering") {
1125
+ this.statsSizePercent = Math.min(1, this.statsSizePercent + rate);
1126
+ if (this.timeInPhase() > BACKGROUND_RETRACT_MS) {
1127
+ this.statsSizePercent = 1;
1128
+ this.enterPhase("ShowingStats");
1129
+ }
1130
+ } else if (this.phase === "Leaving") {
1131
+ this.statsSizePercent -= 1e3 / BACKGROUND_RETRACT_MS * dt;
1132
+ if (this.timeInPhase() > BACKGROUND_RETRACT_MS) {
1133
+ this.statsSizePercent = 0;
1134
+ return true;
1135
+ }
1136
+ }
1137
+ return false;
1138
+ }
1139
+ draw(ctx) {
1140
+ this.gorlorn.background.draw(ctx);
1141
+ const height = this.gorlorn.getYFromPercent(0.6) * this.statsSizePercent;
1142
+ const width = height * 2.4;
1143
+ const x = (this.gorlorn.screenWidth - width) * 0.5;
1144
+ const y = (this.gorlorn.screenHeight - height) * 0.5 + this.gorlorn.getYFromPercent(0.05);
1145
+ this.drawStatsInGrid(ctx, x, y, width, height);
1146
+ }
1147
+ drawStatsInGrid(ctx, x, y, width, height) {
1148
+ const columnWidthPercents = [0.32, 0.26, 0.33, 0.15];
1149
+ const numRows = 5;
1150
+ const rowHeight = height / numRows;
1151
+ const strings = [
1152
+ "High Score: ",
1153
+ this.formatNum(this.stats.highScore),
1154
+ "",
1155
+ "",
1156
+ "Career Score: ",
1157
+ this.formatNum(this.stats.score),
1158
+ "",
1159
+ "",
1160
+ "Highest Combo: ",
1161
+ this.formatNum(this.stats.highestCombo),
1162
+ "Shots Fired: ",
1163
+ this.formatNum(this.stats.shotsFired),
1164
+ "Time Played: ",
1165
+ this.formatTime(this.stats.timePlayedMs),
1166
+ "Enemies Killed: ",
1167
+ this.formatNum(this.stats.enemiesVanquished),
1168
+ "Games Played: ",
1169
+ this.formatNum(this.stats.gamesPlayed),
1170
+ "Hearts Collected: ",
1171
+ this.formatNum(this.stats.heartsCollected)
1172
+ ];
1173
+ ctx.save();
1174
+ ctx.font = `${18}px sans-serif`;
1175
+ ctx.fillStyle = "white";
1176
+ ctx.textAlign = "left";
1177
+ let cy = y;
1178
+ for (let row = 0; row < numRows; row++) {
1179
+ let cx = x;
1180
+ for (let col = 0; col < columnWidthPercents.length; col++) {
1181
+ ctx.fillText(strings[row * columnWidthPercents.length + col], cx, cy);
1182
+ cx += width * columnWidthPercents[col];
1183
+ }
1184
+ cy += rowHeight;
1185
+ }
1186
+ ctx.restore();
1187
+ }
1188
+ formatNum(n) {
1189
+ return n.toLocaleString();
1190
+ }
1191
+ formatTime(ms) {
1192
+ const totalSec = Math.floor(ms / 1e3);
1193
+ const h = Math.floor(totalSec / 3600);
1194
+ const m = Math.floor(totalSec % 3600 / 60);
1195
+ const s = totalSec % 60;
1196
+ return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
1197
+ }
1198
+ timeInPhase() {
1199
+ return Date.now() - this.timeEnteredPhaseMs;
1200
+ }
1201
+ enterPhase(phase) {
1202
+ this.phase = phase;
1203
+ this.timeEnteredPhaseMs = Date.now();
1204
+ }
1205
+ };
1206
+
1207
+ // src/Gorlorn.ts
1208
+ var Gorlorn = class {
1209
+ constructor(screenWidth, screenHeight) {
1210
+ this.gameStats = new GorlornStats();
1211
+ this.activeScreen = null;
1212
+ this.pendingScreen = null;
1213
+ this.isInitialized = false;
1214
+ this.timeStartedMs = 0;
1215
+ this.screenWidth = screenWidth;
1216
+ this.screenHeight = screenHeight;
1217
+ this.cumulativeStats = GorlornStats.load();
1218
+ }
1219
+ // ── Coordinate helpers ────────────────────────────────────────────────
1220
+ getXFromPercent(percent) {
1221
+ return Math.round(this.screenWidth * percent);
1222
+ }
1223
+ getYFromPercent(percent) {
1224
+ return Math.round(this.screenHeight * percent);
1225
+ }
1226
+ getSpeed(screenSizePercentage) {
1227
+ return (this.screenWidth + this.screenHeight) / 2 * screenSizePercentage;
1228
+ }
1229
+ getHighScore() {
1230
+ return this.cumulativeStats.highScore;
1231
+ }
1232
+ // ── Game-flow methods ─────────────────────────────────────────────────
1233
+ startGame() {
1234
+ this.timeStartedMs = Date.now();
1235
+ this.gameStats = new GorlornStats();
1236
+ this.hero = new Hero(this);
1237
+ this.enemyManager = new EnemyManager(this);
1238
+ this.bulletManager = new BulletManager(this);
1239
+ this.heartManager = new HeartManager(this);
1240
+ this.setScreen(new GameScreen(this));
1241
+ }
1242
+ die() {
1243
+ const newHighScore = this.gameStats.score > this.cumulativeStats.highScore;
1244
+ this.gameStats.timePlayedMs = Date.now() - this.timeStartedMs;
1245
+ this.cumulativeStats.add(this.gameStats);
1246
+ this.cumulativeStats.save();
1247
+ this.setScreen(new DeathScreen(this, newHighScore));
1248
+ }
1249
+ showMenu() {
1250
+ if (this.activeScreen instanceof MenuScreen) return;
1251
+ this.setScreen(new MenuScreen(this));
1252
+ }
1253
+ showAboutScreen() {
1254
+ this.setScreen(new AboutScreen(this));
1255
+ }
1256
+ showStatistics() {
1257
+ this.setScreen(new StatisticsScreen(this, this.cumulativeStats));
1258
+ }
1259
+ // Stubs for ad methods (no-ops on web)
1260
+ showAd() {
1261
+ }
1262
+ hideAd() {
1263
+ }
1264
+ // ── Main loop ─────────────────────────────────────────────────────────
1265
+ update(dt) {
1266
+ if (!this.isInitialized) {
1267
+ this.background = new Background(this);
1268
+ this.hud = new HUD(this);
1269
+ this.isInitialized = true;
1270
+ this.setScreen(new MenuScreen(this));
1271
+ return;
1272
+ }
1273
+ this.background.update(dt);
1274
+ if (this.hud.isEscapeDown && !(this.activeScreen instanceof MenuScreen)) {
1275
+ this.showMenu();
1276
+ }
1277
+ if (this.activeScreen === null || this.activeScreen.update(dt)) {
1278
+ if (this.pendingScreen !== null) {
1279
+ this.pendingScreen.show(this.activeScreen);
1280
+ this.activeScreen = this.pendingScreen;
1281
+ this.pendingScreen = null;
1282
+ }
1283
+ }
1284
+ }
1285
+ draw(ctx) {
1286
+ if (!this.isInitialized) {
1287
+ ctx.fillStyle = "rgb(100,100,100)";
1288
+ ctx.fillRect(0, 0, this.screenWidth, this.screenHeight);
1289
+ return;
1290
+ }
1291
+ if (this.activeScreen !== null) {
1292
+ this.activeScreen.draw(ctx);
1293
+ }
1294
+ }
1295
+ // ── Internal ──────────────────────────────────────────────────────────
1296
+ setScreen(screen) {
1297
+ if (this.pendingScreen !== null) return;
1298
+ if (this.activeScreen === null || this.activeScreen.leave()) {
1299
+ screen.show(this.activeScreen);
1300
+ this.activeScreen = screen;
1301
+ } else {
1302
+ this.pendingScreen = screen;
1303
+ }
1304
+ }
1305
+ };
1306
+
1307
+ // src/GorlornGame.tsx
1308
+ import { jsx, jsxs } from "react/jsx-runtime";
1309
+ var GAME_W = 800;
1310
+ var GAME_H = 600;
1311
+ function GorlornGame({ assetBasePath = "/gorlorn" } = {}) {
1312
+ const canvasRef = useRef(null);
1313
+ const containerRef = useRef(null);
1314
+ const [loading, setLoading] = useState(true);
1315
+ const [displaySize, setDisplaySize] = useState({ w: GAME_W, h: GAME_H });
1316
+ useEffect(() => {
1317
+ const container = containerRef.current;
1318
+ if (!container) return;
1319
+ const observer = new ResizeObserver(([entry]) => {
1320
+ const { width, height } = entry.contentRect;
1321
+ const aspect = GAME_W / GAME_H;
1322
+ let w = width, h = width / aspect;
1323
+ if (h > height) {
1324
+ h = height;
1325
+ w = h * aspect;
1326
+ }
1327
+ setDisplaySize({ w: Math.floor(w), h: Math.floor(h) });
1328
+ });
1329
+ observer.observe(container);
1330
+ return () => observer.disconnect();
1331
+ }, []);
1332
+ useEffect(() => {
1333
+ const canvas = canvasRef.current;
1334
+ if (!canvas) return;
1335
+ const W = canvas.width;
1336
+ const H = canvas.height;
1337
+ Images.load(W, H, () => {
1338
+ setLoading(false);
1339
+ const ctx = canvas.getContext("2d");
1340
+ if (!ctx) return;
1341
+ const game = new Gorlorn(W, H);
1342
+ let animFrameId;
1343
+ let lastTime = null;
1344
+ function loop(timestamp) {
1345
+ const dt = lastTime === null ? 0 : (timestamp - lastTime) / 1e3;
1346
+ lastTime = timestamp;
1347
+ game.update(dt);
1348
+ game.draw(ctx);
1349
+ animFrameId = requestAnimationFrame(loop);
1350
+ }
1351
+ animFrameId = requestAnimationFrame(loop);
1352
+ function canvasPoint(e) {
1353
+ const rect = canvas.getBoundingClientRect();
1354
+ const scaleX = W / rect.width;
1355
+ const scaleY = H / rect.height;
1356
+ return {
1357
+ action: e.type === "pointerdown" ? "down" : e.type === "pointerup" ? "up" : "move",
1358
+ id: e.pointerId,
1359
+ x: Math.round((e.clientX - rect.left) * scaleX),
1360
+ y: Math.round((e.clientY - rect.top) * scaleY)
1361
+ };
1362
+ }
1363
+ function onPointerDown(e) {
1364
+ e.preventDefault();
1365
+ canvas.setPointerCapture(e.pointerId);
1366
+ if (game.hud) game.hud.handlePointerEvent(canvasPoint(e));
1367
+ }
1368
+ function onPointerUp(e) {
1369
+ e.preventDefault();
1370
+ if (game.hud) game.hud.handlePointerEvent(canvasPoint(e));
1371
+ }
1372
+ function onPointerMove(e) {
1373
+ e.preventDefault();
1374
+ if (game.hud) game.hud.handlePointerEvent(canvasPoint(e));
1375
+ }
1376
+ function onKeyDown(e) {
1377
+ if (e.key === "ArrowLeft" || e.key === "ArrowRight" || e.key === "Escape") {
1378
+ e.preventDefault();
1379
+ if (game.hud) game.hud.handleKeyDown(e.key);
1380
+ }
1381
+ }
1382
+ function onKeyUp(e) {
1383
+ if (e.key === "ArrowLeft" || e.key === "ArrowRight" || e.key === "Escape") {
1384
+ if (game.hud) game.hud.handleKeyUp(e.key);
1385
+ }
1386
+ }
1387
+ canvas.addEventListener("pointerdown", onPointerDown);
1388
+ canvas.addEventListener("pointerup", onPointerUp);
1389
+ canvas.addEventListener("pointermove", onPointerMove);
1390
+ window.addEventListener("keydown", onKeyDown);
1391
+ window.addEventListener("keyup", onKeyUp);
1392
+ return () => {
1393
+ cancelAnimationFrame(animFrameId);
1394
+ canvas.removeEventListener("pointerdown", onPointerDown);
1395
+ canvas.removeEventListener("pointerup", onPointerUp);
1396
+ canvas.removeEventListener("pointermove", onPointerMove);
1397
+ window.removeEventListener("keydown", onKeyDown);
1398
+ window.removeEventListener("keyup", onKeyUp);
1399
+ };
1400
+ }, assetBasePath);
1401
+ }, []);
1402
+ return /* @__PURE__ */ jsx("div", { ref: containerRef, style: { flex: 1, width: "100%", minHeight: 0, display: "flex", alignItems: "center", justifyContent: "center" }, children: /* @__PURE__ */ jsxs("div", { style: { position: "relative", width: displaySize.w, height: displaySize.h }, children: [
1403
+ /* @__PURE__ */ jsx(
1404
+ "canvas",
1405
+ {
1406
+ ref: canvasRef,
1407
+ width: GAME_W,
1408
+ height: GAME_H,
1409
+ style: { display: "block", touchAction: "none", width: displaySize.w, height: displaySize.h }
1410
+ }
1411
+ ),
1412
+ loading && /* @__PURE__ */ jsx(
1413
+ "div",
1414
+ {
1415
+ style: {
1416
+ position: "absolute",
1417
+ inset: 0,
1418
+ display: "flex",
1419
+ alignItems: "center",
1420
+ justifyContent: "center",
1421
+ background: "rgb(100,100,100)",
1422
+ color: "white",
1423
+ fontSize: 24
1424
+ },
1425
+ children: "Loading\u2026"
1426
+ }
1427
+ )
1428
+ ] }) });
1429
+ }
1430
+ export {
1431
+ GorlornGame
1432
+ };