/** Creator 3.8 适配组件;通过 npm run cocos:export 导出后放入 assets/modes/02-connect-1234。 */ import { _decorator, Component, EventTouch, Graphics, Label, Node, UITransform, Vec3, Color, HorizontalTextAlignment, VerticalTextAlignment } from 'cc'; import { FlowGame, type FlowResult } from '../src/flow'; import { canFlowAppend } from '../src/flow-rules'; import { Gesture } from '../src/input'; import { WIDTH, HEIGHT } from '../src/config'; import { drawBoard, Draw } from '../src/view'; import { FlowAnimation } from '../src/flow-animation'; import type { Point } from '../src/types'; const { ccclass, property, requireComponent } = _decorator; @ccclass('Connect1234Board') @requireComponent(UITransform) export class Connect1234Board extends Component { @property({ tooltip: "开局种子;相同种子和操作得到相同盘面" }) seed = 1234; private game!: FlowGame; private gesture!: Gesture; private graphics!: Graphics; private surface!: Node; private labels: Label[] = []; private touchId: number | null = null; private time = 0; private hints: number[] = []; private animation: FlowAnimation | null = null; private seenReaction = 0; onLoad(): void { this.surface = new Node('BalloonDrawing'); this.surface.layer = this.node.layer; this.node.addChild(this.surface); this.surface.addComponent(UITransform).setContentSize(WIDTH, HEIGHT); this.graphics = this.surface.addComponent(Graphics); this.startNew(this.seed); } onEnable(): void { this.node.on(Node.EventType.TOUCH_START, this.onStart, this); this.node.on(Node.EventType.TOUCH_MOVE, this.onMove, this); this.node.on(Node.EventType.TOUCH_END, this.onEnd, this); this.node.on(Node.EventType.TOUCH_CANCEL, this.cancel, this); } onDisable(): void { this.node.targetOff(this); this.cancel(); } onDestroy(): void { this.surface?.destroy(); } startNew(seed = Date.now() >>> 0): void { this.seed = seed; this.game = new FlowGame(seed); this.gesture = new Gesture(() => this.game.balls, canFlowAppend); this.animation = null; this.touchId = null; this.hints = this.game.hint(); this.emitHUD(); } retry(): void { this.startNew(this.seed); } undo(): void { if (!this.animation && this.game.undo()) { this.cancel(); this.hints = []; this.emitHUD(); } } showHint(): void { if (!this.animation) { this.cancel(); this.hints = this.game.hint(); } } shuffle(): void { if (!this.animation) { this.cancel(); this.begin(this.game.shuffle()); } } snapshot() { return this.game.snapshot(); } private emitHUD(): void { this.node.emit('balloon-hud', { snapshot: this.game.snapshot(), canUndo: this.game.canUndo, busy: !!this.animation, fever: this.game.fever }); } private begin(result: FlowResult): void { this.hints = []; this.seenReaction = 0; this.animation = new FlowAnimation(result, this.time); this.node.emit('balloon-merge', result); this.emitHUD(); } private toPoint(e: EventTouch): Point { const p = e.getUILocation(); const local = this.surface.getComponent(UITransform)!.convertToNodeSpaceAR(new Vec3(p.x, p.y, 0)); return { x: local.x + WIDTH / 2, y: HEIGHT / 2 - local.y }; } private onStart(e: EventTouch): void { if (this.touchId !== null || this.animation) return; this.touchId = e.getID(); this.hints = []; this.gesture.down(this.toPoint(e)); } private onMove(e: EventTouch): void { if (e.getID() === this.touchId) this.gesture.move(this.toPoint(e)); } private onEnd(e: EventTouch): void { if (e.getID() !== this.touchId) return; this.gesture.move(this.toPoint(e)); const path = this.gesture.up(); this.touchId = null; const merged = this.game.commit(path); if (merged) this.begin(merged); } private cancel(e?: EventTouch): void { if (e && e.getID() !== this.touchId) return; this.touchId = null; this.gesture?.cancel(); } update(dt: number): void { this.time += dt * 1000; const ui = this.node.getComponent(UITransform)!; const scale = Math.min(ui.width / WIDTH, ui.height / HEIGHT); this.surface.setScale(scale, scale, 1); // node 可以使用任意 anchor,绘图始终居中于 UITransform 内容区域。 this.surface.setPosition((.5 - ui.anchorX) * ui.width, (.5 - ui.anchorY) * ui.height, 0); let balls = this.game.balls; if (this.animation) { const sample = this.animation.sample(this.time); if (sample) { balls = sample; const current = this.animation.reaction; if (current && current.index > this.seenReaction) { for (const stage of this.animation.result.stages.filter(s => s.index > this.seenReaction && s.index <= current.index)) this.node.emit('balloon-reaction', stage, this.animation.result.doubled); this.seenReaction = current.index; } } else { const result = this.animation.result; this.animation = null; this.emitHUD(); this.node.emit('balloon-settled', result, this.game.snapshot()); } } this.render(drawBoard({ balls, chain: this.animation ? this.animation.activePath : this.gesture.chain, hint: this.animation ? [] : this.hints, time: this.time / 1000, fever: this.game.fever })); } private color(hex: string, alpha = 1): Color { const value = parseInt(hex.slice(1), 16); return new Color((value >> 16) & 255, (value >> 8) & 255, value & 255, Math.round(alpha * 255)); } private render(commands: Draw[]): void { const g = this.graphics; g.clear(); let labelIndex = 0; for (const d of commands) { if (d.type === 'text') { let label = this.labels[labelIndex++]; if (!label) { const node = new Node('BalloonLabel'); node.layer = this.node.layer; this.surface.addChild(node); node.addComponent(UITransform).setContentSize(340, 60); label = node.addComponent(Label); label.horizontalAlign = HorizontalTextAlignment.CENTER; label.verticalAlign = VerticalTextAlignment.CENTER; label.fontFamily = 'Arial'; this.labels.push(label); } label.node.active = true; label.node.setPosition(d.x - WIDTH / 2, HEIGHT / 2 - d.y, 0); label.string = d.text; label.fontSize = d.size; label.lineHeight = d.size * 1.3; label.isBold = (d.weight || 600) >= 650; label.color = this.color(d.fill || '#40678e', d.alpha); label.enableOutline = !!d.stroke; label.outlineWidth = d.width || 1; label.outlineColor = this.color(d.stroke || '#ffffff', d.alpha); continue; } g.fillColor = this.color(d.fill || '#ffffff', d.alpha); g.strokeColor = this.color(d.stroke || '#ffffff', d.alpha); g.lineWidth = d.width || 1; if (d.type === 'circle') g.circle(d.x - WIDTH / 2, HEIGHT / 2 - d.y, d.r); else if (d.type === 'ellipse') g.ellipse(d.x - WIDTH / 2, HEIGHT / 2 - d.y, d.rx, d.ry); else if (d.dash) { // Graphics 没有 Canvas 的 setLineDash;逐段画虚线。 for (let i = 1; i < d.points.length; i++) { const a = d.points[i - 1], b = d.points[i], length = Math.hypot(b.x - a.x, b.y - a.y); for (let t = 0; t < length; t += d.dash[0] + d.dash[1]) { const end = Math.min(length, t + d.dash[0]); g.moveTo(a.x + (b.x - a.x) * t / length - WIDTH / 2, HEIGHT / 2 - a.y - (b.y - a.y) * t / length); g.lineTo(a.x + (b.x - a.x) * end / length - WIDTH / 2, HEIGHT / 2 - a.y - (b.y - a.y) * end / length); } } } else { d.points.forEach((p, i) => i ? g.lineTo(p.x - WIDTH / 2, HEIGHT / 2 - p.y) : g.moveTo(p.x - WIDTH / 2, HEIGHT / 2 - p.y)); if (d.close) g.close(); } if (d.fill) g.fill(); if (d.stroke) g.stroke(); } for (let i = labelIndex; i < this.labels.length; i++) this.labels[i].node.active = false; } }