线性算法验证失败,蠕变导致时飘一直上升
This commit is contained in:
@@ -16,12 +16,17 @@
|
||||
interface Brick {
|
||||
mesh: THREE.Mesh<THREE.BoxGeometry, THREE.MeshStandardMaterial>;
|
||||
alive: boolean;
|
||||
kind: "normal" | "bomb";
|
||||
left: number;
|
||||
right: number;
|
||||
top: number;
|
||||
bottom: number;
|
||||
}
|
||||
|
||||
type GameState = "ready" | "running" | "paused" | "draft" | "over";
|
||||
|
||||
type UpgradeId = "overdrive" | "demolition" | "surge" | "stabilizer";
|
||||
|
||||
interface Copy {
|
||||
title: string;
|
||||
start: string;
|
||||
@@ -38,6 +43,21 @@
|
||||
bricks: string;
|
||||
chase: string;
|
||||
pausedOverlay: string;
|
||||
ready: string;
|
||||
draft: string;
|
||||
startOverlayTitle: string;
|
||||
startOverlayBody: string;
|
||||
startOverlayHint: string;
|
||||
overHint: string;
|
||||
draftOverlayTitle: string;
|
||||
draftOverlayHint: string;
|
||||
skills: string;
|
||||
}
|
||||
|
||||
interface UpgradeCardCopy {
|
||||
id: UpgradeId;
|
||||
title: string;
|
||||
body: string;
|
||||
}
|
||||
|
||||
export let locale: LocaleCode = "zh-CN";
|
||||
@@ -70,8 +90,12 @@
|
||||
const UPPER_STALL_Y = 14;
|
||||
const ANTI_STALL_COOLDOWN_MS = 420;
|
||||
const MAX_BRICK_HITS_WITHOUT_PADDLE = 5;
|
||||
const DRAFT_EVERY_BRICKS = 14;
|
||||
const BASE_BOMB_CHANCE = 0.08;
|
||||
const BASE_BOMB_RADIUS = 8.8;
|
||||
const RUN_TARGET_LEVEL = 4;
|
||||
|
||||
const copyByLocale: Record<LocaleCode, Copy> = {
|
||||
const legacyCopyByLocale = {
|
||||
"zh-CN": {
|
||||
title: "NEON BREAKOUT",
|
||||
start: "开始",
|
||||
@@ -108,6 +132,109 @@
|
||||
}
|
||||
};
|
||||
|
||||
const copyByLocale: Record<LocaleCode, Copy> = {
|
||||
"zh-CN": {
|
||||
title: "NEON BREAKOUT",
|
||||
start: "开始",
|
||||
restart: "重开",
|
||||
pause: "暂停",
|
||||
resume: "继续",
|
||||
running: "进行中",
|
||||
paused: "已暂停",
|
||||
over: "本局结束",
|
||||
score: "得分",
|
||||
combo: "连击",
|
||||
lives: "生命",
|
||||
level: "阶段",
|
||||
bricks: "剩余",
|
||||
chase: "模式",
|
||||
pausedOverlay: "已暂停 / 按上继续",
|
||||
ready: "待开始",
|
||||
draft: "选技能",
|
||||
startOverlayTitle: "按上开始",
|
||||
startOverlayBody: "左右移动挡板,别让球掉出场地。打掉砖块会触发更快的肉鸽强化,炸弹砖能连锁清场。",
|
||||
startOverlayHint: "顶部双角施压或键盘 ArrowUp 开局;对局中同样用上方控制暂停 / 继续。",
|
||||
overHint: "按上重新开一局,或者直接点重开。",
|
||||
draftOverlayTitle: "选择强化",
|
||||
draftOverlayHint: "这一局会越打越快。点一个技能,或者按数字 1 / 2 / 3 选择。",
|
||||
skills: "技能"
|
||||
},
|
||||
"en-US": {
|
||||
title: "NEON BREAKOUT",
|
||||
start: "Start",
|
||||
restart: "Restart",
|
||||
pause: "Pause",
|
||||
resume: "Resume",
|
||||
running: "Running",
|
||||
paused: "Paused",
|
||||
over: "Game Over",
|
||||
score: "Score",
|
||||
combo: "Combo",
|
||||
lives: "Lives",
|
||||
level: "Level",
|
||||
bricks: "Bricks",
|
||||
chase: "Mode",
|
||||
pausedOverlay: "Paused / Press up to resume",
|
||||
ready: "Ready",
|
||||
draft: "Draft",
|
||||
startOverlayTitle: "Press Up To Start",
|
||||
startOverlayBody: "Move the paddle left and right, keep the ball alive, and chain through bomb bricks to clear each run fast.",
|
||||
startOverlayHint: "Use top pressure or ArrowUp to start. The same up control pauses and resumes mid-run.",
|
||||
overHint: "Press up to jump right back in, or click restart.",
|
||||
draftOverlayTitle: "Choose An Upgrade",
|
||||
draftOverlayHint: "Each run speeds up quickly. Pick one perk, or press 1 / 2 / 3.",
|
||||
skills: "Perks"
|
||||
}
|
||||
};
|
||||
|
||||
const upgradeCopyByLocale: Record<LocaleCode, Record<UpgradeId, UpgradeCardCopy>> = {
|
||||
"zh-CN": {
|
||||
overdrive: {
|
||||
id: "overdrive",
|
||||
title: "超频发球",
|
||||
body: "发球更快,球速上限更高,整局节奏立刻提起来。"
|
||||
},
|
||||
demolition: {
|
||||
id: "demolition",
|
||||
title: "爆破砖块",
|
||||
body: "炸弹砖出现更多,爆炸半径更大,更容易一口气清一片。"
|
||||
},
|
||||
surge: {
|
||||
id: "surge",
|
||||
title: "速度滚雪球",
|
||||
body: "每打掉一块砖,球速涨得更多,越打越疯。"
|
||||
},
|
||||
stabilizer: {
|
||||
id: "stabilizer",
|
||||
title: "宽幅挡板",
|
||||
body: "挡板更宽并回复 1 点生命,容错更稳,不容易断节奏。"
|
||||
}
|
||||
},
|
||||
"en-US": {
|
||||
overdrive: {
|
||||
id: "overdrive",
|
||||
title: "Overdrive",
|
||||
body: "Faster launches and a higher speed cap so the whole run ramps up immediately."
|
||||
},
|
||||
demolition: {
|
||||
id: "demolition",
|
||||
title: "Demolition",
|
||||
body: "More bomb bricks and a larger blast radius for faster board clears."
|
||||
},
|
||||
surge: {
|
||||
id: "surge",
|
||||
title: "Surge",
|
||||
body: "Each brick hit adds more speed, turning every rally into a snowball."
|
||||
},
|
||||
stabilizer: {
|
||||
id: "stabilizer",
|
||||
title: "Stabilizer",
|
||||
body: "A wider paddle plus one life to keep the run alive while the pace rises."
|
||||
}
|
||||
}
|
||||
};
|
||||
void legacyCopyByLocale;
|
||||
|
||||
let hostEl: HTMLElement | undefined;
|
||||
let canvasEl: HTMLCanvasElement | undefined;
|
||||
let rafId: number | null = null;
|
||||
@@ -135,7 +262,7 @@
|
||||
const ballVel = new THREE.Vector2(0, 0);
|
||||
let bricks: Brick[] = [];
|
||||
|
||||
let gameState: "idle" | "running" | "paused" | "over" = "idle";
|
||||
let gameState: GameState = "ready";
|
||||
let score = 0;
|
||||
let combo = 0;
|
||||
let lives = 3;
|
||||
@@ -148,6 +275,15 @@
|
||||
let lastPaddleContactAt = 0;
|
||||
let bricksSincePaddle = 0;
|
||||
let lastAntiStallAt = 0;
|
||||
let paddleWidth = PADDLE_W;
|
||||
let launchSpeedBonus = 0;
|
||||
let maxSpeedBonus = 0;
|
||||
let brickSpeedGain = 0.014;
|
||||
let bombBrickChance = BASE_BOMB_CHANCE;
|
||||
let bombExplosionRadius = BASE_BOMB_RADIUS;
|
||||
let bricksUntilDraft = DRAFT_EVERY_BRICKS;
|
||||
let activeDraftOptions: UpgradeCardCopy[] = [];
|
||||
let selectedUpgrades: UpgradeId[] = [];
|
||||
|
||||
let cornerForce: CornerMap = { tl: 0, tr: 0, bl: 0, br: 0 };
|
||||
$: cornerForce = readCorners(pressureMatrix, matrixRows, matrixCols);
|
||||
@@ -157,8 +293,17 @@
|
||||
$: rightForce = cornerForce.tr + cornerForce.br;
|
||||
$: topForce = cornerForce.tl + cornerForce.tr;
|
||||
$: pauseGestureThreshold = Math.max(420, Math.round(Math.max(1000, rangeMax - rangeMin) * 0.07));
|
||||
$: upgradeCopy = upgradeCopyByLocale[locale] ?? upgradeCopyByLocale["en-US"];
|
||||
$: statusText =
|
||||
gameState === "running" ? ui.running : gameState === "paused" ? ui.paused : gameState === "over" ? ui.over : ui.start;
|
||||
gameState === "running"
|
||||
? ui.running
|
||||
: gameState === "paused"
|
||||
? ui.paused
|
||||
: gameState === "draft"
|
||||
? ui.draft
|
||||
: gameState === "over"
|
||||
? ui.over
|
||||
: ui.ready;
|
||||
|
||||
$: if (scene && renderer) {
|
||||
applyTheme();
|
||||
@@ -219,10 +364,10 @@
|
||||
const colors = themedBrickPalette();
|
||||
for (let index = 0; index < bricks.length; index += 1) {
|
||||
const brick = bricks[index];
|
||||
const color = colors[index % colors.length] ?? "#7ad0ff";
|
||||
const color = brick.kind === "bomb" ? "#ff9d5c" : colors[index % colors.length] ?? "#7ad0ff";
|
||||
brick.mesh.material.color.set(color);
|
||||
brick.mesh.material.emissive.set(color);
|
||||
brick.mesh.material.emissiveIntensity = 0.7;
|
||||
brick.mesh.material.emissiveIntensity = brick.kind === "bomb" ? 0.96 : 0.7;
|
||||
brick.mesh.material.needsUpdate = true;
|
||||
}
|
||||
}
|
||||
@@ -339,7 +484,16 @@
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.code === "ArrowLeft" || event.code === "KeyA") keyLeft = true;
|
||||
if (event.code === "ArrowRight" || event.code === "KeyD") keyRight = true;
|
||||
if (event.code === "Space" && gameState !== "running") restartGame();
|
||||
if (event.code === "ArrowUp" || event.code === "KeyW") handleUpControl();
|
||||
if (event.code === "Space" && gameState !== "running" && gameState !== "draft") restartGame();
|
||||
if (gameState === "draft") {
|
||||
const option1 = activeDraftOptions[0];
|
||||
const option2 = activeDraftOptions[1];
|
||||
const option3 = activeDraftOptions[2];
|
||||
if (event.code === "Digit1" && option1) applyUpgrade(option1.id);
|
||||
if (event.code === "Digit2" && option2) applyUpgrade(option2.id);
|
||||
if (event.code === "Digit3" && option3) applyUpgrade(option3.id);
|
||||
}
|
||||
};
|
||||
const onKeyUp = (event: KeyboardEvent) => {
|
||||
if (event.code === "ArrowLeft" || event.code === "KeyA") keyLeft = false;
|
||||
@@ -369,7 +523,7 @@
|
||||
observer.observe(hostEl);
|
||||
resize();
|
||||
|
||||
restartGame();
|
||||
enterReadyState();
|
||||
lastFrameTs = performance.now();
|
||||
const loop = (ts: number) => {
|
||||
const dt = Math.min((ts - lastFrameTs) / 1000, 0.034);
|
||||
@@ -480,11 +634,12 @@
|
||||
const startX = -totalWidth / 2 + BRICK_W / 2;
|
||||
for (let row = 0; row < BRICK_ROWS; row += 1) {
|
||||
for (let col = 0; col < BRICK_COLS; col += 1) {
|
||||
const color = palette[(row + col + level) % palette.length] ?? "#7ad0ff";
|
||||
const kind: Brick["kind"] = Math.random() < bombBrickChance ? "bomb" : "normal";
|
||||
const color = kind === "bomb" ? "#ff9d5c" : palette[(row + col + level) % palette.length] ?? "#7ad0ff";
|
||||
const material = new THREE.MeshStandardMaterial({
|
||||
color,
|
||||
emissive: color,
|
||||
emissiveIntensity: 0.7,
|
||||
emissiveIntensity: kind === "bomb" ? 0.96 : 0.7,
|
||||
roughness: 0.35,
|
||||
metalness: 0.15
|
||||
});
|
||||
@@ -496,6 +651,7 @@
|
||||
bricks.push({
|
||||
mesh,
|
||||
alive: true,
|
||||
kind,
|
||||
left: x - BRICK_W / 2,
|
||||
right: x + BRICK_W / 2,
|
||||
top: y + BRICK_H / 2,
|
||||
@@ -507,7 +663,7 @@
|
||||
}
|
||||
|
||||
function launchBall(nowMs: number): void {
|
||||
const speed = Math.min(MAX_BALL_SPEED, BASE_BALL_SPEED + (level - 1) * 4.2);
|
||||
const speed = Math.min(MAX_BALL_SPEED + maxSpeedBonus, BASE_BALL_SPEED + launchSpeedBonus + (level - 1) * 4.8);
|
||||
const directionX = rand(-0.72, 0.72);
|
||||
ballVel.set(directionX, 1).normalize().multiplyScalar(speed);
|
||||
ballLaunched = true;
|
||||
@@ -525,13 +681,32 @@
|
||||
bricksSincePaddle = 0;
|
||||
}
|
||||
|
||||
function restartGame(): void {
|
||||
if (!paddle || !ball) return;
|
||||
function syncPaddleVisual(): void {
|
||||
if (!paddle) return;
|
||||
paddle.scale.x = paddleWidth / PADDLE_W;
|
||||
}
|
||||
|
||||
function resetRunState(): void {
|
||||
score = 0;
|
||||
combo = 0;
|
||||
lives = 3;
|
||||
level = 1;
|
||||
gameState = "running";
|
||||
launchSpeedBonus = 0;
|
||||
maxSpeedBonus = 0;
|
||||
brickSpeedGain = 0.014;
|
||||
bombBrickChance = BASE_BOMB_CHANCE;
|
||||
bombExplosionRadius = BASE_BOMB_RADIUS;
|
||||
bricksUntilDraft = DRAFT_EVERY_BRICKS;
|
||||
activeDraftOptions = [];
|
||||
selectedUpgrades = [];
|
||||
paddleWidth = PADDLE_W;
|
||||
syncPaddleVisual();
|
||||
}
|
||||
|
||||
function enterReadyState(): void {
|
||||
if (!paddle || !ball) return;
|
||||
resetRunState();
|
||||
gameState = "ready";
|
||||
prevPauseGesture = false;
|
||||
pauseGestureLockUntil = 0;
|
||||
lastPaddleContactAt = performance.now();
|
||||
@@ -541,6 +716,17 @@
|
||||
paddle.position.x = 0;
|
||||
buildBricks();
|
||||
prepareBall(performance.now(), START_DELAY_MS);
|
||||
ball.position.set(ballPos.x, ballPos.y, 1.2);
|
||||
}
|
||||
|
||||
function startRun(): void {
|
||||
if (gameState !== "ready" || !paddle || !ball) return;
|
||||
gameState = "running";
|
||||
prepareBall(performance.now(), 260);
|
||||
}
|
||||
|
||||
function restartGame(): void {
|
||||
enterReadyState();
|
||||
}
|
||||
|
||||
function togglePause(): void {
|
||||
@@ -588,15 +774,71 @@
|
||||
return Math.min(1, Math.max(-1, raw));
|
||||
}
|
||||
|
||||
function handleUpControl(): void {
|
||||
if (gameState === "ready") {
|
||||
startRun();
|
||||
return;
|
||||
}
|
||||
if (gameState === "running" || gameState === "paused") {
|
||||
togglePause();
|
||||
return;
|
||||
}
|
||||
if (gameState === "over") {
|
||||
restartGame();
|
||||
}
|
||||
}
|
||||
|
||||
function sampleUpgradeOptions(): UpgradeCardCopy[] {
|
||||
const pool = Object.values(upgradeCopy);
|
||||
const shuffled = [...pool].sort(() => Math.random() - 0.5);
|
||||
return shuffled.slice(0, 3);
|
||||
}
|
||||
|
||||
function applyUpgrade(id: UpgradeId): void {
|
||||
if (gameState !== "draft") return;
|
||||
|
||||
if (id === "overdrive") {
|
||||
launchSpeedBonus += 5;
|
||||
maxSpeedBonus += 8;
|
||||
if (ballLaunched) {
|
||||
ballVel.multiplyScalar(1.08);
|
||||
}
|
||||
} else if (id === "demolition") {
|
||||
bombBrickChance = Math.min(0.32, bombBrickChance + 0.07);
|
||||
bombExplosionRadius += 2.8;
|
||||
} else if (id === "surge") {
|
||||
brickSpeedGain += 0.01;
|
||||
if (ballLaunched) {
|
||||
ballVel.multiplyScalar(1.05);
|
||||
}
|
||||
} else if (id === "stabilizer") {
|
||||
lives += 1;
|
||||
paddleWidth = Math.min(PADDLE_W + 8, paddleWidth + 2.8);
|
||||
syncPaddleVisual();
|
||||
}
|
||||
|
||||
selectedUpgrades = [...selectedUpgrades, id];
|
||||
activeDraftOptions = [];
|
||||
bricksUntilDraft = DRAFT_EVERY_BRICKS;
|
||||
gameState = "running";
|
||||
lastFrameTs = performance.now();
|
||||
}
|
||||
|
||||
function openUpgradeDraft(): void {
|
||||
if (gameState !== "running") return;
|
||||
activeDraftOptions = sampleUpgradeOptions();
|
||||
gameState = "draft";
|
||||
}
|
||||
|
||||
function handleSensorPauseGesture(nowMs: number): void {
|
||||
if (gameState !== "running" && gameState !== "paused") {
|
||||
if (gameState !== "ready" && gameState !== "running" && gameState !== "paused" && gameState !== "over") {
|
||||
prevPauseGesture = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const active = topForce >= pauseGestureThreshold;
|
||||
if (active && !prevPauseGesture && nowMs >= pauseGestureLockUntil) {
|
||||
togglePause();
|
||||
handleUpControl();
|
||||
pauseGestureLockUntil = nowMs + PAUSE_COOLDOWN_MS;
|
||||
}
|
||||
prevPauseGesture = active;
|
||||
@@ -609,7 +851,7 @@
|
||||
const keyAxis = (keyRight ? 1 : 0) - (keyLeft ? 1 : 0);
|
||||
const axis = keyAxis !== 0 ? keyAxis : sensorControlAxis();
|
||||
paddleX += axis * PADDLE_SPEED * dt;
|
||||
paddleX = Math.min(FIELD_HALF_W - PADDLE_W * 0.5, Math.max(-FIELD_HALF_W + PADDLE_W * 0.5, paddleX));
|
||||
paddleX = Math.min(FIELD_HALF_W - paddleWidth * 0.5, Math.max(-FIELD_HALF_W + paddleWidth * 0.5, paddleX));
|
||||
paddle.position.x = paddleX;
|
||||
|
||||
if (gameState !== "running") {
|
||||
@@ -666,8 +908,8 @@
|
||||
if (!ballLaunched || ballVel.y >= 0) return;
|
||||
const paddleTop = PADDLE_Y + PADDLE_H * 0.5;
|
||||
const paddleBottom = PADDLE_Y - PADDLE_H * 0.5;
|
||||
const left = paddleX - PADDLE_W * 0.5;
|
||||
const right = paddleX + PADDLE_W * 0.5;
|
||||
const left = paddleX - paddleWidth * 0.5;
|
||||
const right = paddleX + paddleWidth * 0.5;
|
||||
const hit =
|
||||
ballPos.y - BALL_RADIUS <= paddleTop &&
|
||||
ballPos.y + BALL_RADIUS >= paddleBottom &&
|
||||
@@ -676,14 +918,66 @@
|
||||
|
||||
if (!hit) return;
|
||||
ballPos.y = paddleTop + BALL_RADIUS + 0.04;
|
||||
const offset = (ballPos.x - paddleX) / (PADDLE_W * 0.5);
|
||||
const speed = Math.min(MAX_BALL_SPEED, Math.max(BASE_BALL_SPEED, ballVel.length()));
|
||||
const offset = (ballPos.x - paddleX) / (paddleWidth * 0.5);
|
||||
const speed = Math.min(MAX_BALL_SPEED + maxSpeedBonus, Math.max(BASE_BALL_SPEED + launchSpeedBonus * 0.4, ballVel.length()));
|
||||
ballVel.set(offset * speed * 0.9, Math.abs(ballVel.y) + 4).normalize().multiplyScalar(speed);
|
||||
combo = 0;
|
||||
lastPaddleContactAt = performance.now();
|
||||
bricksSincePaddle = 0;
|
||||
}
|
||||
|
||||
function destroyBrick(brick: Brick, nowMs: number, scaleBoost = 0): number {
|
||||
if (!brick.alive) return 0;
|
||||
brick.alive = false;
|
||||
brick.mesh.visible = false;
|
||||
bricksLeft -= 1;
|
||||
combo += 1;
|
||||
score += 40 + combo * 5;
|
||||
bricksSincePaddle += 1;
|
||||
bricksUntilDraft -= 1;
|
||||
triggerFlash(brick.mesh.position.x, brick.mesh.position.y, 10 + combo * 0.4 + scaleBoost);
|
||||
|
||||
const nextSpeed = Math.min(MAX_BALL_SPEED + maxSpeedBonus, ballVel.length() * (1 + brickSpeedGain));
|
||||
if (ballLaunched && ballVel.lengthSq() > 0) {
|
||||
ballVel.normalize().multiplyScalar(nextSpeed);
|
||||
}
|
||||
|
||||
if (bricksSincePaddle >= MAX_BRICK_HITS_WITHOUT_PADDLE && ballPos.y > UPPER_STALL_Y) {
|
||||
forceBallDownward(1);
|
||||
lastAntiStallAt = nowMs;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
function detonateBomb(x: number, y: number, nowMs: number): number {
|
||||
let destroyed = 0;
|
||||
for (const candidate of bricks) {
|
||||
if (!candidate.alive) continue;
|
||||
const dx = candidate.mesh.position.x - x;
|
||||
const dy = candidate.mesh.position.y - y;
|
||||
if (Math.hypot(dx, dy) > bombExplosionRadius) continue;
|
||||
destroyed += destroyBrick(candidate, nowMs, 2.8);
|
||||
}
|
||||
triggerFlash(x, y, 18);
|
||||
return destroyed;
|
||||
}
|
||||
|
||||
function handleLevelClear(nowMs: number): void {
|
||||
if (level >= RUN_TARGET_LEVEL) {
|
||||
gameState = "over";
|
||||
ballLaunched = false;
|
||||
return;
|
||||
}
|
||||
|
||||
level += 1;
|
||||
buildBricks();
|
||||
prepareBall(nowMs, 420);
|
||||
if (level > 1) {
|
||||
openUpgradeDraft();
|
||||
}
|
||||
}
|
||||
|
||||
function resolveBrickCollision(nowMs: number): void {
|
||||
if (!ballLaunched) return;
|
||||
for (const brick of bricks) {
|
||||
@@ -702,25 +996,16 @@
|
||||
ballVel.y *= -1;
|
||||
}
|
||||
|
||||
brick.alive = false;
|
||||
brick.mesh.visible = false;
|
||||
bricksLeft -= 1;
|
||||
combo += 1;
|
||||
score += 40 + combo * 5;
|
||||
bricksSincePaddle += 1;
|
||||
|
||||
const nextSpeed = Math.min(MAX_BALL_SPEED, ballVel.length() * 1.014);
|
||||
ballVel.normalize().multiplyScalar(nextSpeed);
|
||||
if (bricksSincePaddle >= MAX_BRICK_HITS_WITHOUT_PADDLE && ballPos.y > UPPER_STALL_Y) {
|
||||
forceBallDownward(1);
|
||||
lastAntiStallAt = nowMs;
|
||||
if (brick.kind === "bomb") {
|
||||
detonateBomb(brick.mesh.position.x, brick.mesh.position.y, nowMs);
|
||||
} else {
|
||||
destroyBrick(brick, nowMs);
|
||||
}
|
||||
triggerFlash(brick.mesh.position.x, brick.mesh.position.y, 10 + combo * 0.4);
|
||||
|
||||
if (bricksLeft <= 0) {
|
||||
level += 1;
|
||||
buildBricks();
|
||||
prepareBall(nowMs, 560);
|
||||
handleLevelClear(nowMs);
|
||||
} else if (bricksUntilDraft <= 0) {
|
||||
openUpgradeDraft();
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -747,7 +1032,10 @@
|
||||
}
|
||||
|
||||
function forceBallDownward(forceScale: number): void {
|
||||
const speed = Math.min(MAX_BALL_SPEED, Math.max(BASE_BALL_SPEED, ballVel.length()));
|
||||
const speed = Math.min(
|
||||
MAX_BALL_SPEED + maxSpeedBonus,
|
||||
Math.max(BASE_BALL_SPEED + launchSpeedBonus * 0.4, ballVel.length())
|
||||
);
|
||||
let dirX = ballVel.x + rand(-8, 8);
|
||||
if (Math.abs(dirX) < 6) dirX = (Math.random() < 0.5 ? -1 : 1) * 6;
|
||||
const dirY = -Math.abs(ballVel.y) - 6 * forceScale;
|
||||
@@ -773,8 +1061,14 @@
|
||||
<p class="title">{ui.title}</p>
|
||||
</div>
|
||||
<div class="action-group">
|
||||
<button type="button" on:click={restartGame}>{gameState === "idle" ? ui.start : ui.restart}</button>
|
||||
<button type="button" on:click={togglePause} disabled={gameState === "idle" || gameState === "over"}>
|
||||
<button type="button" on:click={gameState === "ready" ? startRun : restartGame}>
|
||||
{gameState === "ready" ? ui.start : ui.restart}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
on:click={handleUpControl}
|
||||
disabled={gameState === "draft" || gameState === "over"}
|
||||
>
|
||||
{gameState === "paused" ? ui.resume : ui.pause}
|
||||
</button>
|
||||
</div>
|
||||
@@ -790,9 +1084,24 @@
|
||||
|
||||
<footer class="overlay-foot">
|
||||
<p class="status">{ui.chase} / {statusText}</p>
|
||||
{#if selectedUpgrades.length > 0}
|
||||
<p class="status skill-status">{ui.skills} / {selectedUpgrades.length}</p>
|
||||
{/if}
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
{#if gameState === "ready"}
|
||||
<div class="pause-mask is-ready" aria-live="polite">
|
||||
<div class="pause-panel is-ready">
|
||||
<p>{ui.startOverlayTitle}</p>
|
||||
<div class="overlay-copy">
|
||||
<span>{ui.startOverlayBody}</span>
|
||||
<span>{ui.startOverlayHint}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if gameState === "paused"}
|
||||
<div class="pause-mask" aria-live="polite">
|
||||
<div class="pause-panel">
|
||||
@@ -801,10 +1110,32 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if gameState === "draft"}
|
||||
<div class="pause-mask is-draft" aria-live="polite">
|
||||
<div class="pause-panel is-draft">
|
||||
<p>{ui.draftOverlayTitle}</p>
|
||||
<div class="overlay-copy">
|
||||
<span>{ui.draftOverlayHint}</span>
|
||||
</div>
|
||||
<div class="draft-grid">
|
||||
{#each activeDraftOptions as option, index}
|
||||
<button type="button" class="draft-card" on:click={() => applyUpgrade(option.id)}>
|
||||
<strong>{index + 1}. {option.title}</strong>
|
||||
<span>{option.body}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if gameState === "over"}
|
||||
<div class="pause-mask is-over" aria-live="assertive">
|
||||
<div class="pause-panel is-over">
|
||||
<p>{ui.over}</p>
|
||||
<div class="overlay-copy">
|
||||
<span>{ui.overHint}</span>
|
||||
</div>
|
||||
<button type="button" class="overlay-restart-btn" on:click={restartGame}>{ui.restart}</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -928,6 +1259,11 @@
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.skill-status {
|
||||
border-color: rgb(var(--hud-orange-rgb) / 0.34);
|
||||
background: rgb(var(--hud-surface-rgb) / 0.58);
|
||||
}
|
||||
|
||||
.pause-mask {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
@@ -967,6 +1303,89 @@
|
||||
text-shadow: 0 0 10px rgb(var(--hud-lime-rgb) / 0.28);
|
||||
}
|
||||
|
||||
.overlay-copy {
|
||||
display: grid;
|
||||
gap: 0.46rem;
|
||||
margin-top: 0.7rem;
|
||||
}
|
||||
|
||||
.overlay-copy span {
|
||||
color: rgb(var(--hud-text-main-rgb) / 0.84);
|
||||
font-size: clamp(0.68rem, 1.2vw, 0.86rem);
|
||||
line-height: 1.55;
|
||||
letter-spacing: 0.03em;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.pause-mask.is-ready {
|
||||
background:
|
||||
radial-gradient(circle at center, rgb(var(--hud-surface-rgb) / 0.18), rgb(var(--hud-bg-30) / 0.84)),
|
||||
linear-gradient(135deg, rgb(var(--hud-cyan-rgb) / 0.08), transparent 42%, rgb(var(--hud-lime-rgb) / 0.08));
|
||||
}
|
||||
|
||||
.pause-panel.is-ready {
|
||||
border-color: rgb(var(--hud-cyan-rgb) / 0.64);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgb(var(--hud-border-strong-rgb) / 0.1),
|
||||
0 0 32px rgb(var(--hud-cyan-rgb) / 0.22);
|
||||
max-inline-size: min(34rem, 82%);
|
||||
}
|
||||
|
||||
.pause-mask.is-draft {
|
||||
background:
|
||||
radial-gradient(circle at center, rgb(var(--hud-surface-rgb) / 0.18), rgb(var(--hud-bg-30) / 0.86)),
|
||||
linear-gradient(160deg, rgb(var(--hud-orange-rgb) / 0.08), transparent 48%, rgb(var(--hud-cyan-rgb) / 0.08));
|
||||
}
|
||||
|
||||
.pause-panel.is-draft {
|
||||
border-color: rgb(var(--hud-orange-rgb) / 0.58);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgb(var(--hud-border-strong-rgb) / 0.1),
|
||||
0 0 32px rgb(var(--hud-orange-rgb) / 0.22);
|
||||
inline-size: min(42rem, 88%);
|
||||
}
|
||||
|
||||
.draft-grid {
|
||||
margin-top: 0.92rem;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 0.62rem;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.draft-card {
|
||||
border: 1px solid rgb(var(--hud-border-rgb) / 0.38);
|
||||
border-radius: 0.78rem;
|
||||
padding: 0.8rem 0.86rem;
|
||||
background:
|
||||
linear-gradient(180deg, rgb(var(--hud-surface-alt-rgb) / 0.96), rgb(var(--hud-surface-rgb) / 0.88));
|
||||
color: rgb(var(--hud-text-main-rgb) / 0.96);
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
display: grid;
|
||||
gap: 0.48rem;
|
||||
cursor: pointer;
|
||||
transition: transform 120ms ease, border-color 120ms ease, box-shadow 120ms ease;
|
||||
}
|
||||
|
||||
.draft-card:hover {
|
||||
transform: translateY(-2px);
|
||||
border-color: rgb(var(--hud-cyan-rgb) / 0.56);
|
||||
box-shadow: 0 0 18px rgb(var(--hud-cyan-rgb) / 0.16);
|
||||
}
|
||||
|
||||
.draft-card strong {
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.2;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.draft-card span {
|
||||
color: rgb(var(--hud-text-main-rgb) / 0.82);
|
||||
font-size: 0.66rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.pause-mask.is-over {
|
||||
background:
|
||||
radial-gradient(circle at center, rgb(var(--hud-surface-rgb) / 0.22), rgb(var(--hud-bg-30) / 0.8)),
|
||||
@@ -1023,5 +1442,9 @@
|
||||
.hud-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.draft-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user