mirror of
https://ghproxy.com/https://github.com/StreakingMan/solvable-sheep-game
synced 2025-07-07 15:26:06 +08:00
feat: 计时、最大关卡配置
This commit is contained in:
parent
84fa8483d9
commit
b7482fa209
|
@ -10,6 +10,7 @@ module.exports = {
|
||||||
'stylelint-config-prettier-scss',
|
'stylelint-config-prettier-scss',
|
||||||
],
|
],
|
||||||
rules: {
|
rules: {
|
||||||
'selector-class-pattern': '^[a-z][a-zA-Z0-9]+$',
|
// 后续统一
|
||||||
|
'selector-class-pattern': '^[a-zA-Z0-9-_]+$',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -56,7 +56,8 @@ vite+react 实现,欢迎 star、issue、pr、fork(尽量标注原仓库地
|
||||||
- [x] 关卡生成
|
- [x] 关卡生成
|
||||||
- [x] UI/UX 优化
|
- [x] UI/UX 优化
|
||||||
- [x] 多主题
|
- [x] 多主题
|
||||||
- [ ] 计时、得分排行、保存进度机制
|
- [x] 计时、得分、保存进度机制
|
||||||
|
- [ ] 排行榜
|
||||||
- [ ] 性能优化
|
- [ ] 性能优化
|
||||||
- [x] BGM/音效
|
- [x] BGM/音效
|
||||||
- [ ] ~~点击时的缓冲队列,优化交互动画效果~~
|
- [ ] ~~点击时的缓冲队列,优化交互动画效果~~
|
||||||
|
|
|
@ -37,6 +37,7 @@ yarn install
|
||||||
- backgroundBlur 背景图片是否添加毛玻璃效果
|
- backgroundBlur 背景图片是否添加毛玻璃效果
|
||||||
- backgroundColor 背景颜色 CSS 色值
|
- backgroundColor 背景颜色 CSS 色值
|
||||||
- pure 纯净模式,DIY 时已开启
|
- pure 纯净模式,DIY 时已开启
|
||||||
|
- maxLevel 最大关卡数,默认 50
|
||||||
- sounds 音效数组
|
- sounds 音效数组
|
||||||
- name 名称
|
- name 名称
|
||||||
- src 音效文件相对于 `diy/public` 的路径
|
- src 音效文件相对于 `diy/public` 的路径
|
||||||
|
@ -88,9 +89,9 @@ ps: 如果您的项目托管在公共仓库中,请注意保护密钥,本地
|
||||||
|
|
||||||
应用创建后,点击【云数据库】,创建两个表 `config` 和 `file`
|
应用创建后,点击【云数据库】,创建两个表 `config` 和 `file`
|
||||||
|
|
||||||
`config` 表用来存储自定义配置的json字符串,需要新增 `content` 列
|
`config` 表用来存储自定义配置的 json 字符串,需要新增 `content` 列
|
||||||
|
|
||||||
`file` 表则是为了vercel流量,将一些默认文件转为base64编码存到了数据库中,需要添加三列
|
`file` 表则是为了 vercel 流量,将一些默认文件转为 base64 编码存到了数据库中,需要添加三列
|
||||||

|

|
||||||
|
|
||||||
最后,开发和打包命令分别使用 `yarn dev` 和 `yarn build` 即可
|
最后,开发和打包命令分别使用 `yarn dev` 和 `yarn build` 即可
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
"backgroundBlur": false,
|
"backgroundBlur": false,
|
||||||
"backgroundColor": "#8dac85",
|
"backgroundColor": "#8dac85",
|
||||||
"pure": true,
|
"pure": true,
|
||||||
|
"maxLevel": 50,
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"name": "1",
|
"name": "1",
|
||||||
|
|
|
@ -4,6 +4,7 @@ import {
|
||||||
domRelatedOptForTheme,
|
domRelatedOptForTheme,
|
||||||
LAST_LEVEL_STORAGE_KEY,
|
LAST_LEVEL_STORAGE_KEY,
|
||||||
LAST_SCORE_STORAGE_KEY,
|
LAST_SCORE_STORAGE_KEY,
|
||||||
|
LAST_TIME_STORAGE_KEY,
|
||||||
wrapThemeDefaultSounds,
|
wrapThemeDefaultSounds,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
import { Theme } from './themes/interface';
|
import { Theme } from './themes/interface';
|
||||||
|
@ -18,6 +19,7 @@ const ConfigDialog = React.lazy(() => import('./components/ConfigDialog'));
|
||||||
// 读取缓存关卡得分
|
// 读取缓存关卡得分
|
||||||
const initLevel = Number(localStorage.getItem(LAST_LEVEL_STORAGE_KEY) || '1');
|
const initLevel = Number(localStorage.getItem(LAST_LEVEL_STORAGE_KEY) || '1');
|
||||||
const initScore = Number(localStorage.getItem(LAST_SCORE_STORAGE_KEY) || '0');
|
const initScore = Number(localStorage.getItem(LAST_SCORE_STORAGE_KEY) || '0');
|
||||||
|
const initTime = Number(localStorage.getItem(LAST_TIME_STORAGE_KEY) || '0');
|
||||||
|
|
||||||
const App: FC<{ theme: Theme<any> }> = ({ theme: initTheme }) => {
|
const App: FC<{ theme: Theme<any> }> = ({ theme: initTheme }) => {
|
||||||
console.log('initTheme', initTheme);
|
console.log('initTheme', initTheme);
|
||||||
|
@ -69,6 +71,7 @@ const App: FC<{ theme: Theme<any> }> = ({ theme: initTheme }) => {
|
||||||
theme={theme}
|
theme={theme}
|
||||||
initLevel={initLevel}
|
initLevel={initLevel}
|
||||||
initScore={initScore}
|
initScore={initScore}
|
||||||
|
initTime={initTime}
|
||||||
/>
|
/>
|
||||||
<PersonalInfo />
|
<PersonalInfo />
|
||||||
<div className={'flex-spacer'} />
|
<div className={'flex-spacer'} />
|
||||||
|
|
|
@ -105,8 +105,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.level {
|
.level {
|
||||||
font-size: 1.8em;
|
font-size: 1.5em;
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
line-height: 2;
|
line-height: 2;
|
||||||
text-shadow: 4px 6px 2px rgb(0 0 0 / 20%);
|
text-shadow: 4px 6px 2px rgb(0 0 0 / 20%);
|
||||||
|
font-family: Menlo, Monaco, 'Courier New', monospace, serif;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,53 +5,56 @@ import React, {
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
import './Game.scss';
|
import './Game.scss';
|
||||||
import {
|
import {
|
||||||
LAST_LEVEL_STORAGE_KEY,
|
LAST_LEVEL_STORAGE_KEY,
|
||||||
LAST_SCORE_STORAGE_KEY,
|
LAST_SCORE_STORAGE_KEY,
|
||||||
|
LAST_TIME_STORAGE_KEY,
|
||||||
randomString,
|
randomString,
|
||||||
waitTimeout,
|
waitTimeout,
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
import { Icon, Theme } from '../themes/interface';
|
import { Icon, Theme } from '../themes/interface';
|
||||||
|
|
||||||
// 最大关卡
|
|
||||||
const maxLevel = 50;
|
|
||||||
|
|
||||||
interface MySymbol {
|
interface MySymbol {
|
||||||
id: string;
|
id: string;
|
||||||
status: number; // 0->1->2
|
status: number; // 0->1->2 正常->队列中->三连
|
||||||
isCover: boolean;
|
isCover: boolean;
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
icon: Icon;
|
icon: Icon;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Scene = MySymbol[];
|
type Scene = MySymbol[];
|
||||||
|
|
||||||
// 8*8网格 4*4->8*8
|
// 随机位置、偏移量
|
||||||
|
const randomPositionOffset: (
|
||||||
|
offsetPool: number[],
|
||||||
|
range: number[]
|
||||||
|
) => { offset: number; row: number; column: number } = (offsetPool, range) => {
|
||||||
|
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());
|
||||||
|
return { offset, row, column };
|
||||||
|
};
|
||||||
|
|
||||||
|
// 制作场景:8*8虚拟网格 4*4->8*8
|
||||||
|
const sceneRanges = [
|
||||||
|
[2, 6],
|
||||||
|
[1, 6],
|
||||||
|
[1, 7],
|
||||||
|
[0, 7],
|
||||||
|
[0, 8],
|
||||||
|
];
|
||||||
|
const offsets = [0, 25, -25, 50, -50];
|
||||||
const makeScene: (level: number, icons: Icon[]) => Scene = (level, icons) => {
|
const makeScene: (level: number, icons: Icon[]) => Scene = (level, icons) => {
|
||||||
const curLevel = Math.min(maxLevel, level);
|
// 初始图标x2
|
||||||
const iconPool = icons.slice(0, 2 * curLevel);
|
const iconPool = icons.slice(0, 2 * level);
|
||||||
const offsetPool = [0, 25, -25, 50, -50].slice(0, 1 + curLevel);
|
const offsetPool = offsets.slice(0, 1 + level);
|
||||||
|
|
||||||
const scene: Scene = [];
|
const scene: Scene = [];
|
||||||
|
// 网格范围,随等级由中心扩满
|
||||||
const range = [
|
const range = sceneRanges[Math.min(4, level - 1)];
|
||||||
[2, 6],
|
// 在范围内随机摆放图标
|
||||||
[1, 6],
|
|
||||||
[1, 7],
|
|
||||||
[0, 7],
|
|
||||||
[0, 8],
|
|
||||||
][Math.min(4, curLevel - 1)];
|
|
||||||
|
|
||||||
const randomSet = (icon: Icon) => {
|
const randomSet = (icon: Icon) => {
|
||||||
const offset =
|
const { offset, row, column } = randomPositionOffset(offsetPool, range);
|
||||||
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({
|
scene.push({
|
||||||
isCover: false,
|
isCover: false,
|
||||||
status: 0,
|
status: 0,
|
||||||
|
@ -61,22 +64,20 @@ const makeScene: (level: number, icons: Icon[]) => Scene = (level, icons) => {
|
||||||
y: row * 100 + offset,
|
y: row * 100 + offset,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
// 每间隔5级别增加icon池
|
||||||
// 大于5级别增加icon池
|
let compareLevel = level;
|
||||||
let compareLevel = curLevel;
|
|
||||||
while (compareLevel > 0) {
|
while (compareLevel > 0) {
|
||||||
iconPool.push(
|
iconPool.push(
|
||||||
...iconPool.slice(0, Math.min(10, 2 * (compareLevel - 5)))
|
...iconPool.slice(0, Math.min(10, 2 * (compareLevel - 5)))
|
||||||
);
|
);
|
||||||
compareLevel -= 5;
|
compareLevel -= 5;
|
||||||
}
|
}
|
||||||
|
// icon池中每个生成六张卡片
|
||||||
for (const icon of iconPool) {
|
for (const icon of iconPool) {
|
||||||
for (let i = 0; i < 6; i++) {
|
for (let i = 0; i < 6; i++) {
|
||||||
randomSet(icon);
|
randomSet(icon);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return scene;
|
return scene;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -92,40 +93,29 @@ const fastShuffle: <T = any>(arr: T[]) => T[] = (arr) => {
|
||||||
|
|
||||||
// 洗牌
|
// 洗牌
|
||||||
const washScene: (level: number, scene: Scene) => Scene = (level, scene) => {
|
const washScene: (level: number, scene: Scene) => Scene = (level, scene) => {
|
||||||
|
// 打乱顺序
|
||||||
const updateScene = fastShuffle(scene);
|
const updateScene = fastShuffle(scene);
|
||||||
const offsetPool = [0, 25, -25, 50, -50].slice(0, 1 + level);
|
const offsetPool = offsets.slice(0, 1 + level);
|
||||||
const range = [
|
const range = sceneRanges[Math.min(4, level - 1)];
|
||||||
[2, 6],
|
// 重新设置位置
|
||||||
[1, 6],
|
|
||||||
[1, 7],
|
|
||||||
[0, 7],
|
|
||||||
[0, 8],
|
|
||||||
][Math.min(4, level - 1)];
|
|
||||||
|
|
||||||
const randomSet = (symbol: MySymbol) => {
|
const randomSet = (symbol: MySymbol) => {
|
||||||
const offset =
|
const { offset, row, column } = randomPositionOffset(offsetPool, range);
|
||||||
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.x = column * 100 + offset;
|
||||||
symbol.y = row * 100 + offset;
|
symbol.y = row * 100 + offset;
|
||||||
symbol.isCover = false;
|
symbol.isCover = false;
|
||||||
};
|
};
|
||||||
|
// 仅对仍在牌堆中的进行重置
|
||||||
for (const symbol of updateScene) {
|
for (const symbol of updateScene) {
|
||||||
if (symbol.status !== 0) continue;
|
if (symbol.status !== 0) continue;
|
||||||
randomSet(symbol);
|
randomSet(symbol);
|
||||||
}
|
}
|
||||||
|
|
||||||
return updateScene;
|
return updateScene;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// icon对应的组件
|
||||||
interface SymbolProps extends MySymbol {
|
interface SymbolProps extends MySymbol {
|
||||||
onClick: MouseEventHandler;
|
onClick: MouseEventHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Symbol: FC<SymbolProps> = ({ x, y, icon, isCover, status, onClick }) => {
|
const Symbol: FC<SymbolProps> = ({ x, y, icon, isCover, status, onClick }) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -161,10 +151,11 @@ const Symbol: FC<SymbolProps> = ({ x, y, icon, isCover, status, onClick }) => {
|
||||||
|
|
||||||
const Game: FC<{
|
const Game: FC<{
|
||||||
theme: Theme<any>;
|
theme: Theme<any>;
|
||||||
initLevel: number;
|
initLevel?: number;
|
||||||
initScore: number;
|
initScore?: number;
|
||||||
}> = ({ theme, initLevel, initScore }) => {
|
initTime?: number;
|
||||||
console.log('Game FC');
|
}> = ({ theme, initLevel = 1, initScore = 0, initTime = 0 }) => {
|
||||||
|
const maxLevel = theme.maxLevel || 50;
|
||||||
const [scene, setScene] = useState<Scene>(
|
const [scene, setScene] = useState<Scene>(
|
||||||
makeScene(initLevel, theme.icons)
|
makeScene(initLevel, theme.icons)
|
||||||
);
|
);
|
||||||
|
@ -185,6 +176,7 @@ const Game: FC<{
|
||||||
const bgmRef = useRef<HTMLAudioElement>(null);
|
const bgmRef = useRef<HTMLAudioElement>(null);
|
||||||
const [bgmOn, setBgmOn] = useState<boolean>(false);
|
const [bgmOn, setBgmOn] = useState<boolean>(false);
|
||||||
const [once, setOnce] = useState<boolean>(false);
|
const [once, setOnce] = useState<boolean>(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!bgmRef.current) return;
|
if (!bgmRef.current) return;
|
||||||
if (bgmOn) {
|
if (bgmOn) {
|
||||||
|
@ -199,6 +191,7 @@ const Game: FC<{
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem(LAST_LEVEL_STORAGE_KEY, level.toString());
|
localStorage.setItem(LAST_LEVEL_STORAGE_KEY, level.toString());
|
||||||
localStorage.setItem(LAST_SCORE_STORAGE_KEY, score.toString());
|
localStorage.setItem(LAST_SCORE_STORAGE_KEY, score.toString());
|
||||||
|
localStorage.setItem(LAST_TIME_STORAGE_KEY, usedTime.toString());
|
||||||
}, [level]);
|
}, [level]);
|
||||||
|
|
||||||
// 队列区排序
|
// 队列区排序
|
||||||
|
@ -246,11 +239,9 @@ const Game: FC<{
|
||||||
for (let j = i + 1; j < updateScene.length; j++) {
|
for (let j = i + 1; j < updateScene.length; j++) {
|
||||||
const compare = updateScene[j];
|
const compare = updateScene[j];
|
||||||
if (compare.status !== 0) continue;
|
if (compare.status !== 0) continue;
|
||||||
|
|
||||||
// 两区域有交集视为选中
|
// 两区域有交集视为选中
|
||||||
// 两区域不重叠情况取反即为交集
|
// 两区域不重叠情况取反即为交集
|
||||||
const { x, y } = compare;
|
const { x, y } = compare;
|
||||||
|
|
||||||
if (!(y + 100 <= y1 || y >= y2 || x + 100 <= x1 || x >= x2)) {
|
if (!(y + 100 <= y1 || y >= y2 || x + 100 <= x1 || x >= x2)) {
|
||||||
cur.isCover = true;
|
cur.isCover = true;
|
||||||
break;
|
break;
|
||||||
|
@ -279,7 +270,7 @@ const Game: FC<{
|
||||||
// 音效
|
// 音效
|
||||||
if (soundRefMap.current?.['sound-shift']) {
|
if (soundRefMap.current?.['sound-shift']) {
|
||||||
soundRefMap.current['sound-shift'].currentTime = 0;
|
soundRefMap.current['sound-shift'].currentTime = 0;
|
||||||
soundRefMap.current['sound-shift'].play();
|
soundRefMap.current['sound-shift'].play().then();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -299,7 +290,7 @@ const Game: FC<{
|
||||||
// 音效
|
// 音效
|
||||||
if (soundRefMap.current?.['sound-undo']) {
|
if (soundRefMap.current?.['sound-undo']) {
|
||||||
soundRefMap.current['sound-undo'].currentTime = 0;
|
soundRefMap.current['sound-undo'].currentTime = 0;
|
||||||
soundRefMap.current['sound-undo'].play();
|
soundRefMap.current['sound-undo'].play().then();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -311,15 +302,17 @@ const Game: FC<{
|
||||||
// 音效
|
// 音效
|
||||||
if (soundRefMap.current?.['sound-wash']) {
|
if (soundRefMap.current?.['sound-wash']) {
|
||||||
soundRefMap.current['sound-wash'].currentTime = 0;
|
soundRefMap.current['sound-wash'].currentTime = 0;
|
||||||
soundRefMap.current['sound-wash'].play();
|
soundRefMap.current['sound-wash'].play().then();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 加大难度
|
// 加大难度,该方法由玩家点击下一关触发
|
||||||
const levelUp = () => {
|
const levelUp = () => {
|
||||||
if (level >= maxLevel) {
|
if (level >= maxLevel) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// 跳关扣关卡对应数值的分
|
||||||
|
setScore(score - level);
|
||||||
setFinished(false);
|
setFinished(false);
|
||||||
setLevel(level + 1);
|
setLevel(level + 1);
|
||||||
setQueue([]);
|
setQueue([]);
|
||||||
|
@ -333,15 +326,19 @@ const Game: FC<{
|
||||||
setLevel(1);
|
setLevel(1);
|
||||||
setQueue([]);
|
setQueue([]);
|
||||||
checkCover(makeScene(1, theme.icons));
|
checkCover(makeScene(1, theme.icons));
|
||||||
|
setUsedTime(0);
|
||||||
|
startTimer(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 点击item
|
// 点击item
|
||||||
const clickSymbol = async (idx: number) => {
|
const clickSymbol = async (idx: number) => {
|
||||||
if (finished || animating) return;
|
if (finished || animating) return;
|
||||||
|
|
||||||
|
// 第一次点击时,播放bgm,开启计时
|
||||||
if (!once) {
|
if (!once) {
|
||||||
setBgmOn(true);
|
setBgmOn(true);
|
||||||
setOnce(true);
|
setOnce(true);
|
||||||
|
startTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateScene = scene.slice();
|
const updateScene = scene.slice();
|
||||||
|
@ -350,26 +347,25 @@ const Game: FC<{
|
||||||
symbol.status = 1;
|
symbol.status = 1;
|
||||||
|
|
||||||
// 点击音效
|
// 点击音效
|
||||||
// 不知道为啥敲可选链会提示错误。。。
|
if (soundRefMap.current?.[symbol.icon.clickSound]) {
|
||||||
if (
|
|
||||||
soundRefMap.current &&
|
|
||||||
soundRefMap.current[symbol.icon.clickSound]
|
|
||||||
) {
|
|
||||||
soundRefMap.current[symbol.icon.clickSound].currentTime = 0;
|
soundRefMap.current[symbol.icon.clickSound].currentTime = 0;
|
||||||
soundRefMap.current[symbol.icon.clickSound].play().then();
|
soundRefMap.current[symbol.icon.clickSound].play().then();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 将点击项目加入队列
|
||||||
let updateQueue = queue.slice();
|
let updateQueue = queue.slice();
|
||||||
updateQueue.push(symbol);
|
updateQueue.push(symbol);
|
||||||
|
|
||||||
setQueue(updateQueue);
|
setQueue(updateQueue);
|
||||||
checkCover(updateScene);
|
checkCover(updateScene);
|
||||||
|
|
||||||
|
// 动画锁 150ms
|
||||||
setAnimating(true);
|
setAnimating(true);
|
||||||
await waitTimeout(150);
|
await waitTimeout(150);
|
||||||
|
|
||||||
|
// 查找当前队列中与点击项相同的
|
||||||
const filterSame = updateQueue.filter((sb) => sb.icon === symbol.icon);
|
const filterSame = updateQueue.filter((sb) => sb.icon === symbol.icon);
|
||||||
|
|
||||||
|
// 后续状态判断
|
||||||
// 三连了
|
// 三连了
|
||||||
if (filterSame.length === 3) {
|
if (filterSame.length === 3) {
|
||||||
// 三连一次+3分
|
// 三连一次+3分
|
||||||
|
@ -380,10 +376,7 @@ const Game: FC<{
|
||||||
if (find) {
|
if (find) {
|
||||||
find.status = 2;
|
find.status = 2;
|
||||||
// 三连音效
|
// 三连音效
|
||||||
if (
|
if (soundRefMap.current?.[symbol.icon.tripleSound]) {
|
||||||
soundRefMap.current &&
|
|
||||||
soundRefMap.current[symbol.icon.tripleSound]
|
|
||||||
) {
|
|
||||||
soundRefMap.current[
|
soundRefMap.current[
|
||||||
symbol.icon.tripleSound
|
symbol.icon.tripleSound
|
||||||
].currentTime = 0;
|
].currentTime = 0;
|
||||||
|
@ -422,6 +415,34 @@ const Game: FC<{
|
||||||
setAnimating(false);
|
setAnimating(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 计时相关
|
||||||
|
const [startTime, setStartTime] = useState<number>(0);
|
||||||
|
const [now, setNow] = useState<number>(0);
|
||||||
|
const [usedTime, setUsedTime] = useState<number>(initTime);
|
||||||
|
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
// 结束时重置计时器和关卡信息
|
||||||
|
useEffect(() => {
|
||||||
|
if (finished) {
|
||||||
|
intervalRef.current && clearInterval(intervalRef.current);
|
||||||
|
localStorage.setItem(LAST_LEVEL_STORAGE_KEY, '1');
|
||||||
|
localStorage.setItem(LAST_SCORE_STORAGE_KEY, '0');
|
||||||
|
localStorage.setItem(LAST_TIME_STORAGE_KEY, '0');
|
||||||
|
}
|
||||||
|
}, [finished]);
|
||||||
|
// 更新使用时间
|
||||||
|
useEffect(() => {
|
||||||
|
if (startTime && now) setUsedTime(now - startTime);
|
||||||
|
}, [now]);
|
||||||
|
// 计时器
|
||||||
|
const startTimer = (restart?: boolean) => {
|
||||||
|
setStartTime(Date.now() - (restart ? 0 : initTime));
|
||||||
|
setNow(Date.now());
|
||||||
|
intervalRef.current && clearInterval(intervalRef.current);
|
||||||
|
intervalRef.current = setInterval(() => {
|
||||||
|
setNow(Date.now());
|
||||||
|
}, 10);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="game">
|
<div className="game">
|
||||||
|
@ -456,30 +477,27 @@ const Game: FC<{
|
||||||
<button className="flex-grow" onClick={wash}>
|
<button className="flex-grow" onClick={wash}>
|
||||||
洗牌
|
洗牌
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button className="flex-grow" onClick={levelUp}>
|
||||||
className="flex-grow"
|
|
||||||
onClick={() => {
|
|
||||||
// 跳关扣关卡对应数值的分
|
|
||||||
setScore(score - level);
|
|
||||||
levelUp();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
下一关
|
下一关
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="level">
|
<div className="level">
|
||||||
关卡{level}|剩余{scene.filter((i) => i.status === 0).length}
|
关卡{level}/{maxLevel} 剩余
|
||||||
|得分{score}
|
{scene.filter((i) => i.status === 0).length}
|
||||||
|
<br />
|
||||||
|
得分{score}
|
||||||
|
<br />
|
||||||
|
用时{(usedTime / 1000).toFixed(3)}秒
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/*提示弹窗*/}
|
{/*提示弹窗*/}
|
||||||
{finished && (
|
{finished && (
|
||||||
<div className="modal">
|
<div className="modal">
|
||||||
<h1>{tipText}</h1>
|
<h1>{tipText}</h1>
|
||||||
|
<h1>得分{score}</h1>
|
||||||
|
<h1>用时{(usedTime / 1000).toFixed(3)}秒</h1>
|
||||||
<button onClick={restart}>再来一次</button>
|
<button onClick={restart}>再来一次</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/*bgm*/}
|
{/*bgm*/}
|
||||||
{theme.bgm && (
|
{theme.bgm && (
|
||||||
<button className="bgm-button" onClick={() => setBgmOn(!bgmOn)}>
|
<button className="bgm-button" onClick={() => setBgmOn(!bgmOn)}>
|
||||||
|
@ -487,7 +505,6 @@ const Game: FC<{
|
||||||
<audio ref={bgmRef} loop src={theme.bgm} />
|
<audio ref={bgmRef} loop src={theme.bgm} />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/*音效*/}
|
{/*音效*/}
|
||||||
{theme.sounds.map((sound) => (
|
{theme.sounds.map((sound) => (
|
||||||
<audio
|
<audio
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
.info {
|
.info {
|
||||||
left: 8px;
|
left: 8px;
|
||||||
bottom: 8px;
|
bottom: 8px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
@ -22,6 +25,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&.open {
|
&.open {
|
||||||
|
height: 120px;
|
||||||
|
|
||||||
@media screen and (max-width: 500px) {
|
@media screen and (max-width: 500px) {
|
||||||
width: calc(100% - 70px);
|
width: calc(100% - 70px);
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ body {
|
||||||
#root {
|
#root {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 500px;
|
max-width: 450px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -23,6 +23,7 @@ export interface Theme<SoundNames> {
|
||||||
backgroundBlur?: boolean;
|
backgroundBlur?: boolean;
|
||||||
dark?: boolean;
|
dark?: boolean;
|
||||||
pure?: boolean;
|
pure?: boolean;
|
||||||
|
maxLevel?: number;
|
||||||
icons: Icon<SoundNames>[];
|
icons: Icon<SoundNames>[];
|
||||||
sounds: Sound<SoundNames>[];
|
sounds: Sound<SoundNames>[];
|
||||||
operateSoundMap?: Record<Operation, SoundNames>;
|
operateSoundMap?: Record<Operation, SoundNames>;
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { getDefaultTheme } from './themes/default';
|
||||||
|
|
||||||
export const LAST_LEVEL_STORAGE_KEY = 'lastLevel';
|
export const LAST_LEVEL_STORAGE_KEY = 'lastLevel';
|
||||||
export const LAST_SCORE_STORAGE_KEY = 'lastScore';
|
export const LAST_SCORE_STORAGE_KEY = 'lastScore';
|
||||||
|
export const LAST_TIME_STORAGE_KEY = 'lastTime';
|
||||||
export const LAST_UPLOAD_TIME_STORAGE_KEY = 'lastUploadTime';
|
export const LAST_UPLOAD_TIME_STORAGE_KEY = 'lastUploadTime';
|
||||||
export const DEFAULT_BGM_STORAGE_KEY = 'defaultBgm';
|
export const DEFAULT_BGM_STORAGE_KEY = 'defaultBgm';
|
||||||
export const DEFAULT_TRIPLE_SOUND_STORAGE_KEY = 'defaultTripleSound';
|
export const DEFAULT_TRIPLE_SOUND_STORAGE_KEY = 'defaultTripleSound';
|
||||||
|
|
Loading…
Reference in New Issue
Block a user