react实现砸地鼠小游戏效果代码
代码语言:html
所属分类:游戏
代码描述:react实现砸地鼠小游戏效果代码
下面为部分代码预览,完整代码请点击下载或在bfwstudio webide中打开
<!DOCTYPE html> <html lang="en" > <head> <meta charset="UTF-8"> <link href="https://fonts.googleapis.com/css2?family=Fredoka+One&display=swap" rel="stylesheet"> <style> :root { --controls: hsl(38, 96%, calc((55 + var(--lightness, 0)) * 1%)); --controls-secondary: hsl(55, 100%, 50%); --controls-color: hsl(0, 0%, 100%); --sky: hsl(204, 80%, 80%); --grass: hsl(98, 40%, 50%); --dirt: hsl(35, 40%, 20%); } * { box-sizing: border-box; } body { min-height: 100vh; display: flex; align-items: center; justify-content: center; overflow: hidden; margin: 0; font-family: 'Fredoka One', cursive; background: var(--sky); } .moles { display: inline-grid; grid-template-rows: repeat(2, auto); grid-template-columns: repeat(3, auto); grid-gap: 0 2vmin; cursor: none; } .moles > *:nth-of-type(4), .moles > *:nth-of-type(5) { transform: translate(50%, -25%); } main { height: 100vh; width: 100vw; display: grid; place-items: center; background: linear-gradient(var(--sky) 0 44%, var(--grass) 44%); } button { --controls: hsl(38, 96%, calc((55 + var(--lightness, 0)) * 1%)); background: var(--controls); color: var(--controls-color); padding: 1rem 2rem; font-family: 'Fredoka One', cursive; font-weight: bold; font-size: 1.75rem; border-radius: 1rem; border: 4px var(--controls-color) solid; white-space: nowrap; cursor: pointer; } button:hover { --lightness: 5; } button:active { --lightness: -15; } .celebration { font-size: 4rem; line-height: 1; margin: 0; padding: 0; text-transform: uppercase; text-align: center; } .word { display: inline-block; white-space: nowrap; } .celebration .char { display: inline-block; color: hsl(calc((360 / var(--char-total)) * var(--char-index)), 70%, 65%); -webkit-animation: jump 0.35s calc(var(--char-index, 0) * -1s) infinite; animation: jump 0.35s calc(var(--char-index, 0) * -1s) infinite; } .countdown-number { font-size: 10rem; color: var(--dirt); -webkit-text-stroke: 0.25rem var(--controls-color); position: fixed; top: 50%; left: 50%; z-index: 12; margin: 0; padding: 0; transform: translate(-50%, -50%); display: 'none'; } @-webkit-keyframes jump { 50% { transform: translate(0, -25%); } } @keyframes jump { 50% { transform: translate(0, -25%); } } .icon-button { height: 48px; width: 48px; outline: transparent; background: none; border: 0; display: grid; place-items: center; padding: 0; margin: 0; } .mute-button { position: fixed; bottom: 0; right: 0; z-index: 2; } .mute-button:hover ~ .mallet, .end-button:hover ~ .mallet { display: none; } .end-button { position: fixed; top: 0; right: 0; z-index: 2; } .game-info { position: fixed; top: 1rem; left: 1rem; display: grid; grid-template-columns: repeat(2, auto); grid-template-rows: repeat(2, auto); align-items: center; grid-gap: 0.5rem 1rem; z-index: 2; background: var(--controls-color); border: 4px solid var(--controls); border-radius: 1rem; padding: 1rem; width: 190px; } .info-screen { z-index: 2; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); display: flex; align-items: center; justify-content: center; flex-direction: column; } .results { background: var(--controls-color); padding: 2rem; border: 4px solid var(--controls); border-radius: 1rem; } .info-screen > * + * { margin-top: 1rem; } .icon { fill: hsl(35, 50%, 28%); stroke-width: 20px; overflow: visible; height: 24px; width: 24px; } @media(min-width: 768px) { .end-button { top: 1rem; right: 1rem; } .mute-button { bottom: 1rem; right: 1rem; } .icon { height: 48px; width: 48px; } } .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border-width: 0; } .info__text { font-size: clamp(1rem, 5vmin, 2rem); line-height: 1; color: var(--dirt); margin: 0; } .boring-text { font-size: 2rem; text-align: center; } .title { -webkit-text-stroke: 0.1vmin var(--controls-color); font-size: 6rem; font-weight: bold; color: transparent; background: linear-gradient(40deg, var(--controls), var(--controls-secondary)); -webkit-background-clip: text; background-clip: text; text-align: center; display: inline-block; line-height: 0.75; margin: 0 0 4rem 0; padding: 0; transform: rotate(-15deg); } .title span { display: block; } .title span:nth-of-type(2) { transform: translate(0, -10%) rotate(15deg); color: var(--controls); } .hole { fill: hsl(0, 0%, 12%); } .hole__lip { fill: hsl(38, 20%, 50%); } .mole__feature { fill: hsl(0, 0%, 10%); } .mole__eyes--crossed { display: none; } .mole__mole { display: none; } .specs__lens { fill: hsla(198, 80%, calc((80 - (var(--shades, 0) * 75)) * 1%), calc(0.5 + (var(--shades, 0) * 0.5))); stroke: hsl(var(--accent), 25%, calc((30 - (var(--shades, 0) * 30)) * 1%)); } .cap__accent { fill: hsl(var(--accent, 10), 80%, 50%); } .cap__body { fill: hsl(0, 0%, 5%); } .specs__glare { fill: hsla(0, 0%, 100%, calc(0.5 + (var(--shades, 0) * 0.25))); } .specs__bridge { stroke: hsl(var(--accent), 25%, calc((30 - (var(--shades, 0) * 30)) * 1%)); } .mole__hole { width: 20vmin; height: 20vmin; position: relative; cursor: none; } .mole__hole * { cursor: none; } .mole__body { fill: hsl(var(--hue), calc((10 + (var(--golden, 0) * 40)) * 1%), calc(var(--lightness, 65) * 1%)); } .mole__white { fill: hsl(40, 80%, calc((98 - (var(--golden, 0) * 15)) * 1%)); } .mole__whiskers { stroke: hsl(40, calc((0 + (var(--golden, 0) * 35)) * 1%), calc((5 + (var(--golden, 0) * 40)) * 1%)); } .mole__shadow { fill: hsl(var(--hue), 16%, 43%); } .mole__nose { fill: hsl(calc(10 + (var(--golden, 0) * 30)), 90%, calc((88 - (var(--golden, 0) * 35)) * 1%)); } .mole { position: absolute; height: 100%; width: 100%; } .mole__whack { height: 100%; width: 100%; border: 0; opacity: 0; transform: translate(0, 0%); position: absolute; top: 0; left: 0; } .mole__points-holder { position: absolute; transform: rotate(calc(var(--angle, 0) * 1deg)); transform-origin: 50% 200%; pointer-events: none; position: fixed; z-index: 10; } .mole__points { font-size: clamp(2rem, 8vmin, 18rem); pointer-events: none; font-weight: bold; color: hsl(var(--accent, 0), 90%, 75%); margin: 0; transform: translate(-50%, -200%); -webkit-text-stroke: 0.1vmin hsl(var(--accent), 50%, 35%); } .mallet { height: 0px; width: 0px; background: green; pointer-events: none; position: fixed; top: calc(var(--y) * 1px); left: calc(var(--x) * 1px); z-index: 10; transform: translate(-50%, -50%); display: none; } .mallet img { position: absolute; bottom: 0; height: 18vmin; transform-origin: 75% 85%; pointer-events: none; } @media (hover: none) { .mallet img { display: none; } } .hiscore { text-transform: uppercase; position: fixed; top: 1rem; left: 1rem; z-index: 2; } </style> </head> <body > <div id="root"></div> <script type="module"> import React, { Fragment, useCallback, useEffect, useState, useRef } from 'https://cdn.skypack.dev/react'; import ReactDOM from 'https://cdn.skypack.dev/react-dom'; import confetti from 'https://cdn.skypack.dev/canvas-confetti'; import Splitting from 'https://cdn.skypack.dev/splitting'; import gsap from 'https://cdn.skypack.dev/gsap'; import T from 'https://cdn.skypack.dev/prop-types'; const malletSrc = '//repo.bfw.wiki/bfwrepo/image/6080b789c7e9f.png'; // Constants const constants = { TIME_LIMIT: 30000, MOLE_SCORE: 100, POINTS_MULTIPLIER: 0.9, TIME_MULTIPLIER: 1.2, MOLES: 5, REGULAR_SCORE: 100, GOLDEN_SCORE: 1000 }; // Custom Hooks const useAudio = (src, volume = 1) => { const [audio, setAudio] = useState(null); useEffect(() => { const AUDIO = new Audio(src); AUDIO.volume = volume; setAudio(AUDIO); }, [src]); return { play: () => audio.play(), pause: () => audio.pause(), stop: () => { audio.pause(); audio.currentTime = 0; } }; }; const usePersistentState = (key, initialValue) => { const [state, setState] = useState( window.localStorage.getItem(key) ? JSON.parse(window.localStorage.getItem(key)) : initialValue); useEffect(() => { window.localStorage.setItem(key, state); }, [key, state]); return [state, setState]; }; // Utils const generateMoles = () => new Array(constants.MOLES).fill().map(() => ({ speed: gsap.utils.random(0.5, 2), delay: gsap.utils.random(0.5, 5), points: constants.MOLE_SCORE })); // Components const CountDown = ({ fx, onComplete }) => { const count = useRef(null); const three = useRef(null); const two = useRef(null); const one = useRef(null); useEffect(() => { gsap.set([three.current, two.current, one.current], { display: 'none' }); count.current = gsap. timeline({ delay: 0.5, onComplete }). set(three.current, { display: 'block' }). fromTo( three.current, { scale: 1, rotate: gsap.utils.random(-30, 30) }, { scale: 0, rotate: gsap.utils.random(-30, 30), duration: 1, onStart: () => fx() }). set(two.current, { display: 'block' }). fromTo( two.current, { scale: 1, rotate: gsap.utils.random(-30, 30) }, { scale: 0, rotate: gsap.utils.random(-30, 30), duration: 1, onStart: () => fx() }). set(one.current, { display: 'block' }). fromTo( one.current, { scale: 1, rotate: gsap.utils.random(-30, 30) }, { scale: 0, rotate: gsap.utils.random(-30, 30), duration: 1, onStart: () => fx() }); return () => { if (count.current) count.current.kill(); }; }, []); return /*#__PURE__*/( React.createElement(Fragment, null, /*#__PURE__*/ React.createElement("h2", { ref: three, className: "countdown-number", style: { display: 'none' } }, "3"), /*#__PURE__*/ React.createElement("h2", { ref: two, className: "countdown-number", style: { display: 'none' } }, "2"), /*#__PURE__*/ React.createElement("h2", { ref: one, className: "countdown-number", style: { display: 'none' } }, "1"))); }; CountDown.propTypes = { fx: T.func.isRequired, onComplete: T.func.isRequired }; const FinishScreen = ({ newHigh, onRestart, onReset, result }) => /*#__PURE__*/ React.createElement("div", { className: "info-screen" }, /*#__PURE__*/ React.createElement("div", { className: "results" }, newHigh && /*#__PURE__*/ React.createElement(Fragment, null, /*#__PURE__*/ React.createElement("h2", { className: "celebration", dangerouslySetInnerHTML: { __html: Splitting.html({ content: `New High Score!` }) } }), /*#__PURE__*/ React.createElement("h2", { className: "celebration" }, result)), !newHigh && /*#__PURE__*/ React.createElement("h2", { className: "info__text boring-text" }, `You Scored ${result}`)), /*#__PURE__*/ React.createElement("button", { onClick: onRestart }, "Play Again"), /*#__PURE__*/ React.createElement("button", { onClick: onReset }, "Main Menu")); FinishScreen.propTypes = { newHigh: T.bool.isRequired, onRestart: T.func.isRequired, onReset: T.func.isRequired, result: T.number.isRequired }; const StartScreen = ({ onStart }) => /*#__PURE__*/ React.createElement("div", { className: "info-screen" }, /*#__PURE__*/ React.createElement("h1", { className: "title" }, /*#__PURE__*/ React.createElement("span", null, "Whac"), /*#__PURE__*/ React.createElement("span", null, "a"), /*#__PURE__*/ React.createElement("span", null, "Mole")), /*#__PURE__*/ React.createElement("button", { onClick: onStart }, "Start Game")); StartScreen.propTypes = { onStart: T.func.isRequired }; const Timer = ({ time, interval = 1000, onEnd }) => { const [internalTime, setInternalTime] = useState(time); const timerRef = useRef(time); const timeRef = useRef(time); useEffect(() => { if (internalTime === 0 && onEnd) { onEnd(); } }, [internalTime, onEnd]); useEffect(() => { timerRef.current = setInterval(() => { setInternalTime(timeRef.current -= interval); }, interval); return () => { clearInterval(timerRef.current); }; }, [interval]); return /*#__PURE__*/( React.createElement(Fragment, null, /*#__PURE__*/ React.createElement("svg", { className: "icon", viewBox: "0 0 512 512", width: "100", title: "clock" }, /*#__PURE__*/ React.createElement("path", { d: "M256,8C119,8,8,119,8,256S119,504,256,504,504,393,504,256,393,8,256,8Zm92.49,313h0l-20,25a16,16,0,0,1-22.49,2.5h0l-67-49.72a40,40,0,0,1-15-31.23V112a16,16,0,0,1,16-16h32a16,16,0,0,1,16,16V256l58,42.5A16,16,0,0,1,348.49,321Z" })), /*#__PURE__*/ React.createElement("span", { className: "info__text" }, `${internalTime / 1000}s`))); }; Timer.defaultProps = { interval: 1000 }; Timer.propTypes = { time: T.number.isRequired, interval: T.number, onEnd: T.func.isRequired }; // This is the centerpiece of the game. // It's the most complex component. But don't be scared of it! const Mole = ({ active = false, loading = false, onWhack, speed, delay, points, pointsMin = 10 }) => { const [whacked, setWhacked] = useState(false); const delayedRef = useRef(null); const pointsRef = useRef(points); const buttonRef = useRef(null); const capBody = useRef(null); const moleRef = useRef(null); const capPeak = useRef(null); const loadingRef = useRef(null); const noseRef = useRef(null); const moleContainerRef = useRef(null); const faceRef = useRef(null); const capRef = useRef(null); const specsRef = useRef(null); const bobRef = useRef(null); const eyesRef = useRef(null); const tummyRef = useRef(null); // Use a callback to cache the function and share it between effects. const setMole = useCallback( ( override, accent = 45, shades = 1, golden = 1, hue = 45, lightness = 65) => { // Give a 1% chance of getting the "Golden" Mole. if (Math.random() > 0.99 || override) { // Create the "Golden" Mole pointsRef.current = constants.GOLDEN_SCORE; // Set the specs and cap as displayed gsap.set([capRef.current, specsRef.current], { display: 'block' }); // Set specific colors and that the shades/golden are active gsap.set(moleContainerRef.current, { '--accent': accent, '--shades': shades, '--golden': golden, '--hue': hue, '--lightness': lightness }); } else { // Create a "Regular" Mole pointsRef.current = constants.REGULAR_SCORE; // Set whether Mole has a cap or specs gsap.set([capRef.current, specsRef.current], { display: () => Math.random() > 0.5 ? 'block' : 'none' }); // Set random colors for Mole. gsap.set(moleContainerRef.current, { '--accent': gsap.utils.random(0, 359), '--shades': Math.random() > 0.65 ? 1 : 0, '--golden': 0, '--hue': Math.random() > 0.5 ? gsap.utils.random(185, 215) : gsap.utils.random(30, 50), '--lightness': gsap.utils.random(45, 75) }); } }, []); // Use an effect to get the Mole moving useEffect(() => { // Set the Mole position and overlay button to underground gsap.set([moleRef.current, buttonRef.current], { yPercent: 100 }); // Show Mole gsap.set(moleRef.current, { display: 'block' }); // Create the bobbing timeline and store a ref so we can kill it on unmount. // Timeline behavior defined by props if (active) { // Set characteristics for the Mole. setMole(); bobRef.current = gsap.to([buttonRef.current, moleRef.current], { yPercent: 0, duration: speed, yoyo: true, repeat: -1, delay, repeatDelay: delay, onRepeat: () => { pointsRef.current = Math.floor( Math.max(pointsRef.current * constants.POINTS_MULTIPLIER, pointsMin)); } }); } // Cleanup the timeline on unmount return () => { if (bobRef.current) bobRef.current.kill(); }; }, [active, delay, pointsMin, speed, setMole]); // When a Mole is whacked, animate it underground // Swap out the Mole style, reset it, and speed up the bobbing timeline. useEffect(() => { if (whacked) { // Render something in the body bobRef.current.pause(); gsap.to([moleRef.current, buttonRef.current], { yPercent: 100, duration: 0.1, onComplete: () => { delayedRef.current = gsap.delayedCall(gsap.utils.random(1, 3), () => { setMole(); setWhacked(false); bobRef.current. restart(). timeScale(bobRef.current.timeScale() * constants.TIME_MULTIPLIER); }); } }); } // If the delayed restart isn't started and we unmount, it will need cleaning up. return () => { if (delayedRef.current) delayedRef.current.kill(); }; }, [whacked, setMole]); // If a Mole is set to loading, play the loading animation version useEffect(() => { if (loading) { setMole(true, 10, 1, 0, 200, 70); loadingRef.current = gsap. timeline({ repeat: -1, repeatDelay: 1 }) // Shooting up! .to(moleRef.current, { yPercent: 5, ease: 'back.out(1)' }). to( capRef.current, { yPercent: -15, duration: 0.1, repeat: 1, yoyo: true }, '>-0.2') // Side to side .to([capBody.current, faceRef.current], { xPercent: 10 }). to( capPeak.current, { xPercent: -10 }, '<'). to( [eyesRef.current, specsRef.current, tummyRef.current], { xPercent: 8 }, '<'). to( noseRef.current, { xPercent: 25 }, '<'). to([faceRef.current, capBody.current], { xPercent: -10, duration: 0.75 }). to( capPeak.current, { xPercent: 28, duration: 0.5 }, '<'). to( [eyesRef.current, specsRef.current, tummyRef.current], { xPercent: -8, duration: 0.75 }, '<'). to( noseRef.current, { xPercent: -25, duration: 0.75 }, '<'). to(moleRef.current, { yPercent: 100, delay: 0.2, ease: 'power4.in' }). to( capRef.current, { yPercent: -15, duration: 0.2, ease: 'power4.in' }, '<+0.05'); } return () => { gsap.set( [ capRef.current, capPeak.current, capBody.current, faceRef.current, noseRef.current, eyesRef.current, specsRef.current, tummyRef.current], { xPercent: 0, yPercent: 0 }); if (loadingRef.current) loadingRef.current.kill(); }; }, [loading]); // To render the score, we don't need React elements. // We can render them straight to the DOM and remove them once they've animated. // Alternatively, we could use a React DOM Portal. However, our element has // a short lifespan and doesn't update, etc. const renderScore = (x, y) => { const SCORE_HOLDER = document.createElement('div'); SCORE_HOLDER.className = 'mole__points-holder'; const SCORE = document.createElement('div'); SCORE.className = 'mole__points'; SCORE.innerText = pointsRef.current; SCORE_HOLDER.appendChild(SCORE); document.body.appendChild(SCORE_HOLDER); gsap.set(SCORE_HOLDER, { '--angle': gsap.utils.random(-35, 35), '--accent': gsap.utils.random(0, 359) }); gsap. timeline({ onComplete: () => SCORE_HOLDER.remove() }).........完整代码请登录后点击上方下载按钮下载查看
网友评论0