Загрузка данных
// CrashAviatorVisual.ts
//
// PixiJS 8 / TypeScript
//
// ВАЖНО:
// Этот класс НЕ содержит игровую механику.
// Он только визуализирует уже существующий Crash.
//
// Подключение к существующей игре:
//
// const visual = await CrashAviatorVisual.mount(gameFieldElement);
//
// когда реально начинается раунд:
// visual.startRound();
//
// когда обновляется существующий multiplier:
// visual.setMultiplier(currentMultiplier);
//
// когда существующая игра сообщает CRASH:
// visual.crash();
//
// перед следующим раундом:
// visual.reset();
//
// при размонтировании страницы:
// visual.destroy();
import {
Application,
Container,
Graphics,
Text,
} from 'pixi.js';
type CrashVisualState = 'idle' | 'flying' | 'crashed';
interface CrashVisualOptions {
showMultiplier?: boolean;
maxVisualMultiplier?: number;
}
interface Debris {
view: Graphics;
depth: number;
vx: number;
vy: number;
rotationSpeed: number;
}
interface Particle {
view: Graphics;
depth: number;
vx: number;
vy: number;
}
const clamp = (value: number, min: number, max: number) =>
Math.max(min, Math.min(max, value));
const lerp = (a: number, b: number, t: number) =>
a + (b - a) * t;
const easeOut = (t: number) =>
1 - Math.pow(1 - t, 3);
const easeInCubic = (t: number) =>
t * t * t;
export class CrashAviatorVisual {
private app!: Application;
private readonly host: HTMLElement;
private readonly root = new Container();
private readonly background = new Graphics();
private readonly ambient = new Graphics();
private readonly grid = new Graphics();
private readonly graphOuter = new Graphics();
private readonly graphMiddle = new Graphics();
private readonly graphMain = new Graphics();
private readonly graphPointGlow = new Graphics();
private readonly graphPoint = new Graphics();
private readonly debrisLayer = new Container();
private readonly particlesLayer = new Container();
private readonly rocket = new Container();
private readonly rocketTrail = new Graphics();
private readonly rocketEngine = new Graphics();
private readonly rocketBody = new Container();
private readonly multiplierText: Text;
private readonly debris: Debris[] = [];
private readonly particles: Particle[] = [];
private readonly options: Required<CrashVisualOptions>;
private state: CrashVisualState = 'idle';
private targetMultiplier = 1;
private displayMultiplier = 1;
private visualProgress = 0;
private targetProgress = 0;
private elapsed = 0;
private crashElapsed = 0;
private crashStartX = 0;
private crashStartY = 0;
private crashStartRotation = 0;
private rocketX = 0;
private rocketY = 0;
private rocketRotation = 0;
private width = 1;
private height = 1;
private resizeObserver?: ResizeObserver;
private constructor(
host: HTMLElement,
options: CrashVisualOptions = {},
) {
this.host = host;
this.options = {
showMultiplier: options.showMultiplier ?? true,
maxVisualMultiplier: options.maxVisualMultiplier ?? 50,
};
this.multiplierText = new Text({
text: '1.00x',
style: {
fill: 0xffffff,
fontFamily: 'Inter, Arial, sans-serif',
fontSize: 72,
fontWeight: '800',
letterSpacing: -2,
},
});
this.multiplierText.anchor.set(0.5);
}
static async mount(
host: HTMLElement,
options?: CrashVisualOptions,
): Promise<CrashAviatorVisual> {
const instance = new CrashAviatorVisual(host, options);
await instance.init();
return instance;
}
private async init(): Promise<void> {
this.app = new Application();
await this.app.init({
resizeTo: this.host,
antialias: true,
autoDensity: true,
resolution: Math.min(window.devicePixelRatio || 1, 2),
backgroundAlpha: 0,
});
this.host.style.position ||= 'relative';
this.host.style.overflow = 'hidden';
this.app.canvas.style.position = 'absolute';
this.app.canvas.style.inset = '0';
this.app.canvas.style.width = '100%';
this.app.canvas.style.height = '100%';
this.host.appendChild(this.app.canvas);
this.app.stage.addChild(this.root);
this.root.addChild(
this.background,
this.ambient,
this.grid,
this.graphOuter,
this.graphMiddle,
this.graphMain,
this.debrisLayer,
this.particlesLayer,
this.graphPointGlow,
this.graphPoint,
this.rocket,
);
if (this.options.showMultiplier) {
this.root.addChild(this.multiplierText);
}
this.createRocket();
this.createDebris();
this.createParticles();
this.resize();
this.resizeObserver = new ResizeObserver(() => {
this.resize();
});
this.resizeObserver.observe(this.host);
this.app.ticker.add(this.tick);
}
// ============================================================
// PUBLIC API
// ============================================================
public startRound(): void {
this.state = 'flying';
this.targetMultiplier = 1;
this.displayMultiplier = 1;
this.targetProgress = 0;
this.visualProgress = 0;
this.crashElapsed = 0;
this.rocket.visible = true;
this.rocket.alpha = 1;
this.drawGraph(0);
}
public setMultiplier(multiplier: number): void {
if (!Number.isFinite(multiplier)) return;
this.targetMultiplier = Math.max(1, multiplier);
if (this.state !== 'flying') return;
this.targetProgress =
this.progressFromMultiplier(this.targetMultiplier);
}
public crash(): void {
if (this.state === 'crashed') return;
this.state = 'crashed';
this.crashElapsed = 0;
this.crashStartX = this.rocket.x;
this.crashStartY = this.rocket.y;
this.crashStartRotation = this.rocket.rotation;
}
public reset(): void {
this.state = 'idle';
this.targetMultiplier = 1;
this.displayMultiplier = 1;
this.targetProgress = 0;
this.visualProgress = 0;
this.multiplierText.text = '1.00x';
this.rocket.visible = true;
this.rocket.alpha = 1;
this.positionRocket(0);
this.drawGraph(0);
}
public destroy(): void {
this.resizeObserver?.disconnect();
this.app.ticker.remove(this.tick);
this.app.destroy(true);
}
// ============================================================
// MAIN LOOP
// ============================================================
private tick = (ticker: { deltaMS: number }): void => {
const dt = Math.min(ticker.deltaMS / 1000, 0.05);
this.elapsed += dt;
if (this.state === 'flying') {
this.updateFlying(dt);
}
if (this.state === 'crashed') {
this.updateCrash(dt);
}
if (this.state === 'idle') {
this.updateIdle();
}
this.updateParticles(dt);
this.updateDebris(dt);
};
private updateFlying(dt: number): void {
// Плавно догоняем реальный multiplier.
this.displayMultiplier +=
(this.targetMultiplier - this.displayMultiplier) *
Math.min(1, dt * 12);
this.visualProgress +=
(this.targetProgress - this.visualProgress) *
Math.min(1, dt * 8);
this.multiplierText.text =
`${this.displayMultiplier.toFixed(2)}x`;
const intensity =
this.getVisualIntensity(this.displayMultiplier);
this.positionRocket(this.visualProgress);
this.updateRocketFX(intensity);
this.drawGraph(this.visualProgress);
this.updateGraphPoint();
}
// ============================================================
// AVIATOR STYLE FLIGHT PATH
// ============================================================
private getFlightPosition(progress: number): {
x: number;
y: number;
angle: number;
} {
const p = clamp(progress, 0, 1);
/*
* Ракета СТАРТУЕТ снизу-слева.
*
* Сначала летит почти горизонтально,
* затем всё сильнее поднимается.
*
* То есть логика движения ближе к Aviator,
* а не к статичной ракете с картинки.
*/
const startX = this.width * 0.08;
const endX = this.width * 0.84;
const startY = this.height * 0.80;
const endY = this.height * 0.17;
const x =
lerp(startX, endX, p);
// Экспоненциальный подъём.
const verticalProgress =
Math.pow(p, 2.15);
const y =
lerp(startY, endY, verticalProgress);
/*
* Вычисляем касательную,
* чтобы нос ракеты реально смотрел
* по направлению траектории.
*/
const dx =
endX - startX;
const derivative =
2.15 * Math.pow(Math.max(p, 0.001), 1.15);
const dy =
-(startY - endY) * derivative;
const angle =
Math.atan2(dy, dx);
return {
x,
y,
angle,
};
}
private positionRocket(progress: number): void {
const flight =
this.getFlightPosition(progress);
const intensity =
this.getVisualIntensity(this.displayMultiplier);
// Микровибрация двигателя.
const floatingX =
Math.sin(this.elapsed * 4.2) *
(1.2 + intensity * 1.5);
const floatingY =
Math.sin(this.elapsed * 5.7) *
(1.5 + intensity * 2);
const rotationNoise =
Math.sin(this.elapsed * 3.4) *
(0.003 + intensity * 0.005);
this.rocketX = flight.x + floatingX;
this.rocketY = flight.y + floatingY;
this.rocketRotation =
flight.angle + rotationNoise;
this.rocket.position.set(
this.rocketX,
this.rocketY,
);
this.rocket.rotation =
this.rocketRotation;
}
// ============================================================
// MULTIPLIER -> VISUAL POSITION
// ============================================================
private progressFromMultiplier(multiplier: number): number {
/*
* ВАЖНО:
*
* Это НЕ игровая математика.
*
* Она только определяет,
* где визуально находится ракета.
*
* Реальный multiplier приходит извне.
*/
const maximum =
this.options.maxVisualMultiplier;
const progress =
Math.log(Math.max(1, multiplier)) /
Math.log(maximum);
return clamp(progress, 0, 1);
}
private getVisualIntensity(multiplier: number): number {
if (multiplier <= 1) return 0.05;
const t =
Math.log(multiplier) /
Math.log(20);
return clamp(t, 0.05, 1);
}
// ============================================================
// ROCKET
// ============================================================
private createRocket(): void {
/*
* rocket local coordinate system:
*
* двигатель = LEFT
* нос = RIGHT
*
* Сам контейнер потом вращается
* по касательной графика.
*/
this.rocket.addChild(
this.rocketTrail,
this.rocketEngine,
this.rocketBody,
);
this.drawRocketBody();
this.rocket.scale.set(
this.getRocketScale(),
);
}
private drawRocketBody(): void {
this.rocketBody.removeChildren();
// ----------------------------------------------------------
// BACK FIN
// ----------------------------------------------------------
const backFin = new Graphics();
backFin
.moveTo(-58, -20)
.bezierCurveTo(
-82, -42,
-100, -52,
-108, -48,
)
.bezierCurveTo(
-102, -25,
-89, -8,
-69, 2,
)
.closePath()
.fill({
color: 0x130d15,
})
.stroke({
color: 0xb71935,
width: 2,
alpha: 0.55,
});
this.rocketBody.addChild(backFin);
// ----------------------------------------------------------
// BOTTOM FIN
// ----------------------------------------------------------
const bottomFin = new Graphics();
bottomFin
.moveTo(-20, 26)
.bezierCurveTo(
-28, 54,
-17, 67,
-4, 68,
)
.bezierCurveTo(
6, 53,
13, 37,
18, 20,
)
.closePath()
.fill({
color: 0x100d14,
})
.stroke({
color: 0xd32140,
width: 2,
alpha: 0.65,
});
this.rocketBody.addChild(bottomFin);
// ----------------------------------------------------------
// MAIN BODY SHADOW
// ----------------------------------------------------------
const shadow = new Graphics();
shadow
.moveTo(-73, 0)
.bezierCurveTo(
-58, -35,
-9, -49,
48, -40,
)
.bezierCurveTo(
76, -35,
95, -17,
105, 0,
)
.bezierCurveTo(
94, 19,
73, 38,
41, 44,
)
.bezierCurveTo(
-14, 51,
-57, 34,
-73, 0,
)
.closePath()
.fill({
color: 0x07080d,
});
this.rocketBody.addChild(shadow);
// ----------------------------------------------------------
// MAIN BODY
// ----------------------------------------------------------
const body = new Graphics();
body
.moveTo(-72, -2)
.bezierCurveTo(
-58, -39,
-4, -52,
52, -40,
)
.bezierCurveTo(
79, -34,
98, -17,
108, 0,
)
.bezierCurveTo(
94, 13,
77, 26,
46, 34,
)
.bezierCurveTo(
-6, 45,
-52, 30,
-72, -2,
)
.closePath()
.fill({
color: 0x11121a,
});
this.rocketBody.addChild(body);
// ----------------------------------------------------------
// BODY HIGHLIGHT
// ----------------------------------------------------------
const highlight = new Graphics();
highlight
.moveTo(-42, -28)
.bezierCurveTo(
1, -45,
50, -34,
82, -15,
)
.bezierCurveTo(
48, -24,
2, -23,
-42, -12,
)
.closePath()
.fill({
color: 0x45424d,
alpha: 0.24,
});
this.rocketBody.addChild(highlight);
// ----------------------------------------------------------
// RED NOSE LIGHT
// ----------------------------------------------------------
const noseGlow = new Graphics();
noseGlow
.moveTo(79, -28)
.bezierCurveTo(
94, -17,
103, -6,
108, 0,
)
.bezierCurveTo(
102, 6,
96, 10,
91, 13,
)
.bezierCurveTo(
94, -3,
89, -16,
79, -28,
)
.closePath()
.fill({
color: 0xee2945,
alpha: 0.55,
});
this.rocketBody.addChild(noseGlow);
// ----------------------------------------------------------
// TOP/BACK BODY LINE
// ----------------------------------------------------------
const seam = new Graphics();
seam
.moveTo(65, -36)
.lineTo(91, -15)
.stroke({
color: 0x06060a,
width: 3,
alpha: 0.9,
});
this.rocketBody.addChild(seam);
// ----------------------------------------------------------
// WINDOW OUTER GLOW
// ----------------------------------------------------------
const windowGlow = new Graphics();
windowGlow
.circle(35, -4, 22)
.fill({
color: 0xc81738,
alpha: 0.13,
});
this.rocketBody.addChild(windowGlow);
// ----------------------------------------------------------
// WINDOW RED RING
// ----------------------------------------------------------
const ringOuter = new Graphics();
ringOuter
.circle(35, -4, 17)
.fill({
color: 0xe02a45,
});
ringOuter
.circle(35, -4, 12)
.fill({
color: 0x331019,
});
this.rocketBody.addChild(ringOuter);
// ----------------------------------------------------------
// WINDOW
// ----------------------------------------------------------
const glass = new Graphics();
glass
.circle(35, -4, 10)
.fill({
color: 0x05060a,
});
glass
.circle(31, -8, 4)
.fill({
color: 0x4a3540,
alpha: 0.35,
});
this.rocketBody.addChild(glass);
// ----------------------------------------------------------
// LOWER RED RIM LIGHT
// ----------------------------------------------------------
const rim = new Graphics();
rim
.moveTo(-52, 26)
.bezierCurveTo(
-3, 44,
52, 28,
88, 11,
)
.stroke({
color: 0xe92744,
width: 2.5,
alpha: 0.7,
});
this.rocketBody.addChild(rim);
// ----------------------------------------------------------
// ENGINE
// ----------------------------------------------------------
const engine = new Graphics();
engine
.moveTo(-73, -14)
.lineTo(-88, -11)
.lineTo(-92, 11)
.lineTo(-73, 15)
.closePath()
.fill({
color: 0x161016,
})
.stroke({
color: 0xa51730,
width: 2,
alpha: 0.65,
});
this.rocketBody.addChild(engine);
}
private getRocketScale(): number {
const base =
this.height / 380;
return clamp(base, 0.48, 1.05);
}
// ============================================================
// ENGINE + SPEED TRAILS
// ============================================================
private updateRocketFX(intensity: number): void {
const pulse =
0.94 +
Math.sin(this.elapsed * 19) * 0.06;
const length =
70 +
intensity * 110;
this.rocketEngine.clear();
// Outer glow.
this.rocketEngine
.moveTo(-84, -13)
.lineTo(-84 - length * 1.25 * pulse, 0)
.lineTo(-84, 13)
.closePath()
.fill({
color: 0x8d0828,
alpha: 0.18 + intensity * 0.12,
});
// Red flame.
this.rocketEngine
.moveTo(-83, -8)
.lineTo(-83 - length * pulse, 0)
.lineTo(-83, 8)
.closePath()
.fill({
color: 0xff304a,
alpha: 0.68 + intensity * 0.2,
});
// Hot inner core.
this.rocketEngine
.moveTo(-82, -3.5)
.lineTo(-82 - length * 0.52 * pulse, 0)
.lineTo(-82, 3.5)
.closePath()
.fill({
color: 0xffc1a9,
alpha: 0.88,
});
this.drawSpeedTrails(intensity);
}
private drawSpeedTrails(intensity: number): void {
this.rocketTrail.clear();
const baseLength =
125 + intensity * 230;
const trails = [
{
y: -16,
width: 2,
alpha: 0.20,
length: 0.78,
color: 0x8c0927,
},
{
y: -9,
width: 3,
alpha: 0.35,
length: 0.95,
color: 0xbc1233,
},
{
y: -3,
width: 4,
alpha: 0.50,
length: 1.0,
color: 0xff304a,
},
{
y: 5,
width: 3,
alpha: 0.33,
length: 0.88,
color: 0xd4193b,
},
{
y: 13,
width: 2,
alpha: 0.18,
length: 0.72,
color: 0x710720,
},
];
for (const trail of trails) {
const wobble =
Math.sin(
this.elapsed * 7 +
trail.y,
) * 2;
this.rocketTrail
.moveTo(-75, trail.y)
.bezierCurveTo(
-130,
trail.y + wobble,
-180,
trail.y * 1.3,
-75 -
baseLength *
trail.length,
trail.y * 1.6,
)
.stroke({
width:
trail.width +
intensity * 1.2,
color:
trail.color,
alpha:
trail.alpha +
intensity * 0.08,
});
}
}
// ============================================================
// GRAPH
// ============================================================
private drawGraph(progress: number): void {
this.graphOuter.clear();
this.graphMiddle.clear();
this.graphMain.clear();
if (progress <= 0.002) return;
this.drawSingleGraph(
this.graphOuter,
progress,
18,
0xd7173b,
0.10,
);
this.drawSingleGraph(
this.graphMiddle,
progress,
9,
0xf22947,
0.23,
);
this.drawSingleGraph(
this.graphMain,
progress,
4,
0xff6670,
0.95,
);
}
private drawSingleGraph(
graphics: Graphics,
progress: number,
width: number,
color: number,
alpha: number,
): void {
const samples =
Math.max(
10,
Math.floor(70 * progress),
);
for (let i = 0; i <= samples; i++) {
const p =
progress *
(i / samples);
const point =
this.getFlightPosition(p);
if (i === 0) {
graphics.moveTo(
point.x,
point.y,
);
} else {
graphics.lineTo(
point.x,
point.y,
);
}
}
graphics.stroke({
color,
width,
alpha,
});
}
private updateGraphPoint(): void {
if (this.visualProgress <= 0.005) {
this.graphPoint.visible = false;
this.graphPointGlow.visible = false;
return;
}
this.graphPoint.visible = true;
this.graphPointGlow.visible = true;
const point =
this.getFlightPosition(
this.visualProgress,
);
const pulse =
1 +
Math.sin(this.elapsed * 6) *
0.09;
this.graphPointGlow.clear();
this.graphPointGlow
.circle(
point.x,
point.y,
22 * pulse,
)
.fill({
color: 0xff183d,
alpha: 0.12,
});
this.graphPoint.clear();
this.graphPoint
.circle(
point.x,
point.y,
6.5 * pulse,
)
.fill({
color: 0xff7a81,
});
}
// ============================================================
// BACKGROUND / GRID
// ============================================================
private drawBackground(): void {
this.background.clear();
this.background
.rect(
0,
0,
this.width,
this.height,
)
.fill({
color: 0x050812,
});
this.ambient.clear();
// Большие слабые круги создают псевдо-gradient.
this.ambient
.circle(
this.width * 0.74,
this.height * 0.46,
this.width * 0.38,
)
.fill({
color: 0x520719,
alpha: 0.08,
});
this.ambient
.circle(
this.width * 0.86,
this.height * 0.30,
this.width * 0.22,
)
.fill({
color: 0x8b0b28,
alpha: 0.055,
});
}
private drawGrid(): void {
this.grid.clear();
const startX =
this.width * 0.43;
const top =
this.height * 0.08;
const spacingX =
Math.max(
44,
this.width * 0.095,
);
const spacingY =
Math.max(
38,
this.height * 0.17,
);
for (
let x = startX;
x <= this.width;
x += spacingX
) {
const relative =
(x - startX) /
(this.width - startX);
this.grid
.moveTo(x, top)
.lineTo(x, this.height)
.stroke({
color: 0x75152a,
width: 1,
alpha:
0.025 +
relative * 0.09,
});
}
for (
let y = top;
y <= this.height;
y += spacingY
) {
this.grid
.moveTo(startX, y)
.lineTo(this.width, y)
.stroke({
color: 0x75152a,
width: 1,
alpha: 0.075,
});
}
}
// ============================================================
// DEBRIS
// ============================================================
private createDebris(): void {
const count =
window.innerWidth < 600
? 9
: 15;
for (let i = 0; i < count; i++) {
const view =
this.createDebrisGraphic();
const depth =
0.3 + Math.random() * 0.9;
view.scale.set(
0.4 +
depth * 0.7,
);
this.debrisLayer.addChild(view);
this.debris.push({
view,
depth,
vx:
4 +
Math.random() * 12,
vy:
1 +
Math.random() * 5,
rotationSpeed:
(Math.random() - 0.5) *
0.35,
});
}
}
private createDebrisGraphic(): Graphics {
const g =
new Graphics();
const radius =
6 + Math.random() * 12;
const points: {
x: number;
y: number;
}[] = [];
const vertexCount =
5 +
Math.floor(Math.random() * 4);
for (
let i = 0;
i < vertexCount;
i++
) {
const angle =
(i / vertexCount) *
Math.PI *
2;
const variation =
0.55 +
Math.random() * 0.45;
points.push({
x:
Math.cos(angle) *
radius *
variation,
y:
Math.sin(angle) *
radius *
variation,
});
}
g.moveTo(
points[0].x,
points[0].y,
);
for (
let i = 1;
i < points.length;
i++
) {
g.lineTo(
points[i].x,
points[i].y,
);
}
g.closePath().fill({
color: 0x171019,
});
// Красная подсвеченная грань.
if (points.length >= 3) {
g
.moveTo(0, 0)
.lineTo(
points[1].x,
points[1].y,
)
.lineTo(
points[2].x,
points[2].y,
)
.closePath()
.fill({
color: 0x79142b,
alpha:
0.25 +
Math.random() * 0.25,
});
}
return g;
}
private resetDebrisPosition(
item: Debris,
initial = false,
): void {
item.view.x =
initial
? Math.random() * this.width
: this.width +
Math.random() *
this.width *
0.25;
item.view.y =
this.height * 0.12 +
Math.random() *
this.height *
0.80;
}
private updateDebris(dt: number): void {
const intensity =
this.getVisualIntensity(
this.displayMultiplier,
);
for (
const item of this.debris
) {
const speed =
(5 + intensity * 38) *
item.depth;
item.view.x -=
speed * dt;
item.view.y +=
speed *
0.18 *
dt;
item.view.rotation +=
item.rotationSpeed *
dt;
if (
item.view.x < -70 ||
item.view.y >
this.height + 70
) {
this.resetDebrisPosition(
item,
);
}
}
}
// ============================================================
// PARTICLES
// ============================================================
private createParticles(): void {
const count =
window.innerWidth < 600
? 22
: 38;
for (let i = 0; i < count; i++) {
const g =
new Graphics();
const radius =
0.6 +
Math.random() * 1.8;
g.circle(0, 0, radius).fill({
color:
Math.random() > 0.5
? 0xff3049
: 0xb71632,
alpha:
0.15 +
Math.random() * 0.35,
});
const depth =
0.3 +
Math.random() * 1.2;
this.particlesLayer.addChild(g);
this.particles.push({
view: g,
depth,
vx:
10 +
Math.random() * 30,
vy:
2 +
Math.random() * 10,
});
}
}
private updateParticles(
dt: number,
): void {
const intensity =
this.getVisualIntensity(
this.displayMultiplier,
);
for (
const particle
of this.particles
) {
const speed =
particle.vx *
(0.25 + intensity * 1.5) *
particle.depth;
particle.view.x -=
speed * dt;
particle.view.y +=
particle.vy *
dt *
(0.3 + intensity);
if (
particle.view.x < -10 ||
particle.view.y >
this.height + 10
) {
particle.view.x =
this.width +
Math.random() * 80;
particle.view.y =
Math.random() *
this.height *
0.85;
}
}
}
// ============================================================
// CRASH
// ============================================================
private updateCrash(dt: number): void {
this.crashElapsed += dt;
const duration =
0.42;
const progress =
clamp(
this.crashElapsed /
duration,
0,
1,
);
const eased =
easeInCubic(progress);
/*
* При CRASH ракета резко улетает
* верх-вправо.
*
* Никакого взрыва.
*/
this.rocket.x =
this.crashStartX +
this.width *
0.58 *
eased;
this.rocket.y =
this.crashStartY -
this.height *
0.80 *
eased;
this.rocket.rotation =
this.crashStartRotation -
0.12 * eased;
this.updateRocketFX(
clamp(
0.75 +
eased * 0.4,
0,
1,
),
);
if (progress >= 1) {
this.rocket.visible = false;
}
}
// ============================================================
// IDLE
// ============================================================
private updateIdle(): void {
this.multiplierText.text =
'1.00x';
const floating =
Math.sin(
this.elapsed * 2.3,
);
const start =
this.getFlightPosition(0);
this.rocket.position.set(
start.x,
start.y +
floating * 2,
);
this.rocket.rotation =
start.angle +
floating * 0.004;
this.updateRocketFX(0.04);
}
// ============================================================
// RESIZE
// ============================================================
private resize(): void {
this.width =
this.app.screen.width;
this.height =
this.app.screen.height;
this.drawBackground();
this.drawGrid();
this.rocket.scale.set(
this.getRocketScale(),
);
this.multiplierText.style.fontSize =
clamp(
this.height * 0.20,
38,
88,
);
/*
* Коэффициент оставляем примерно
* в центре игрового поля.
*
* Если существующая игра уже
* рисует multiplier в DOM,
* при mount() передать:
*
* { showMultiplier: false }
*/
this.multiplierText.position.set(
this.width * 0.50,
this.height * 0.56,
);
for (
const item of this.debris
) {
this.resetDebrisPosition(
item,
true,
);
}
for (
const particle
of this.particles
) {
particle.view.position.set(
Math.random() *
this.width,
Math.random() *
this.height,
);
}
if (
this.state === 'flying'
) {
this.positionRocket(
this.visualProgress,
);
this.drawGraph(
this.visualProgress,
);
this.updateGraphPoint();
} else {
this.positionRocket(0);
}
}
}
У меня уже существует рабочая Crash-игра.
Не создавай игру заново и не меняй backend, ставки, Gold, START,
cashout, crash multiplier и логику раунда.
Встрой CrashAviatorVisual.ts в существующее игровое поле.
Найди в проекте:
1. текущий multiplier;
2. событие начала настоящего раунда;
3. событие настоящего CRASH;
4. reset следующего раунда.
Свяжи их:
round started
→ visual.startRound()
при каждом обновлении существующего multiplier
→ visual.setMultiplier(currentMultiplier)
existing crash event
→ visual.crash()
reset/new round
→ visual.reset()
Если существующий multiplier уже нарисован HTML/CSS,
создай visual через:
CrashAviatorVisual.mount(element, { showMultiplier: false })
Старые белые частицы/старый canvas-визуал удалить или отключить.
Не трогай нижний блок ставки и остальной интерфейс.
Если проект уже использует одну Pixi Application,
не создавай вторую Application:
адаптируй класс под существующий stage/container,
сохранив всю визуальную реализацию.
Используй PixiJS 8 API проекта и исправь несовместимости,
если установленная версия имеет немного другой API Graphics.
Главное:
не переписывай работающую Crash-логику.
Замени только renderer/visual игрового поля.