diff --git a/src/App.scss b/src/App.scss index c78dff5..d4da44e 100644 --- a/src/App.scss +++ b/src/App.scss @@ -4,106 +4,3 @@ max-width: 500px; margin: 0 auto; } - -.app { - width: 100%; - margin: 0 auto; -} - -.scene { - &-container { - width: 100%; - padding-bottom: 100%; - position: relative; - margin: 10% 0; - } - - &-inner { - position: absolute; - left: 0; - right: 0; - bottom: 0; - top: 0; - overflow: visible; - font-size: 28px; - } -} - -.symbol { - width: 12.5%; - padding-bottom: 12.5%; - position: absolute; - transition: 150ms; - left: 0; - top: 0; - border-radius: 8px; - - &-inner { - position: absolute; - left: 0; - right: 0; - bottom: 0; - top: 0; - display: flex; - justify-content: center; - align-items: center; - border-radius: 8px; - border: 2px solid #444; - transition: 0.3s; - overflow: hidden; - user-select: none; - - img { - width: 100%; - height: 100%; - object-fit: cover; - -webkit-user-drag: none; - } - } -} - -.queue-container { - border-radius: 8px; - width: 100%; - padding-bottom: 15%; - border: 2px solid gray; - margin-bottom: 16px; -} - -.modal { - position: fixed; - width: 100vw; - height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - backdrop-filter: blur(10px); - background-color: rgb(0 0 0 / 10%); - top: 0; - left: 0; -} - -.bgm-button { - position: fixed; - left: 0; - top: 0; - padding: 4px; - width: 36px; - height: 36px; -} - -.zhenghuo-button { - width: 100%; - margin-top: 8px; -} - -.background { - position: fixed; - left: 0; - top: 0; - width: 100vw; - height: 100vh; - object-fit: cover; - z-index: -1; -} diff --git a/src/App.tsx b/src/App.tsx index 62b0603..6ec42dc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,30 +1,21 @@ -import React, { - FC, - MouseEventHandler, - useEffect, - useRef, - useState, -} from 'react'; +import React, { FC, useState } from 'react'; import './App.scss'; -import { BilibiliLink, PersonalInfo } from './components/PersonalInfo'; import { + LAST_LEVEL_STORAGE_KEY, parsePathCustomThemeId, parsePathThemeName, - randomString, - waitTimeout, } from './utils'; import { defaultTheme } from './themes/default'; -import { Icon, Theme } from './themes/interface'; +import { 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 './components/BeiAn'; -import { Info } from './components/Info'; import { owTheme } from './themes/ow'; -import { ConfigDialog } from './components/ConfigDialog'; -import Bmob from 'hydrogen-js-sdk'; +import Game from './Game'; +import { Loading } from './components/Loading'; +import { BeiAn } from './components/BeiAn'; // 内置主题 const builtInThemes: Theme[] = [ @@ -35,154 +26,9 @@ const builtInThemes: Theme[] = [ pddTheme, owTheme, ]; - -// 最大关卡 -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.startsWith('http') ? ( - /*图片外链*/ - - ) : ( - /*字符表情*/ - {icon.content} - ) - ) : ( - /*ReactNode*/ - icon.content - )} -
-
- ); -}; - // 从url初始化主题 -const themeFromPath: string = parsePathThemeName(location.href); +const themeFromPath = parsePathThemeName(location.href); const customThemeIdFromPath = parsePathCustomThemeId(location.href); -const CUSTOM_THEME_FAIL_TIP = '查询配置失败'; const initTheme = customThemeIdFromPath ? { title: '', icons: [], sounds: [], name: '' } : themeFromPath @@ -191,444 +37,40 @@ const initTheme = customThemeIdFromPath : defaultTheme; // 读取缓存关卡数 -const LAST_LEVEL_STORAGE_KEY = 'lastLevel'; const initLevel = Number(localStorage.getItem(LAST_LEVEL_STORAGE_KEY) || '1'); const App: FC = () => { - const [curTheme, setCurTheme] = useState>(initTheme); - const [themes, setThemes] = useState[]>(builtInThemes); - const [pureMode, setPureMode] = useState(!!customThemeIdFromPath); - - const [scene, setScene] = useState( - makeScene(initLevel, curTheme.icons) - ); - const [level, setLevel] = useState(initLevel); - const [queue, setQueue] = useState([]); - const [sortedQueue, setSortedQueue] = useState< - Record - >({}); - const [finished, setFinished] = useState(false); - const [tipText, setTipText] = useState(''); - const [animating, setAnimating] = useState(false); - const [configDialogShow, setConfigDialogShow] = 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().then(); - } else { - bgmRef.current.pause(); - } - }, [bgmOn]); - - // 关卡缓存 - useEffect(() => { - localStorage.setItem(LAST_LEVEL_STORAGE_KEY, level.toString()); - }, [level]); - - // 初始化主题 - useEffect(() => { - if (customThemeIdFromPath) { - // 自定义主题 - Bmob.Query('config') - .get(customThemeIdFromPath) - .then((res) => { - // @ts-ignore - const { content } = res; - try { - const customTheme = JSON.parse(content); - if (!customTheme.pure) { - setPureMode(false); - setThemes([...themes, customTheme]); - } - setCurTheme(customTheme); - } catch (e) { - console.log(e); - } - }) - .catch((e) => { - setCurTheme({ ...curTheme, title: CUSTOM_THEME_FAIL_TIP }); - console.log(e); - }); - } - }, []); - - // 主题切换 - useEffect(() => { - // 初始化时不加载bgm - if (once) { - setBgmOn(false); - setTimeout(() => { - setBgmOn(true); - }, 300); - } - restart(); - // 更改路径query - if (customThemeIdFromPath) return; - history.pushState( - {}, - curTheme.title, - `/?theme=${encodeURIComponent(curTheme.name)}` - ); - }, [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; + console.log('???'); + const [theme, setTheme] = useState>(initTheme); + const [loading, setLoading] = useState(!!customThemeIdFromPath); + const [error, setError] = useState(''); + if (customThemeIdFromPath) { + // debugger + // 自定义主题 + /*Bmob.Query('config') + .get(customThemeIdFromPath) + .then((res) => { + // @ts-ignore + const { content } = res; + try { + const customTheme = JSON.parse(content); + setTheme(customTheme); + setLoading(false); + } catch (e) { + setError('主题配置解析失败'); } - } - } - setScene(updateScene); - }; + }) + .catch(({ error }) => { + setError(error); + });*/ + } - // 弹出 - 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] - ) { - soundRefMap.current[symbol.icon.clickSound].currentTime = 0; - soundRefMap.current[symbol.icon.clickSound].play().then(); - } - - 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] - ) { - soundRefMap.current[ - symbol.icon.tripleSound - ].currentTime = 0; - soundRefMap.current[symbol.icon.tripleSound] - .play() - .then(); - } - } - } - } - - // 输了 - 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); - }; - - // 自定义整活 - const customZhenghuo = (theme: Theme) => { - setCurTheme(theme); - }; - - return ( + return loading ? ( + + ) : ( <> - {curTheme.background && ( - background - )} -

- {curTheme.title}{' '} - {curTheme.title === CUSTOM_THEME_FAIL_TIP && ( - 返回首页 - )} -

- - {curTheme.desc} - - {!pureMode && } -

- {!pureMode && ( - <> - 主题: - {/*TODO themes维护方式调整*/} - - - )} - Level: {level} -

- -
-
-
- {scene.map((item, idx) => ( - clickSymbol(idx)} - /> - ))} -
-
-
-
-
- - - - -
- - {!pureMode && ( - - )} - - - + - - {pureMode && } - - {/*提示弹窗*/} - {finished && ( -
-

{tipText}

- -
- )} - - {/*自定义主题弹窗*/} - setConfigDialogShow(false)} - previewMethod={customZhenghuo} - /> - - {/*bgm*/} - - - {/*音效*/} - {curTheme.sounds.map((sound) => ( -