import { RainbowCell, RainbowCut, RAINBOW_SHAPES, rainbowPriority } from "./RainbowRules"; export interface RocketCut { cell: RainbowCell; fragments: RainbowCut[]; } function key(cells: RainbowCell[]): string { const x = Math.min(...cells.map(p => p.x)), y = Math.min(...cells.map(p => p.y)); return cells.map(p => (p.x - x) + "," + (p.y - y)).sort().join(";"); } /** 每次仅删除一格;其余四邻接连通片各自匹配已有形状。 */ export function rocketCuts(cells: RainbowCell[], edgeOnly = false): RocketCut[] { return cells.map(cell => { const pending = cells.filter(p => p !== cell).slice(); const fragments: RainbowCut[] = []; while (pending.length) { const group = [pending.pop()]; for (let i = 0; i < group.length; i++) { for (let j = pending.length - 1; j >= 0; j--) { if (Math.abs(pending[j].x - group[i].x) + Math.abs(pending[j].y - group[i].y) === 1) { group.push(pending.splice(j, 1)[0]); } } } const shape = RAINBOW_SHAPES.findIndex(points => points.length === group.length && key(points) === key(group)); if (shape < 0) throw new Error("火箭残块缺少对应形状: " + key(group)); fragments.push({ cell, shape, offset: { x: Math.min(...group.map(p => p.x)) - Math.min(...RAINBOW_SHAPES[shape].map(p => p.x)), y: Math.min(...group.map(p => p.y)) - Math.min(...RAINBOW_SHAPES[shape].map(p => p.y)) } }); } return { cell, fragments }; }).filter(cut => !edgeOnly || cut.fragments.length === 1); } export const ROCKET_CUTS = RAINBOW_SHAPES.map(shape => rocketCuts(shape)); export function rocketEdgeOnly(block: any, protectedColors: Set): boolean { const info = block.block_Info; return protectedColors.has(block.color) || (block.type !== 0 && block.type !== 16) || !!block.flowerType || info.lock != null || info.colorArray != null || info.colorChange != null || !!info.horizontal || !!info.vertical || info.floorMove === true || (block.teamBlocks || []).length > 1; } export function rocketCandidates(map: any): any[] { const blocks = map.blocks.filter(node => cc.isValid(node, true) && node.active) .map(node => node.getComponent("Block")).filter(Boolean); const buttons = new Set((map.buttonBlock || []).map(node => node.getComponent("MapBlock").color)); const protectedColors = new Set(); (map.wallArray || []).forEach(node => { const wall = node.getChildByName("wall").getComponent("Wall"); (wall.colorStartArray || []).forEach(color => protectedColors.add(color)); if (wall.special === 2) protectedColors.add(wall.color); }); const glued = new Set(); blocks.forEach(block => { if (block.type === 9 || block.type === 19) { glued.add(block.node); if (block.block_Info.node) glued.add(block.block_Info.node); } if ((block.adhesiveNode || []).some(node => cc.isValid(node, true) && node.active && node.getComponent("Adhesive")?.target)) glued.add(block.node); }); return blocks.filter(block => !block.spawnLocked && !block.over && !block.rainbowEliminating && !block.oldRainbow && block.color !== 100 && block.color !== 11 && [10, 11, 22].indexOf(block.type) < 0 && !!ROCKET_CUTS[block.block_Info.block]) .map(block => { const upper = block.type === 1 && block.block_Info.node?.getComponent("Block"); const layers = upper ? [block, upper] : [block]; if (layers.some(layer => glued.has(layer.node) || buttons.has(layer.color))) return null; const edgeOnly = layers.some(layer => rocketEdgeOnly(layer, protectedColors)); const cuts = ROCKET_CUTS[block.block_Info.block].filter(cut => !edgeOnly || cut.fragments.length === 1); return { block, cuts, edgeOnly, origin: block.rocketOrigin || block.node.uuid, priority: block.allBlocks.length === 1 ? 2 : layers.some(layer => rainbowPriority(layer) > 0) ? 1 : 0 }; }).filter(candidate => candidate && candidate.cuts.length); } export function chooseRocketTarget(candidates: any[], hit: Set, remaining: number, random: () => number = Math.random): any { if (!candidates.length) return null; const priority = Math.min(...candidates.map(candidate => candidate.priority)); let pool = candidates.filter(candidate => candidate.priority === priority); const fresh = pool.filter(candidate => !hit.has(candidate.origin)); if (fresh.length) pool = fresh; const target = pool[Math.floor(random() * pool.length)]; // 1932 等候选不足的开局,重复从边缘裁切,保留主体供后续火箭命中。 const edges = target.cuts.filter(cut => cut.fragments.length === 1); const cuts = candidates.length < remaining && edges.length ? edges : target.cuts; return { target, cut: cuts[Math.floor(random() * cuts.length)] }; } export interface RocketShot { x: number; y: number; origin: string; cut: RocketCut; } /** 只在候选快照上模拟逐发裁切;同一原块的实际命中须保持规划顺序。 */ export function planRocketShots(map: any, count = 3, random: () => number = Math.random): RocketShot[] { let pool = rocketCandidates(map).map(candidate => ({ ...candidate, x: candidate.block.posX, y: candidate.block.posY })); const shots: RocketShot[] = [], hit = new Set(); for (let remaining = count; remaining > 0; remaining--) { const choice = chooseRocketTarget(pool, hit, remaining, random); if (!choice) break; const { target, cut } = choice; shots.push({ x: target.x + cut.cell.x, y: target.y + cut.cell.y, origin: target.origin, cut }); hit.add(target.origin); pool = pool.filter(candidate => candidate !== target); for (const fragment of cut.fragments) { const cuts = ROCKET_CUTS[fragment.shape].filter(next => !target.edgeOnly || next.fragments.length === 1); if (cuts.length) pool.push({ ...target, cuts, x: target.x + fragment.offset.x, y: target.y + fragment.offset.y, priority: RAINBOW_SHAPES[fragment.shape].length === 1 ? 2 : target.priority }); } } return shots; }