初始提交:三玩法 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
+35
View File
@@ -0,0 +1,35 @@
import type { Ball, Point } from './types';
import { canAppend } from './rules';
/** 同一控制器接 PointerEvent 或 Cocos EventTouch;取消不等于松手。 */
export class Gesture {
chain: number[] = [];
active = false;
private previous: Point | null = null;
constructor(private readonly balls: () => Ball[], private readonly append = canAppend) {}
down(p: Point): void {
this.cancel(); this.previous = p;
const b = this.hit(p);
if (b) { this.active = true; this.chain = [b.id]; }
}
move(p: Point): void {
if (!this.active) return;
const from = this.previous || p;
// 分段采样避免快速扫过小球时跳号;一次事件最多 120 次。
const steps = Math.min(120, Math.max(1, Math.ceil(Math.hypot(p.x - from.x, p.y - from.y) / 6)));
for (let i = 1; i <= steps; i++) {
const b = this.hit({ x: from.x + (p.x - from.x) * i / steps, y: from.y + (p.y - from.y) * i / steps });
if (!b || b.id === this.chain[this.chain.length - 1]) continue;
if (this.chain.length > 1 && b.id === this.chain[this.chain.length - 2]) { this.chain.pop(); continue; }
const balls = this.balls();
if (this.append(this.chain.map(id => balls.find(b => b.id === id)!), b)) this.chain.push(b.id);
}
this.previous = p;
}
up(): number[] { const path = [...this.chain]; this.cancel(); return path; }
cancel(): void { this.chain = []; this.active = false; this.previous = null; }
private hit(p: Point): Ball | undefined {
return this.balls().filter(b => Math.hypot(b.x - p.x, b.y - p.y) <= b.r + 4)
.sort((a, b) => Math.hypot(a.x - p.x, a.y - p.y) / a.r - Math.hypot(b.x - p.x, b.y - p.y) / b.r)[0];
}
}