初始提交:三玩法 Web 原型与设计文档

- 玩法一 数字一笔:规则库、求解器、26 关、竖屏触摸 Web 原型、测试
- 玩法二 连连气球:连消畅玩版核心、Web 原型、Cocos 球槽组件与导出脚本
- 玩法三 六边一笔:规则库、求解器、10 关 + 转墙实验包、Web 原型、测试
- 产品文档、UI 设计规范、各玩法 GDD / HANDOFF
- 文档移除全部外部参考作引用;删除玩法二 v0 旧稿
This commit is contained in:
cabinw
2026-09-16 14:30:59 +08:00
commit 8026e4628d
169 changed files with 22162 additions and 0 deletions
+46
View File
@@ -0,0 +1,46 @@
import { adjacent, points, resultNumber } from './rules';
import { legalFlow } from './flow-rules';
import type { Ball } from './types';
export const balloonRadius = (n: number): number => 24 + Math.min(9, n - 1) * 3.2;
export const copyBalls = (balls: Ball[]): Ball[] => balls.map(b => ({ ...b }));
export interface Reaction { before: Ball[]; path: Ball[]; created: Ball; points: number; index: number }
export interface Cascade { stages: Reaction[]; balls: Ball[]; score: number; popped: number; flight: Ball | null }
/** 自动接上 n−2 → n−1 → 新球;新球一直留在玩家选择的链尾。 */
export function continuation(balls: Ball[], head: Ball): number[] | null {
const seconds = balls.filter(b => b.n === head.n - 1 && adjacent(b, head))
.sort((a, b) => Math.hypot(a.x - head.x, a.y - head.y) - Math.hypot(b.x - head.x, b.y - head.y));
for (const second of seconds) {
const first = balls.find(b => b.n === head.n - 2 && adjacent(b, second));
if (first) return [first.id, second.id, head.id];
}
const same = balls.filter(b => b.id !== head.id && b.n === head.n);
for (const second of same.filter(b => adjacent(b, head))) {
const first = same.find(b => b.id !== second.id && adjacent(b, second));
if (first) return [first.id, second.id, head.id];
}
return null;
}
export function cascade(source: Ball[], ids: number[], fever = false, offset = 0): Cascade | null {
let balls = copyBalls(source), next: number[] | null = ids;
const initial = ids.map(id => balls.find(b => b.id === id));
if (initial.some(b => !b) || !legalFlow(initial as Ball[])) return null;
const stages: Reaction[] = [];
let score = 0, popped = 0, flight: Ball | null = null;
while (next && stages.length + offset < (fever ? 4 : 3)) {
const path = next.map(id => balls.find(b => b.id === id)!);
const tail = path[path.length - 1], n = resultNumber(path), index = stages.length + offset + 1;
const created = { ...tail, n, r: balloonRadius(n), vx: 0, vy: 0 };
const gain = points(path) * index * (fever ? 2 : 1) + (n >= 10 ? 10000 : 0);
stages.push({ before: copyBalls(balls), path: copyBalls(path), created: { ...created }, points: gain, index });
score += gain; popped += path.length;
const removed = new Set(next);
balls = balls.filter(b => !removed.has(b.id));
if (n >= 10) { flight = created; break; }
balls.push(created);
next = continuation(balls, created);
}
return { stages, balls, score, popped, flight };
}