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