feat: 主题切换

This commit is contained in:
streakingman 2022-09-19 03:02:32 +08:00
parent 6906dec65e
commit 968f9d6c39
21 changed files with 173 additions and 36 deletions

View File

@ -1,6 +1,6 @@
# 能够解出来的 "羊了个羊" 小游戏 Demo # 能够解出来的 "羊了个羊" 小游戏 Demo
![qrcode.png](public/qrcode.png) ![qrcode.png](qrcode.png)
坑爹的小游戏本来玩法挺有意思的非得恶心人根本无解99.99%无解),气的我自己写了个 demo 坑爹的小游戏本来玩法挺有意思的非得恶心人根本无解99.99%无解),气的我自己写了个 demo
扫码或:<a href="https://solvable-sheep-game.streakingman.com/" target="_blank">pc 浏览器体验</a> 扫码或:<a href="https://solvable-sheep-game.streakingman.com/" target="_blank">pc 浏览器体验</a>
@ -14,22 +14,24 @@
开心就好 😄 开心就好 😄
![preview.png](public/preview.png) ![preview.png](preview.png)
## Contribution ## Contribution
vite+react 实现,欢迎 star、issue、pr、fork尽量标注原仓库地址 vite+react 实现,欢迎 star、issue、pr、fork尽量标注原仓库地址
切换主题参考 `src/themes` 下的代码,欢迎整活
## Todo List ## Todo List
- [x] 基础操作 - [x] 基础操作
- [x] 关卡生成 - [x] 关卡生成
- [ ] UI/UX 优化 - [ ] UI/UX 优化
- [ ] 多主题 - [x] 多主题
- [ ] 计时 - [ ] 计时
- [ ] 性能优化 - [ ] 性能优化
- [ ] BGM/音效 - [x] BGM/音效
- [ ] 点击时的缓冲队列,优化交互动画效果 - [ ] ~~点击时的缓冲队列,优化交互动画效果~~
- [ ] 该游戏似乎涉嫌抄袭,考证后补充来源说明 - [ ] 该游戏似乎涉嫌抄袭,考证后补充来源说明
## License ## License

View File

Before

Width:  |  Height:  |  Size: 245 KiB

After

Width:  |  Height:  |  Size: 245 KiB

View File

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 65 KiB

View File

@ -50,6 +50,12 @@
transition: 0.3s; transition: 0.3s;
} }
.symbol-inner img {
width: 100%;
height: 100%;
object-fit: contain;
}
.queue-container { .queue-container {
border-radius: 8px; border-radius: 8px;
width: 100%; width: 100%;

View File

@ -1,4 +1,5 @@
import React, { import React, {
ChangeEventHandler,
FC, FC,
MouseEventHandler, MouseEventHandler,
useEffect, useEffect,
@ -9,8 +10,11 @@ import React, {
import './App.css'; import './App.css';
import { GithubIcon } from './GithubIcon'; import { GithubIcon } from './GithubIcon';
import { randomString, waitTimeout } from './utils'; import { randomString, waitTimeout } from './utils';
import { DefaultSoundNames, defaultTheme } from './themes/default';
import { Icon, Theme } from './themes/interface';
import { fishermanTheme } from './themes/fisherman';
const icons = [`🎨`, `🌈`, `⚙️`, `💻`, `📚`, `🐯`, `🐤`, `🐼`, `🐏`, `🍀`]; const themes = [defaultTheme, fishermanTheme];
// 最大关卡 // 最大关卡
const maxLevel = 50; const maxLevel = 50;
@ -21,13 +25,13 @@ interface MySymbol {
isCover: boolean; isCover: boolean;
x: number; x: number;
y: number; y: number;
icon: string; icon: Icon;
} }
type Scene = MySymbol[]; type Scene = MySymbol[];
// 8*8网格 4*4->8*8 // 8*8网格 4*4->8*8
const makeScene: (level: number) => Scene = (level) => { const makeScene: (level: number, icons: Icon[]) => Scene = (level, icons) => {
const curLevel = Math.min(maxLevel, level); const curLevel = Math.min(maxLevel, level);
const iconPool = icons.slice(0, 2 * curLevel); const iconPool = icons.slice(0, 2 * curLevel);
const offsetPool = [0, 25, -25, 50, -50].slice(0, 1 + curLevel); const offsetPool = [0, 25, -25, 50, -50].slice(0, 1 + curLevel);
@ -42,7 +46,7 @@ const makeScene: (level: number) => Scene = (level) => {
[0, 8], [0, 8],
][Math.min(4, curLevel - 1)]; ][Math.min(4, curLevel - 1)];
const randomSet = (icon: string) => { const randomSet = (icon: Icon) => {
const offset = const offset =
offsetPool[Math.floor(offsetPool.length * Math.random())]; offsetPool[Math.floor(offsetPool.length * Math.random())];
const row = const row =
@ -127,14 +131,20 @@ const Symbol: FC<SymbolProps> = ({ x, y, icon, isCover, status, onClick }) => {
className="symbol-inner" className="symbol-inner"
style={{ backgroundColor: isCover ? '#999' : 'white' }} style={{ backgroundColor: isCover ? '#999' : 'white' }}
> >
<i>{icon}</i> {typeof icon.content === 'string' ? (
<i>{icon.content}</i>
) : (
icon.content
)}
</div> </div>
</div> </div>
); );
}; };
const App: FC = () => { const App: FC = () => {
const [scene, setScene] = useState<Scene>(makeScene(1)); const [curTheme, setCurTheme] =
useState<Theme<DefaultSoundNames>>(defaultTheme);
const [scene, setScene] = useState<Scene>(makeScene(1, curTheme.icons));
const [level, setLevel] = useState<number>(1); const [level, setLevel] = useState<number>(1);
const [queue, setQueue] = useState<MySymbol[]>([]); const [queue, setQueue] = useState<MySymbol[]>([]);
const [sortedQueue, setSortedQueue] = useState< const [sortedQueue, setSortedQueue] = useState<
@ -143,13 +153,14 @@ const App: FC = () => {
const [finished, setFinished] = useState<boolean>(false); const [finished, setFinished] = useState<boolean>(false);
const [tipText, setTipText] = useState<string>(''); const [tipText, setTipText] = useState<string>('');
const [animating, setAnimating] = useState<boolean>(false); const [animating, setAnimating] = useState<boolean>(false);
// 音效
const soundRefMap = useRef<Record<string, HTMLAudioElement>>({});
// 第一次点击时播放bgm
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);
const tapSoundRef = useRef<HTMLAudioElement>(null);
const tripleSoundRef = useRef<HTMLAudioElement>(null);
const levelUpSoundRef = useRef<HTMLAudioElement>(null);
useEffect(() => { useEffect(() => {
if (bgmOn) { if (bgmOn) {
bgmRef.current?.play(); bgmRef.current?.play();
@ -158,14 +169,19 @@ const App: FC = () => {
} }
}, [bgmOn]); }, [bgmOn]);
// 主题切换
useEffect(() => {
restart();
}, [curTheme]);
// 队列区排序 // 队列区排序
useEffect(() => { useEffect(() => {
const cache: Record<string, MySymbol[]> = {}; const cache: Record<string, MySymbol[]> = {};
for (const symbol of queue) { for (const symbol of queue) {
if (cache[symbol.icon]) { if (cache[symbol.icon.name]) {
cache[symbol.icon].push(symbol); cache[symbol.icon.name].push(symbol);
} else { } else {
cache[symbol.icon] = [symbol]; cache[symbol.icon.name] = [symbol];
} }
} }
const temp = []; const temp = [];
@ -181,12 +197,6 @@ const App: FC = () => {
setSortedQueue(updateSortedQueue); setSortedQueue(updateSortedQueue);
}, [queue]); }, [queue]);
const test = () => {
const level = Math.ceil(maxLevel * Math.random());
setLevel(level);
checkCover(makeScene(level));
};
// 初始化覆盖状态 // 初始化覆盖状态
useEffect(() => { useEffect(() => {
checkCover(scene); checkCover(scene);
@ -264,7 +274,7 @@ const App: FC = () => {
setFinished(false); setFinished(false);
setLevel(level + 1); setLevel(level + 1);
setQueue([]); setQueue([]);
checkCover(makeScene(level + 1)); checkCover(makeScene(level + 1, curTheme.icons));
}; };
// 重开 // 重开
@ -272,7 +282,7 @@ const App: FC = () => {
setFinished(false); setFinished(false);
setLevel(1); setLevel(1);
setQueue([]); setQueue([]);
checkCover(makeScene(1)); checkCover(makeScene(1, curTheme.icons));
}; };
// 点击item // 点击item
@ -289,9 +299,10 @@ const App: FC = () => {
if (symbol.isCover || symbol.status !== 0) return; if (symbol.isCover || symbol.status !== 0) return;
symbol.status = 1; symbol.status = 1;
if (tapSoundRef.current) { // 点击音效
tapSoundRef.current.currentTime = 0; if (soundRefMap.current) {
tapSoundRef.current.play(); soundRefMap.current[symbol.icon.clickSound].currentTime = 0;
soundRefMap.current[symbol.icon.clickSound].play();
} }
let updateQueue = queue.slice(); let updateQueue = queue.slice();
@ -312,9 +323,12 @@ const App: FC = () => {
const find = updateScene.find((i) => i.id === sb.id); const find = updateScene.find((i) => i.id === sb.id);
if (find) { if (find) {
find.status = 2; find.status = 2;
if (tripleSoundRef.current) { // 三连音效
tripleSoundRef.current.currentTime = 0.55; if (soundRefMap.current) {
tripleSoundRef.current.play(); soundRefMap.current[
symbol.icon.tripleSound
].currentTime = 0;
soundRefMap.current[symbol.icon.tripleSound].play();
} }
} }
} }
@ -336,7 +350,7 @@ const App: FC = () => {
// 升级 // 升级
setLevel(level + 1); setLevel(level + 1);
setQueue([]); setQueue([]);
checkCover(makeScene(level + 1)); checkCover(makeScene(level + 1, curTheme.icons));
} else { } else {
setQueue(updateQueue); setQueue(updateQueue);
checkCover(updateScene); checkCover(updateScene);
@ -351,7 +365,21 @@ const App: FC = () => {
<h6> <h6>
<GithubIcon /> <GithubIcon />
</h6> </h6>
<h3>Level: {level} </h3> <h3 className="flex-container flex-center">
:
<select
onChange={(e) =>
setCurTheme(themes[Number(e.target.value)])
}
>
{themes.map((t, idx) => (
<option key={t.name} value={idx}>
{t.name}
</option>
))}
</select>
Level: {level}
</h3>
<div className="app"> <div className="app">
<div className="scene-container"> <div className="scene-container">
@ -404,13 +432,22 @@ const App: FC = () => {
</div> </div>
)} )}
{/*bgm*/}
<button className="bgm-button" onClick={() => setBgmOn(!bgmOn)}> <button className="bgm-button" onClick={() => setBgmOn(!bgmOn)}>
{bgmOn ? '🔊' : '🔈'} {bgmOn ? '🔊' : '🔈'}
<audio ref={bgmRef} loop src="/sound-disco.mp3" /> <audio ref={bgmRef} loop src="/sound-disco.mp3" />
</button> </button>
<audio ref={tapSoundRef} src="/sound-button-click.mp3" /> {/*音效*/}
<audio ref={tripleSoundRef} src="/sound-triple.mp3" /> {curTheme.sounds.map((sound) => (
<audio
key={sound.name}
ref={(ref) => {
if (ref) soundRefMap.current[sound.name] = ref;
}}
src={sound.src}
/>
))}
</> </>
); );
}; };

View File

@ -33,6 +33,12 @@ h1 {
line-height: 1.1; line-height: 1.1;
} }
select {
border: 2px solid gray;
border-radius: 4px;
padding: 4px 8px;
}
button { button {
border-radius: 8px; border-radius: 8px;
border: 1px solid transparent; border: 1px solid transparent;

View File

@ -0,0 +1,40 @@
import { Theme } from '../interface';
const icons = <const>[
`🎨`,
`🌈`,
`⚙️`,
`💻`,
`📚`,
`🐯`,
`🐤`,
`🐼`,
`🐏`,
`🍀`,
];
export type DefaultSoundNames = 'button-click' | 'triple';
import soundButtonClickUrl from './sounds/sound-button-click.mp3';
import soundTripleUrl from './sounds/sound-triple.mp3';
export const defaultSounds: Theme<DefaultSoundNames>['sounds'] = [
{
name: 'button-click',
src: soundButtonClickUrl,
},
{
name: 'triple',
src: soundTripleUrl,
},
];
export const defaultTheme: Theme<DefaultSoundNames> = {
name: '默认',
icons: icons.map((icon) => ({
name: icon,
content: icon,
clickSound: 'button-click',
tripleSound: 'triple',
})),
sounds: defaultSounds,
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@ -0,0 +1,27 @@
// 钓鱼佬主题
import React from 'react';
import { Theme } from '../interface';
import { DefaultSoundNames, defaultSounds } from '../default';
const imagesUrls = import.meta.glob('./images/*.png', {
import: 'default',
eager: true,
});
const fishes = Object.entries(imagesUrls).map(([key, value]) => ({
name: key.slice(9, -4),
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
content: <img src={value} alt="" />,
}));
export const fishermanTheme: Theme<DefaultSoundNames> = {
name: '钓鱼佬',
icons: fishes.map(({ name, content }) => ({
name,
content,
clickSound: 'button-click',
tripleSound: 'triple',
})),
sounds: defaultSounds,
};

19
src/themes/interface.ts Normal file
View File

@ -0,0 +1,19 @@
import { ReactNode } from 'react';
export interface Icon<T = string> {
name: string;
content: ReactNode;
clickSound: T;
tripleSound: T;
}
interface Sound<T = string> {
name: T;
src: string;
}
export interface Theme<SoundNames> {
name: string;
icons: Icon<SoundNames>[];
sounds: Sound<SoundNames>[];
}