import React, { FC, MouseEventHandler, useEffect, useRef, useState, } from 'react'; import './App.css'; import { GithubIcon } from './GithubIcon'; import { randomString, waitTimeout } from './utils'; import { defaultTheme } from './themes/default'; import { Icon, Theme } from './themes/interface'; import { fishermanTheme } from './themes/fisherman'; import { jinlunTheme } from './themes/jinlun'; import { ikunTheme } from './themes/ikun'; import { pddTheme } from './themes/pdd'; import { BeiAn } from './BeiAn'; import { Info } from './Info'; // 主题 const themes = [defaultTheme, fishermanTheme, jinlunTheme, ikunTheme, pddTheme]; // 最大关卡 const maxLevel = 50; interface MySymbol { id: string; status: number; // 0->1->2 isCover: boolean; x: number; y: number; icon: Icon; } type Scene = MySymbol[]; // 8*8网格 4*4->8*8 const makeScene: (level: number, icons: Icon[]) => Scene = (level, icons) => { const curLevel = Math.min(maxLevel, level); const iconPool = icons.slice(0, 2 * curLevel); const offsetPool = [0, 25, -25, 50, -50].slice(0, 1 + curLevel); const scene: Scene = []; const range = [ [2, 6], [1, 6], [1, 7], [0, 7], [0, 8], ][Math.min(4, curLevel - 1)]; const randomSet = (icon: Icon) => { const offset = offsetPool[Math.floor(offsetPool.length * Math.random())]; const row = range[0] + Math.floor((range[1] - range[0]) * Math.random()); const column = range[0] + Math.floor((range[1] - range[0]) * Math.random()); scene.push({ isCover: false, status: 0, icon, id: randomString(6), x: column * 100 + offset, y: row * 100 + offset, }); }; // 大于5级别增加icon池 let compareLevel = curLevel; while (compareLevel > 0) { iconPool.push( ...iconPool.slice(0, Math.min(10, 2 * (compareLevel - 5))) ); compareLevel -= 5; } for (const icon of iconPool) { for (let i = 0; i < 6; i++) { randomSet(icon); } } return scene; }; // o(n) 时间复杂度的洗牌算法 const fastShuffle: (arr: T[]) => T[] = (arr) => { const res = arr.slice(); for (let i = 0; i < res.length; i++) { const idx = (Math.random() * res.length) >> 0; [res[i], res[idx]] = [res[idx], res[i]]; } return res; }; // 洗牌 const washScene: (level: number, scene: Scene) => Scene = (level, scene) => { const updateScene = fastShuffle(scene); const offsetPool = [0, 25, -25, 50, -50].slice(0, 1 + level); const range = [ [2, 6], [1, 6], [1, 7], [0, 7], [0, 8], ][Math.min(4, level - 1)]; const randomSet = (symbol: MySymbol) => { const offset = offsetPool[Math.floor(offsetPool.length * Math.random())]; const row = range[0] + Math.floor((range[1] - range[0]) * Math.random()); const column = range[0] + Math.floor((range[1] - range[0]) * Math.random()); symbol.x = column * 100 + offset; symbol.y = row * 100 + offset; symbol.isCover = false; }; for (const symbol of updateScene) { if (symbol.status !== 0) continue; randomSet(symbol); } return updateScene; }; interface SymbolProps extends MySymbol { onClick: MouseEventHandler; } const Symbol: FC = ({ x, y, icon, isCover, status, onClick }) => { return (
{typeof icon.content === 'string' ? ( {icon.content} ) : ( icon.content )}
); }; const App: FC = () => { const [curTheme, setCurTheme] = useState>(defaultTheme); const [scene, setScene] = useState(makeScene(1, curTheme.icons)); const [level, setLevel] = useState(1); const [queue, setQueue] = useState([]); const [sortedQueue, setSortedQueue] = useState< Record >({}); const [finished, setFinished] = useState(false); const [tipText, setTipText] = useState(''); const [animating, setAnimating] = useState(false); // 音效 const soundRefMap = useRef>({}); // 第一次点击时播放bgm const bgmRef = useRef(null); const [bgmOn, setBgmOn] = useState(false); const [once, setOnce] = useState(false); useEffect(() => { if (!bgmRef.current) return; if (bgmOn) { bgmRef.current.volume = 0.5; bgmRef.current.play(); } else { bgmRef.current?.pause(); } }, [bgmOn]); // 主题切换 useEffect(() => { // 初始化时不加载bgm if (once) { setBgmOn(false); setTimeout(() => { setBgmOn(true); }, 300); } restart(); }, [curTheme]); // 队列区排序 useEffect(() => { const cache: Record = {}; // 加上索引,避免以id字典序来排 const idx = 0; for (const symbol of queue) { if (cache[idx + symbol.icon.name]) { cache[idx + symbol.icon.name].push(symbol); } else { cache[idx + symbol.icon.name] = [symbol]; } } const temp = []; for (const symbols of Object.values(cache)) { temp.push(...symbols); } const updateSortedQueue: typeof sortedQueue = {}; let x = 50; for (const symbol of temp) { updateSortedQueue[symbol.id] = x; x += 100; } setSortedQueue(updateSortedQueue); }, [queue]); // 初始化覆盖状态 useEffect(() => { checkCover(scene); }, []); // 向后检查覆盖 const checkCover = (scene: Scene) => { const updateScene = scene.slice(); for (let i = 0; i < updateScene.length; i++) { // 当前item对角坐标 const cur = updateScene[i]; cur.isCover = false; if (cur.status !== 0) continue; const { x: x1, y: y1 } = cur; const x2 = x1 + 100, y2 = y1 + 100; for (let j = i + 1; j < updateScene.length; j++) { const compare = updateScene[j]; if (compare.status !== 0) continue; // 两区域有交集视为选中 // 两区域不重叠情况取反即为交集 const { x, y } = compare; if (!(y + 100 <= y1 || y >= y2 || x + 100 <= x1 || x >= x2)) { cur.isCover = true; break; } } } setScene(updateScene); }; // 弹出 const pop = () => { if (!queue.length) return; const updateQueue = queue.slice(); const symbol = updateQueue.shift(); if (!symbol) return; const find = scene.find((s) => s.id === symbol.id); if (find) { setQueue(updateQueue); find.status = 0; find.x = 100 * Math.floor(8 * Math.random()); find.y = 700; checkCover(scene); // 音效 if (soundRefMap.current?.['sound-shift']) { soundRefMap.current['sound-shift'].currentTime = 0; soundRefMap.current['sound-shift'].play(); } } }; // 撤销 const undo = () => { if (!queue.length) return; const updateQueue = queue.slice(); const symbol = updateQueue.pop(); if (!symbol) return; const find = scene.find((s) => s.id === symbol.id); if (find) { setQueue(updateQueue); find.status = 0; checkCover(scene); // 音效 if (soundRefMap.current?.['sound-undo']) { soundRefMap.current['sound-undo'].currentTime = 0; soundRefMap.current['sound-undo'].play(); } } }; // 洗牌 const wash = () => { checkCover(washScene(level, scene)); // 音效 if (soundRefMap.current?.['sound-wash']) { soundRefMap.current['sound-wash'].currentTime = 0; soundRefMap.current['sound-wash'].play(); } }; // 加大难度 const levelUp = () => { if (level >= maxLevel) { return; } setFinished(false); setLevel(level + 1); setQueue([]); checkCover(makeScene(level + 1, curTheme.icons)); }; // 重开 const restart = () => { setFinished(false); setLevel(1); setQueue([]); checkCover(makeScene(1, curTheme.icons)); }; // 点击item const clickSymbol = async (idx: number) => { if (finished || animating) return; if (!once) { setBgmOn(true); setOnce(true); } const updateScene = scene.slice(); const symbol = updateScene[idx]; if (symbol.isCover || symbol.status !== 0) return; symbol.status = 1; // 点击音效 if (soundRefMap.current) { soundRefMap.current[symbol.icon.clickSound].currentTime = 0; soundRefMap.current[symbol.icon.clickSound].play(); } let updateQueue = queue.slice(); updateQueue.push(symbol); setQueue(updateQueue); checkCover(updateScene); setAnimating(true); await waitTimeout(150); const filterSame = updateQueue.filter((sb) => sb.icon === symbol.icon); // 三连了 if (filterSame.length === 3) { updateQueue = updateQueue.filter((sb) => sb.icon !== symbol.icon); for (const sb of filterSame) { const find = updateScene.find((i) => i.id === sb.id); if (find) { find.status = 2; // 三连音效 if (soundRefMap.current) { soundRefMap.current[ symbol.icon.tripleSound ].currentTime = 0; soundRefMap.current[symbol.icon.tripleSound].play(); } } } } // 输了 if (updateQueue.length === 7) { setTipText('失败了'); setFinished(true); } if (!updateScene.find((s) => s.status !== 2)) { // 胜利 if (level === maxLevel) { setTipText('完成挑战'); setFinished(true); return; } // 升级 setLevel(level + 1); setQueue([]); checkCover(makeScene(level + 1, curTheme.icons)); } else { setQueue(updateQueue); checkCover(updateScene); } setAnimating(false); }; return ( <>

{curTheme.title}

主题: Level: {level}

{curTheme.desc}
{scene.map((item, idx) => ( clickSymbol(idx)} /> ))}
{/**/}
{finished && (

{tipText}

)} {/*bgm*/} {/*音效*/} {curTheme.sounds.map((sound) => (