void }) {
+ return (
+
+
e.stopPropagation()}>
+
Архив Данных
+
{entry.title}
+
{entry.text}
+
+
+
+ );
+}
diff --git a/src/components/ShopPanel.tsx b/src/components/ShopPanel.tsx
index d4a4b28..91ec2ee 100644
--- a/src/components/ShopPanel.tsx
+++ b/src/components/ShopPanel.tsx
@@ -13,7 +13,7 @@ export function ShopPanel({
onBuy: (key: UpgradeKey) => void;
onPrestige: () => void;
}) {
- const locked = snapshot.cutsceneActive;
+ const locked = snapshot.cutsceneActive || snapshot.loreActive;
return (
{KEYS.map((key) => {
diff --git a/src/game/GameEngine.ts b/src/game/GameEngine.ts
index 3584c8f..5b44708 100644
--- a/src/game/GameEngine.ts
+++ b/src/game/GameEngine.ts
@@ -1,4 +1,4 @@
-import { WORLD, UPGRADES, DROID_TIERS, type DroidTierConfig } from './constants';
+import { WORLD, UPGRADES, DROID_TIERS, LORE_ENTRIES, type DroidTierConfig } from './constants';
import { formatNumber } from './format';
import { makeJitter, mulberry32 } from './random';
import type {
@@ -9,6 +9,7 @@ import type {
FloatingText,
GameSnapshot,
GoldenAsteroidEntity,
+ LoreEntry,
Particle,
UpgradeKey,
} from './types';
@@ -21,6 +22,7 @@ interface SaveData {
crystalsInZone: number;
darkMatter: number;
levels: Record;
+ seenLore?: number[];
}
let idCounter = 1;
@@ -79,6 +81,8 @@ export class GameEngine {
private moveTarget: { x: number; y: number } | null = null;
private cutscene = { active: false, timer: 0 };
+ private lore: { active: boolean; entry: LoreEntry | null } = { active: false, entry: null };
+ private seenLore = new Set();
private knight: KnightState = {
x: 0,
@@ -173,7 +177,7 @@ export class GameEngine {
// ------------------------------------------------------------------ input
private handlePointerDown(e: PointerEvent) {
- if (this.cutscene.active) return;
+ if (this.cutscene.active || this.lore.active) return;
const rect = this.canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
@@ -230,7 +234,7 @@ export class GameEngine {
}
buyUpgrade(key: UpgradeKey): boolean {
- if (this.cutscene.active) return false;
+ if (this.cutscene.active || this.lore.active) return false;
const cost = this.cost(key);
if (this.score < cost) return false;
this.score -= cost;
@@ -246,7 +250,7 @@ export class GameEngine {
}
prestige(): boolean {
- if (this.cutscene.active || this.zone < WORLD.PRESTIGE_MIN_ZONE) return false;
+ if (this.cutscene.active || this.lore.active || this.zone < WORLD.PRESTIGE_MIN_ZONE) return false;
this.cutscene.active = true;
this.cutscene.timer = 0;
this.moveTarget = null;
@@ -254,6 +258,13 @@ export class GameEngine {
return true;
}
+ dismissLore(): void {
+ if (!this.lore.active) return;
+ this.lore.active = false;
+ this.lore.entry = null;
+ this.notify();
+ }
+
private applyPrestigeReset() {
this.darkMatter += WORLD.SUPERNOVA_DARK_MATTER_GAIN;
this.score = 0;
@@ -479,6 +490,8 @@ export class GameEngine {
if (this.cutscene.active) {
this.updateCutscene(dt);
+ } else if (this.lore.active) {
+ // frozen: the story archive modal pauses the world until dismissed
} else {
this.updateAsteroids(dt);
if (this.boss) this.updateBossMode(dt);
@@ -488,10 +501,12 @@ export class GameEngine {
for (const c of this.crystals) c.rotation += c.spin * dt;
}
- this.updateParticles(dt);
- if (this.pirate && this.pirate.hitFlash > 0) this.pirate.hitFlash = Math.max(0, this.pirate.hitFlash - dt * 3);
- if (this.boss && this.boss.hitFlash > 0) this.boss.hitFlash = Math.max(0, this.boss.hitFlash - dt * 3);
- if (this.knight.hitFlash > 0) this.knight.hitFlash = Math.max(0, this.knight.hitFlash - dt * 3);
+ if (!this.lore.active) {
+ this.updateParticles(dt);
+ if (this.pirate && this.pirate.hitFlash > 0) this.pirate.hitFlash = Math.max(0, this.pirate.hitFlash - dt * 3);
+ if (this.boss && this.boss.hitFlash > 0) this.boss.hitFlash = Math.max(0, this.boss.hitFlash - dt * 3);
+ if (this.knight.hitFlash > 0) this.knight.hitFlash = Math.max(0, this.knight.hitFlash - dt * 3);
+ }
this.snapshotTimer += dt;
if (this.snapshotTimer > 0.12) {
@@ -734,12 +749,22 @@ export class GameEngine {
this.knight.locked = false;
}
if (this.boss && this.boss.hp <= 0) {
+ const defeatedZone = this.zone;
this.boss = null;
this.zone += 1;
this.crystalsInZone = 0;
this.pirateSpawnTimer = 0;
this.crystalSpawnTimer = 0;
if (this.zone % 10 === 0) this.startBoss();
+
+ const entry = LORE_ENTRIES.find((e) => e.zone === defeatedZone);
+ if (entry && !this.seenLore.has(entry.zone)) {
+ this.seenLore.add(entry.zone);
+ this.lore.active = true;
+ this.lore.entry = entry;
+ this.moveTarget = null;
+ }
+
this.save();
}
}
@@ -1182,6 +1207,8 @@ export class GameEngine {
canPrestige: this.zone >= WORLD.PRESTIGE_MIN_ZONE,
prestigeGain: WORLD.SUPERNOVA_DARK_MATTER_GAIN,
cutsceneActive: this.cutscene.active,
+ loreActive: this.lore.active,
+ loreEntry: this.lore.entry,
};
}
@@ -1194,6 +1221,7 @@ export class GameEngine {
crystalsInZone: this.crystalsInZone,
darkMatter: this.darkMatter,
levels: { ...this.levels },
+ seenLore: Array.from(this.seenLore),
};
localStorage.setItem(SAVE_KEY, JSON.stringify(data));
} catch {
@@ -1216,6 +1244,7 @@ export class GameEngine {
damage: data.levels?.damage ?? 1,
droid: data.levels?.droid ?? 1,
};
+ this.seenLore = new Set(data.seenLore ?? []);
} catch {
// corrupted save, start fresh
}
diff --git a/src/game/constants.ts b/src/game/constants.ts
index 7d691f0..4e8d388 100644
--- a/src/game/constants.ts
+++ b/src/game/constants.ts
@@ -1,4 +1,4 @@
-import type { UpgradeKey } from './types';
+import type { LoreEntry, UpgradeKey } from './types';
export const WORLD = {
CRYSTALS_PER_ZONE: 10,
@@ -8,7 +8,7 @@ export const WORLD = {
BOSS_HP_MULT: 5,
CONTACT_RANGE: 46,
COLLECT_RANGE: 26,
- PIRATE_HP_ZONE_MULT: 1.4,
+ PIRATE_HP_ZONE_MULT: 1.2,
CRYSTAL_VALUE_ZONE_MULT: 1.25,
BASE_PIRATE_HP: 24,
BASE_PIRATE_SPEED: 72,
@@ -87,3 +87,21 @@ export const DROID_TIERS: DroidTierConfig[] = [
{ minLevel: 10, radius: 15, spikes: 10, color: '#ff5ad1', stroke: '#8a1b6e', laserWidth: 4, glow: 12 },
{ minLevel: 20, radius: 19, spikes: 12, color: '#ffffff', stroke: '#c81b6e', laserWidth: 6, glow: 20 },
];
+
+export const LORE_ENTRIES: LoreEntry[] = [
+ {
+ zone: 10,
+ title: 'Кто такой Рыцарь',
+ text: 'Архив Данных: Сканирование памяти завершено. Этот Рыцарь — последний уцелевший пилот из древнего Ордена Звёздных Защитников. Его цивилизация исчезла много веков назад, но его высокотехнологичный скафандр сохранил пилота в режиме анабиоза, чтобы он исполнил финальную миссию.',
+ },
+ {
+ zone: 20,
+ title: 'Что за кристаллы',
+ text: 'Архив Данных: Обнаружена информация о кристаллах. Это не просто камни, а Хроно-Кристаллы — спрессованная чистая энергия погибших звёзд. В них записана история и координаты древней столицы галактики. Рыцарь собирает их, чтобы запустить Главный Звёздный Реактор.',
+ },
+ {
+ zone: 30,
+ title: 'Кто такие Пираты',
+ text: 'Архив Данных: Анализ противника завершен. Пираты-Желейки — это биомеханические дроны, созданные заброшенными автоматическими заводами Тёмной туманности. У них нет разума, их цель — поглощать энергию Хроно-Кристаллов, чтобы строить новые армии. Их нужно остановить любой ценой!',
+ },
+];
diff --git a/src/game/types.ts b/src/game/types.ts
index 7da624e..3fb75e4 100644
--- a/src/game/types.ts
+++ b/src/game/types.ts
@@ -77,6 +77,12 @@ export interface FloatingText {
color: string;
}
+export interface LoreEntry {
+ zone: number;
+ title: string;
+ text: string;
+}
+
export interface GameSnapshot {
score: number;
zone: number;
@@ -96,4 +102,6 @@ export interface GameSnapshot {
canPrestige: boolean;
prestigeGain: number;
cutsceneActive: boolean;
+ loreActive: boolean;
+ loreEntry: LoreEntry | null;
}