Add knight health, boss attack patterns, chases, and music
Knight now has a hit-bar (pirates deal 1, bosses deal 3 damage) with a new vitality upgrade. Bosses grow bigger/scarier and cycle through melee/missile/bomb/laser attack patterns; boss HP is now exactly 3x the local pirate's. Zones 20 and 40 replace their boss with a unique chase (Pirate Cruiser laser dodge + fuel cell collection; Black Swarm plasma dodge with a tap-to-escape engulf event). Pirates spawn 5x faster and got a bandana/eyepatch redesign. Added a procedural music engine, a visible destination galaxy in the supernova cutscene, and expanded the Data Archive lore to trigger at zones 10/20/40/60/80/100. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D5SuZW1XGCkahPX7cHMoRP
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
0788f42699
commit
799d595e7c
+94
-2
@@ -41,6 +41,8 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding-right: 44px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.hud-hint {
|
||||
@@ -113,6 +115,57 @@
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.hp-bar {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 18px;
|
||||
margin-top: 4px;
|
||||
border-radius: 9px;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hp-bar-fill {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 0%;
|
||||
background: linear-gradient(90deg, #ff5a7a, #7bffb0);
|
||||
transition: width 0.15s ease-out;
|
||||
}
|
||||
|
||||
.hp-bar-text {
|
||||
position: relative;
|
||||
display: block;
|
||||
text-align: center;
|
||||
font-size: 11px;
|
||||
line-height: 18px;
|
||||
color: #fff;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.music-btn {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 16px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
background: rgba(11, 13, 32, 0.6);
|
||||
color: #eef1ff;
|
||||
font-size: 15px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.music-btn:hover {
|
||||
border-color: #5b8cff;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ boss banner */
|
||||
.boss-banner {
|
||||
position: absolute;
|
||||
@@ -168,11 +221,44 @@
|
||||
color: #ffd7de;
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------- chase banner */
|
||||
.chase-banner {
|
||||
position: absolute;
|
||||
top: 76px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 10px 20px;
|
||||
border-radius: 10px;
|
||||
background: rgba(20, 8, 20, 0.6);
|
||||
border: 1px solid rgba(180, 90, 255, 0.5);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.chase-title {
|
||||
font-weight: 700;
|
||||
color: #d7b3ff;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.chase-sub {
|
||||
font-size: 13px;
|
||||
color: #ffdede;
|
||||
}
|
||||
|
||||
.chase-hint {
|
||||
font-size: 12px;
|
||||
color: #c8ceec;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------- shop panel */
|
||||
.shop-panel {
|
||||
flex: 0 0 auto;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
padding: 10px;
|
||||
background: #0b0d20;
|
||||
@@ -258,7 +344,7 @@
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.shop-panel {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.hud-zone {
|
||||
@@ -266,6 +352,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 560px) {
|
||||
.shop-panel {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------- supernova cutscene */
|
||||
.supernova-overlay {
|
||||
position: absolute;
|
||||
|
||||
+5
-1
@@ -1,5 +1,6 @@
|
||||
import { useRef } from 'react';
|
||||
import { BossBanner } from './components/BossBanner';
|
||||
import { ChaseBanner } from './components/ChaseBanner';
|
||||
import { Hud } from './components/Hud';
|
||||
import { LoreModal } from './components/LoreModal';
|
||||
import { ShopPanel } from './components/ShopPanel';
|
||||
@@ -15,8 +16,11 @@ function App() {
|
||||
<div className="game-root">
|
||||
<div className="game-viewport">
|
||||
<canvas ref={canvasRef} className="game-canvas" />
|
||||
{snapshot && !snapshot.cutsceneActive && <Hud snapshot={snapshot} />}
|
||||
{snapshot && !snapshot.cutsceneActive && (
|
||||
<Hud snapshot={snapshot} onToggleMusic={() => engine.current?.toggleMusic()} />
|
||||
)}
|
||||
{snapshot?.bossActive && !snapshot.cutsceneActive && <BossBanner snapshot={snapshot} />}
|
||||
{snapshot?.chaseActive && !snapshot.cutsceneActive && <ChaseBanner snapshot={snapshot} />}
|
||||
{snapshot?.cutsceneActive && <SupernovaOverlay />}
|
||||
{snapshot?.loreActive && snapshot.loreEntry && (
|
||||
<LoreModal entry={snapshot.loreEntry} onDismiss={() => engine.current?.dismissLore()} />
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { GameSnapshot } from '../game/types';
|
||||
|
||||
export function ChaseBanner({ snapshot }: { snapshot: GameSnapshot }) {
|
||||
if (!snapshot.chaseActive) return null;
|
||||
|
||||
if (snapshot.chaseKind === 'cruiser') {
|
||||
return (
|
||||
<div className="chase-banner">
|
||||
<div className="chase-title">Погоня: Пиратский Крейсер</div>
|
||||
<div className="chase-sub">
|
||||
Топливных ячеек: {snapshot.chaseFuelCells}/{snapshot.chaseFuelRequired}
|
||||
</div>
|
||||
<div className="chase-hint">Уворачивайтесь от лазеров и астероидов</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="chase-banner">
|
||||
<div className="chase-title">Погоня: Чёрный Рой</div>
|
||||
<div className="chase-sub">Осталось: {Math.ceil(snapshot.chaseTimeRemaining)} с</div>
|
||||
<div className="chase-hint">Уворачивайтесь от плазмы</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+23
-6
@@ -1,8 +1,9 @@
|
||||
import { formatNumber } from '../game/format';
|
||||
import type { GameSnapshot } from '../game/types';
|
||||
|
||||
export function Hud({ snapshot }: { snapshot: GameSnapshot }) {
|
||||
export function Hud({ snapshot, onToggleMusic }: { snapshot: GameSnapshot; onToggleMusic: () => void }) {
|
||||
const progressPct = Math.min(100, (snapshot.crystalsInZone / snapshot.crystalsRequired) * 100);
|
||||
const hpPct = Math.min(100, (snapshot.knightHp / snapshot.knightMaxHp) * 100);
|
||||
return (
|
||||
<div className="hud">
|
||||
<div className="hud-row">
|
||||
@@ -10,19 +11,32 @@ export function Hud({ snapshot }: { snapshot: GameSnapshot }) {
|
||||
<span className="hud-label">Очки</span>
|
||||
<span className="hud-value">{formatNumber(snapshot.score)}</span>
|
||||
</div>
|
||||
<div className="hud-item">
|
||||
<span className="hud-label">Здоровье</span>
|
||||
<div className="hp-bar">
|
||||
<div className="hp-bar-fill" style={{ width: `${hpPct}%` }} />
|
||||
<span className="hp-bar-text">
|
||||
{Math.ceil(snapshot.knightHp)}/{Math.ceil(snapshot.knightMaxHp)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hud-item hud-zone">
|
||||
<span className="hud-label">Зона {snapshot.zone}</span>
|
||||
{!snapshot.bossActive ? (
|
||||
{snapshot.bossActive ? (
|
||||
<div className="zone-progress zone-progress--boss">
|
||||
<span className="zone-progress-text">Битва с боссом</span>
|
||||
</div>
|
||||
) : snapshot.chaseActive ? (
|
||||
<div className="zone-progress zone-progress--boss">
|
||||
<span className="zone-progress-text">Погоня!</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="zone-progress">
|
||||
<div className="zone-progress-fill" style={{ width: `${progressPct}%` }} />
|
||||
<span className="zone-progress-text">
|
||||
{snapshot.crystalsInZone}/{snapshot.crystalsRequired} кристаллов
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="zone-progress zone-progress--boss">
|
||||
<span className="zone-progress-text">Битва с боссом</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="hud-item">
|
||||
@@ -31,6 +45,9 @@ export function Hud({ snapshot }: { snapshot: GameSnapshot }) {
|
||||
</div>
|
||||
</div>
|
||||
<div className="hud-hint">Тап по экрану — полёт · уворачивайтесь от астероидов</div>
|
||||
<button type="button" className="music-btn" onClick={onToggleMusic}>
|
||||
{snapshot.musicMuted ? '\u{1F507}' : '\u{1F3B5}'}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { UPGRADES } from '../game/constants';
|
||||
import { formatNumber } from '../game/format';
|
||||
import type { GameSnapshot, UpgradeKey } from '../game/types';
|
||||
|
||||
const KEYS: UpgradeKey[] = ['crystal', 'speed', 'damage', 'droid'];
|
||||
const KEYS: UpgradeKey[] = ['crystal', 'speed', 'damage', 'droid', 'vitality'];
|
||||
|
||||
export function ShopPanel({
|
||||
snapshot,
|
||||
|
||||
+792
-36
File diff suppressed because it is too large
Load Diff
+64
-8
@@ -4,8 +4,8 @@ export const WORLD = {
|
||||
CRYSTALS_PER_ZONE: 10,
|
||||
MAX_CRYSTALS_ON_SCREEN: 6,
|
||||
CRYSTAL_SPAWN_INTERVAL: 1.1,
|
||||
PIRATE_SPAWN_INTERVAL: 15,
|
||||
BOSS_HP_MULT: 5,
|
||||
PIRATE_SPAWN_INTERVAL: 3,
|
||||
BOSS_HP_MULT: 3,
|
||||
CONTACT_RANGE: 46,
|
||||
COLLECT_RANGE: 26,
|
||||
PIRATE_HP_ZONE_MULT: 1.2,
|
||||
@@ -34,6 +34,39 @@ export const WORLD = {
|
||||
GOLDEN_ASTEROID_REWARD_MULT: 20,
|
||||
SUPERNOVA_DURATION: 10,
|
||||
SUPERNOVA_DARK_MATTER_GAIN: 1,
|
||||
BASE_KNIGHT_MAX_HP: 10,
|
||||
PIRATE_CONTACT_DAMAGE: 1,
|
||||
BOSS_HIT_DAMAGE: 3,
|
||||
HIT_INVULN_DURATION: 0.6,
|
||||
BOSS_BASE_RADIUS: 48,
|
||||
BOSS_RADIUS_PER_TIER: 6,
|
||||
BOSS_MAX_RADIUS: 96,
|
||||
BOSS_MELEE_ATTACK_INTERVAL: 1.3,
|
||||
BOSS_MISSILE_INTERVAL: 2.4,
|
||||
BOSS_MISSILE_SPEED: 150,
|
||||
BOSS_MISSILE_RADIUS: 9,
|
||||
BOSS_BOMB_INTERVAL: 3.4,
|
||||
BOSS_BOMB_TELEGRAPH: 1.3,
|
||||
BOSS_BOMB_EXPLODE_DURATION: 0.35,
|
||||
BOSS_BOMB_RADIUS: 62,
|
||||
BOSS_LASER_ROTATE_SPEED: 1.6,
|
||||
BOSS_LASER_LENGTH: 420,
|
||||
BOSS_LASER_WIDTH: 16,
|
||||
CHASE_LASER_INTERVAL: 2,
|
||||
CHASE_LASER_SPEED: 260,
|
||||
CHASE_LASER_RADIUS: 8,
|
||||
CHASE_LASER_DAMAGE_RATIO: 0.15,
|
||||
CHASE_PLASMA_INTERVAL: 1.6,
|
||||
CHASE_PLASMA_SPEED: 220,
|
||||
CHASE_PLASMA_RADIUS: 12,
|
||||
CHASE_PLASMA_DAMAGE_RATIO: 0.2,
|
||||
CRUISER_FUEL_CELLS_REQUIRED: 10,
|
||||
FUEL_CELL_MAX_ON_SCREEN: 4,
|
||||
FUEL_CELL_SPAWN_INTERVAL: 1.4,
|
||||
SWARM_DURATION: 40,
|
||||
SWARM_ENGULF_INTERVAL: 10,
|
||||
SWARM_ESCAPE_FILL_PER_TAP: 0.22,
|
||||
SWARM_ESCAPE_DECAY_PER_SEC: 0.12,
|
||||
} as const;
|
||||
|
||||
export const UPGRADES: Record<UpgradeKey, { title: string; desc: string; icon: string; baseCost: number; growth: number; bonus: number }> = {
|
||||
@@ -69,6 +102,14 @@ export const UPGRADES: Record<UpgradeKey, { title: string; desc: string; icon: s
|
||||
growth: 1.65,
|
||||
bonus: 0,
|
||||
},
|
||||
vitality: {
|
||||
title: 'Укрепление Скафандра',
|
||||
desc: '+50% к максимальному здоровью, лечит при покупке',
|
||||
icon: '\u{1F6E1}\u{FE0F}',
|
||||
baseCost: 50,
|
||||
growth: 1.2,
|
||||
bonus: 0.5,
|
||||
},
|
||||
};
|
||||
|
||||
export interface DroidTierConfig {
|
||||
@@ -92,16 +133,31 @@ export const LORE_ENTRIES: LoreEntry[] = [
|
||||
{
|
||||
zone: 10,
|
||||
title: 'Кто такой Рыцарь',
|
||||
text: 'Архив Данных: Сканирование памяти завершено. Этот Рыцарь — последний уцелевший пилот из древнего Ордена Звёздных Защитников. Его цивилизация исчезла много веков назад, но его высокотехнологичный скафандр сохранил пилота в режиме анабиоза, чтобы он исполнил финальную миссию.',
|
||||
text: 'Архив Данных: Сканирование памяти завершено. Этот Рыцарь — последний уцелевший пилот из древнего Ордена Звёздных Защитников. Его цивилизация исчезла много веков назад, но высокотехнологичный скафандр сохранил пилота в режиме анабиоза, чтобы он исполнил финальную миссию.',
|
||||
},
|
||||
{
|
||||
zone: 20,
|
||||
title: 'Что за кристаллы',
|
||||
text: 'Архив Данных: Обнаружена информация о кристаллах. Это не просто камни, а Хроно-Кристаллы — спрессованная чистая энергия погибших звёзд. В них записана история и координаты древней столицы галактики. Рыцарь собирает их, чтобы запустить Главный Звёздный Реактор.',
|
||||
title: 'Крейсер уничтожен',
|
||||
text: 'Архив Данных: Крейсер уничтожен! Вы смогли расшифровать данные с его бортового компьютера. Оказывается, Космические Кристаллы — это Хроно-Кристаллы, спрессованная энергия погибших звёзд. Рыцарь собирает их, чтобы запустить Главный Звёздный Реактор.',
|
||||
},
|
||||
{
|
||||
zone: 30,
|
||||
title: 'Кто такие Пираты',
|
||||
text: 'Архив Данных: Анализ противника завершен. Пираты-Желейки — это биомеханические дроны, созданные заброшенными автоматическими заводами Тёмной туманности. У них нет разума, их цель — поглощать энергию Хроно-Кристаллов, чтобы строить новые армии. Их нужно остановить любой ценой!',
|
||||
zone: 40,
|
||||
title: 'Вырвались из Облака',
|
||||
text: 'Архив Данных: Вы вырвались из Черного Облака! Это нанороботы ИИ "Ноль", который уничтожает галактику. Но теперь ваша ракета при Взрыве Сверхновой точно знает путь в параллельные измерения. Каждая Тёмная Материя — это ключ к скрытым базам Ордена, где спрятано мощное оружие.',
|
||||
},
|
||||
{
|
||||
zone: 60,
|
||||
title: 'Сверхразум "Ноль"',
|
||||
text: 'Архив Данных: Поймана закодированная радиопередача. Пираты-Желейки не просто собирают кристаллы. Оказывается, глубоко в Тёмной туманности ожил Сверхразум — заброшенный военный ИИ под названием "Ноль". Это он отправляет дронов и пытается перегрузить реакторы галактики.',
|
||||
},
|
||||
{
|
||||
zone: 80,
|
||||
title: 'Тайна скафандра',
|
||||
text: 'Архив Данных: Обнаружен скрытый лог-файл в шлеме Рыцаря. Высокотехнологичный скафандр был создан не для войны. Это спасательная капсула, внутри которой запечатано сознание создателя Ордена Защитников. Костюм защищает пилота, пока тот собирает энергию.',
|
||||
},
|
||||
{
|
||||
zone: 100,
|
||||
title: 'Врата к Сердцу Галактики',
|
||||
text: 'Архив Данных: Звёздный Реактор активирован на 100%! Вы открыли врата к Сердцу Галактики. Впереди виден Древний Город, но он полностью окружен армадой ИИ "Ноль". Нажмите "Взрыв Сверхновой", чтобы совершить решающий прыжок с силой Тёмной Материи!',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
type Note = [frequency: number, duration: number];
|
||||
|
||||
const MELODY: Note[] = [
|
||||
[392.0, 0.25],
|
||||
[440.0, 0.25],
|
||||
[523.25, 0.25],
|
||||
[440.0, 0.25],
|
||||
[349.23, 0.25],
|
||||
[392.0, 0.25],
|
||||
[329.63, 0.25],
|
||||
[392.0, 0.5],
|
||||
[440.0, 0.25],
|
||||
[523.25, 0.25],
|
||||
[587.33, 0.25],
|
||||
[523.25, 0.25],
|
||||
[440.0, 0.25],
|
||||
[392.0, 0.25],
|
||||
[349.23, 0.25],
|
||||
[329.63, 0.5],
|
||||
];
|
||||
|
||||
const BASS: Note[] = [
|
||||
[130.81, 1],
|
||||
[174.61, 1],
|
||||
[146.83, 1],
|
||||
[164.81, 1],
|
||||
];
|
||||
|
||||
export class MusicEngine {
|
||||
private ctx: AudioContext | null = null;
|
||||
private gain: GainNode | null = null;
|
||||
private muted = false;
|
||||
private loopHandle: number | null = null;
|
||||
|
||||
private ensureContext() {
|
||||
if (!this.ctx) {
|
||||
const Ctx = window.AudioContext ?? (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext;
|
||||
this.ctx = new Ctx();
|
||||
this.gain = this.ctx.createGain();
|
||||
this.gain.gain.value = this.muted ? 0 : 0.09;
|
||||
this.gain.connect(this.ctx.destination);
|
||||
}
|
||||
if (this.ctx.state === 'suspended') {
|
||||
void this.ctx.resume();
|
||||
}
|
||||
}
|
||||
|
||||
start() {
|
||||
this.ensureContext();
|
||||
if (this.loopHandle !== null) return;
|
||||
this.scheduleLoop();
|
||||
}
|
||||
|
||||
private scheduleLoop = () => {
|
||||
const ctx = this.ctx;
|
||||
const gain = this.gain;
|
||||
if (!ctx || !gain) return;
|
||||
|
||||
const startTime = ctx.currentTime + 0.05;
|
||||
let t = startTime;
|
||||
for (const [freq, dur] of MELODY) {
|
||||
this.playNote(freq, t, dur * 0.85, 'square', 0.55);
|
||||
t += dur;
|
||||
}
|
||||
const loopDuration = t - startTime;
|
||||
|
||||
let bt = startTime;
|
||||
while (bt < startTime + loopDuration) {
|
||||
for (const [freq, dur] of BASS) {
|
||||
if (bt >= startTime + loopDuration) break;
|
||||
this.playNote(freq, bt, dur * 0.9, 'triangle', 0.7);
|
||||
bt += dur;
|
||||
}
|
||||
}
|
||||
|
||||
this.loopHandle = window.setTimeout(() => {
|
||||
this.loopHandle = null;
|
||||
this.scheduleLoop();
|
||||
}, loopDuration * 1000);
|
||||
};
|
||||
|
||||
private playNote(freq: number, time: number, dur: number, type: OscillatorType, level: number) {
|
||||
const ctx = this.ctx;
|
||||
const gain = this.gain;
|
||||
if (!ctx || !gain) return;
|
||||
const osc = ctx.createOscillator();
|
||||
const noteGain = ctx.createGain();
|
||||
osc.type = type;
|
||||
osc.frequency.value = freq;
|
||||
noteGain.gain.setValueAtTime(0, time);
|
||||
noteGain.gain.linearRampToValueAtTime(level, time + 0.02);
|
||||
noteGain.gain.linearRampToValueAtTime(0, time + dur);
|
||||
osc.connect(noteGain);
|
||||
noteGain.connect(gain);
|
||||
osc.start(time);
|
||||
osc.stop(time + dur + 0.05);
|
||||
}
|
||||
|
||||
toggleMute(): boolean {
|
||||
this.muted = !this.muted;
|
||||
if (this.gain) this.gain.gain.value = this.muted ? 0 : 0.09;
|
||||
return this.muted;
|
||||
}
|
||||
|
||||
isMuted(): boolean {
|
||||
return this.muted;
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.loopHandle !== null) {
|
||||
window.clearTimeout(this.loopHandle);
|
||||
this.loopHandle = null;
|
||||
}
|
||||
this.ctx?.close();
|
||||
this.ctx = null;
|
||||
this.gain = null;
|
||||
}
|
||||
}
|
||||
+52
-1
@@ -1,4 +1,6 @@
|
||||
export type UpgradeKey = 'crystal' | 'speed' | 'damage' | 'droid';
|
||||
export type UpgradeKey = 'crystal' | 'speed' | 'damage' | 'droid' | 'vitality';
|
||||
|
||||
export type BossType = 'melee' | 'missile' | 'bomb' | 'laser';
|
||||
|
||||
export interface CrystalEntity {
|
||||
id: number;
|
||||
@@ -56,6 +58,45 @@ export interface GoldenAsteroidEntity {
|
||||
jitter: number[];
|
||||
}
|
||||
|
||||
export interface BossProjectile {
|
||||
id: number;
|
||||
x: number;
|
||||
y: number;
|
||||
vx: number;
|
||||
vy: number;
|
||||
radius: number;
|
||||
}
|
||||
|
||||
export interface BossBomb {
|
||||
x: number;
|
||||
y: number;
|
||||
radius: number;
|
||||
state: 'telegraph' | 'boom';
|
||||
timer: number;
|
||||
}
|
||||
|
||||
export type ChaseKind = 'cruiser' | 'swarm';
|
||||
|
||||
export interface FuelCellEntity {
|
||||
id: number;
|
||||
x: number;
|
||||
y: number;
|
||||
radius: number;
|
||||
rotation: number;
|
||||
spin: number;
|
||||
jitter: number[];
|
||||
}
|
||||
|
||||
export interface ChaseProjectile {
|
||||
id: number;
|
||||
kind: 'laser' | 'plasma';
|
||||
x: number;
|
||||
y: number;
|
||||
vx: number;
|
||||
vy: number;
|
||||
radius: number;
|
||||
}
|
||||
|
||||
export interface Particle {
|
||||
x: number;
|
||||
y: number;
|
||||
@@ -104,4 +145,14 @@ export interface GameSnapshot {
|
||||
cutsceneActive: boolean;
|
||||
loreActive: boolean;
|
||||
loreEntry: LoreEntry | null;
|
||||
knightHp: number;
|
||||
knightMaxHp: number;
|
||||
musicMuted: boolean;
|
||||
chaseActive: boolean;
|
||||
chaseKind: ChaseKind | null;
|
||||
chaseFuelCells: number;
|
||||
chaseFuelRequired: number;
|
||||
chaseTimeRemaining: number;
|
||||
chaseEngulfed: boolean;
|
||||
chaseEscapeMeter: number;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user