├── .gitignore ├── README.md ├── craco.config.js ├── package.json ├── public ├── card-v1.png ├── card-v2.png ├── icon.png ├── icon192.png ├── icon@2x.png ├── index.html ├── manifest.json └── robots.txt ├── src ├── App.js ├── data.js ├── index.css ├── index.js ├── logo.svg ├── serviceWorker.js ├── setupTests.js └── store.js ├── tailwind.config.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | .now -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Original project: https://github.com/HackPlan/mafan -------------------------------------------------------------------------------- /craco.config.js: -------------------------------------------------------------------------------- 1 | // Override the CRA config 2 | // Doc: https://github.com/sharegate/craco/blob/master/packages/craco/README.md#configuration-overview 3 | module.exports = function ({ env }) { 4 | return { 5 | eslint: { 6 | configure: { 7 | rules: { 8 | }, 9 | }, 10 | }, 11 | babel: { 12 | plugins: [ 13 | '@babel/plugin-proposal-optional-chaining', 14 | ["@babel/plugin-proposal-decorators", { "legacy": true }], 15 | ], 16 | }, 17 | style: { 18 | postcss: { 19 | mode: 'extends', 20 | plugins: [ 21 | require('postcss-import'), 22 | require('tailwindcss')('./tailwind.config.js'), 23 | require('postcss-nested'), 24 | require('autoprefixer'), 25 | require('postcss-preset-env')({ stage: 1 }), 26 | ], 27 | }, 28 | }, 29 | webpack: { 30 | plugins: [] 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mafan2020", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@babel/plugin-proposal-decorators": "^7.8.3", 7 | "@craco/craco": "^5.6.4", 8 | "@fullhuman/postcss-purgecss": "^2.1.0", 9 | "@hackplan/uui": "^0.2.11", 10 | "@testing-library/jest-dom": "^4.2.4", 11 | "@testing-library/react": "^9.3.2", 12 | "@testing-library/user-event": "^7.1.2", 13 | "autoprefixer": "^9.7.5", 14 | "mobx": "^5.15.4", 15 | "mobx-persist": "^0.4.1", 16 | "mobx-react-lite": "^1.5.2", 17 | "postcss-import": "^12.0.1", 18 | "postcss-nested": "^4.2.1", 19 | "postcss-preset-env": "^6.7.0", 20 | "react": "^16.13.1", 21 | "react-dom": "^16.13.1", 22 | "react-scripts": "3.4.1", 23 | "tailwindcss": "^1.2.0" 24 | }, 25 | "scripts": { 26 | "start": "craco start", 27 | "build": "craco build", 28 | "test": "craco test" 29 | }, 30 | "eslintConfig": { 31 | "extends": "react-app" 32 | }, 33 | "browserslist": { 34 | "production": [ 35 | ">0.2%", 36 | "not dead", 37 | "not op_mini all" 38 | ], 39 | "development": [ 40 | "last 1 chrome version", 41 | "last 1 firefox version", 42 | "last 1 safari version" 43 | ] 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /public/card-v1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/untsop/mafan-2020/ee81ca1d47406d5500bded12fe8e003f88c995a7/public/card-v1.png -------------------------------------------------------------------------------- /public/card-v2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/untsop/mafan-2020/ee81ca1d47406d5500bded12fe8e003f88c995a7/public/card-v2.png -------------------------------------------------------------------------------- /public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/untsop/mafan-2020/ee81ca1d47406d5500bded12fe8e003f88c995a7/public/icon.png -------------------------------------------------------------------------------- /public/icon192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/untsop/mafan-2020/ee81ca1d47406d5500bded12fe8e003f88c995a7/public/icon192.png -------------------------------------------------------------------------------- /public/icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/untsop/mafan-2020/ee81ca1d47406d5500bded12fe8e003f88c995a7/public/icon@2x.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 17 | 18 | 22 | 23 | 32 | 麻烦 33 | 34 | 35 | 36 |
37 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "麻烦", 3 | "name": "麻烦", 4 | "icons": [ 5 | { 6 | "src": "icon192.png", 7 | "type": "image/png", 8 | "sizes": "192x192" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#ffffff", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useContext, useReducer, useEffect, useState } from 'react'; 2 | import { observer } from "mobx-react-lite"; 3 | import { gameContext, problems, levels } from "./store"; 4 | import { Button, Tag } from '@hackplan/uui'; 5 | import logo from './logo.svg'; 6 | import './index.css'; 7 | 8 | const App = observer(() => { 9 | const gameStore = useContext(gameContext); 10 | const level = gameStore.useLevel() 11 | const goods = gameStore.useGoods() 12 | const defaultStates = Object.fromEntries(problems.map((p) => [p.title, 0])) 13 | 14 | const [shared, setShared] = useState(false) 15 | const counterReducer = (state, action) => { 16 | switch (action.type) { 17 | case 'increment': 18 | return { ...state, [action.title]: state[action.title] + 1 }; 19 | case 'debug': 20 | return { ...state, [action.title]: state[action.title] + 10 }; 21 | case 'reset': 22 | return { ...state, [action.title]: 0 }; 23 | default: 24 | throw new Error(); 25 | } 26 | } 27 | 28 | const [problemsStates, dispatch] = useReducer(counterReducer, defaultStates) 29 | 30 | useEffect(() => { 31 | for (const title in problemsStates) { 32 | if (problemsStates[title] > 0) { 33 | const problem = problems.find((p)=> p.title === title) 34 | const targetTime = problem.time * 2 35 | problem.percent = (problemsStates[problem.title] / targetTime).toFixed(4); 36 | problem.percentage = Math.min((Math.floor((problem.percent || 0) * 10000) / 100), 100).toFixed(1) + "%" 37 | if (problemsStates[title] >= targetTime) { 38 | resetProblemTimer(problem) 39 | gameStore.solve(problem) 40 | } 41 | } 42 | } 43 | }, [problemsStates]); 44 | 45 | const resetProblemTimer = (problem) => { 46 | clearInterval(problem.counting); 47 | problem.percent = 0 48 | problem.percentage = 0 49 | dispatch({ 50 | title: problem.title, 51 | type: 'reset' 52 | }) 53 | } 54 | 55 | const shareOnTwitter = () => { 56 | setShared(true) 57 | window.open("https://twitter.com/intent/tweet?tw_p=tweetbutton&text=我正在玩麻烦这个游戏:https://mafan.qqsun.xyz/ by @QQSun", "_blank"); 58 | } 59 | 60 | const notSolvable = (problem) => { 61 | if (problem.take && !problem.take.every((item) => { 62 | if (item.name === '你拥有的一切') { 63 | return true 64 | } 65 | const stuff = goods.find((g) => g.name === item.name) 66 | if (stuff && (stuff.number || 1) >= (item.number || 1)) { 67 | return true 68 | } 69 | return false 70 | })){ 71 | return true 72 | } 73 | return false 74 | } 75 | 76 | const fixProblem = (problem) => { 77 | if (problemsStates[problem.title] > 0) { 78 | return 79 | } 80 | resetProblemTimer(problem) 81 | 82 | problem.counting = setInterval(() => { 83 | if (false) { 84 | dispatch({ 85 | title: problem.title, 86 | type: 'debug' 87 | }) 88 | } else{ 89 | dispatch({ 90 | title: problem.title, 91 | type: 'increment' 92 | }) 93 | } 94 | }, 50) 95 | } 96 | 97 | return
98 | 99 | QQ Coin 100 |
101 | 你是 {levels.find((l) => l.level === (level || 0) )['name']} 102 |
103 |
104 |

你拥有

105 | {goods.map((stuff) => { 106 | return 107 | {stuff.name} 108 | {stuff.number && × {stuff.number}} 109 | 110 | })} 111 | {goods.length === 0 &&
一无所有
} 112 |
113 | 114 |
115 |

你的麻烦

116 | {problems.filter((problem) => { 117 | if (!problem.level.includes(level)) { 118 | return false 119 | } 120 | if (problem.dismiss && problem.dismiss.some((item) => goods.some((g) => g.name === item) )) { 121 | return false 122 | } 123 | if (problem.require && !problem.require.some((item) => goods.some((g) => g.name === item))) { 124 | return false 125 | } 126 | return true 127 | }).sort((a, b) => (b.growth ? 1 : 0) - (a.growth ? 1 : 0)).map((problem) => { 128 | return
129 |
130 |
131 |
{problem.title}
132 | {problem.take && problem.take.map((item) => - {item.name} {item.number && × {item.number}} )} 133 | {problem.gain && problem.gain.map((item) => + {item.name} {item.number && × {item.number}} )} 134 |
135 |
136 | 137 |
138 |
139 |
140 |
141 |
142 |
143 | })} 144 | 145 | {!shared &&
146 |
147 |
148 |
麻烦分享到 Twitter
149 |
150 |
151 | 152 |
153 |
154 |
} 155 |
156 | 157 | 158 | 159 |
160 | source code 161 |
162 | 163 |
164 | }) 165 | 166 | export default App; -------------------------------------------------------------------------------- /src/data.js: -------------------------------------------------------------------------------- 1 | export const mafan = { 2 | levels: [ 3 | { 4 | level: 0, 5 | name: "虚无" 6 | }, 7 | { 8 | level: 1, 9 | name: "婴儿" 10 | }, 11 | { 12 | level: 2, 13 | name: "小孩儿" 14 | }, 15 | { 16 | level: 3, 17 | name: "青年" 18 | }, 19 | { 20 | level: 4, 21 | name: "中年" 22 | }, 23 | { 24 | level: 5, 25 | name: "老年" 26 | }, 27 | { 28 | level: 6, 29 | name: "幽灵" 30 | }, 31 | ], 32 | problems: [ 33 | { 34 | level: [0], 35 | title: "什么都没有", 36 | dismiss: ["希望"], 37 | gain: [ 38 | { 39 | name: "希望" 40 | } 41 | ], 42 | time: 30 43 | }, 44 | { 45 | level: [0], 46 | title: "你没有生命", 47 | require: ["希望"], 48 | dismiss: ["生命"], 49 | gain: [ 50 | { 51 | name: "生命" 52 | } 53 | ], 54 | growth: true, 55 | time: 30 56 | }, 57 | { 58 | level: [1, 2, 3, 4], 59 | title: "你需要学习", 60 | gain: [ 61 | { 62 | name: "知识" 63 | } 64 | ], 65 | time: 60 66 | }, 67 | { 68 | level: [1, 2, 3, 4], 69 | title: "你需要玩耍", 70 | gain: [ 71 | { 72 | name: "记忆" 73 | } 74 | ], 75 | time: 30 76 | }, 77 | { 78 | level: [1], 79 | title: "你是一个婴儿", 80 | take: [ 81 | { 82 | name: "记忆", 83 | number: 5 84 | } 85 | ], 86 | growth: true, 87 | time: 60 88 | }, 89 | { 90 | level: [2], 91 | title: "你是一个小孩儿", 92 | take: [ 93 | { 94 | name: "知识", 95 | number: 5 96 | }, 97 | { 98 | name: "经历", 99 | number: 3 100 | } 101 | ], 102 | growth: true, 103 | time: 60 104 | }, 105 | { 106 | level: [2, 3, 4], 107 | title: "你需要朋友", 108 | gain: [ 109 | { 110 | name: "朋友" 111 | } 112 | ], 113 | time: 20 114 | }, 115 | { 116 | level: [2, 3, 4], 117 | title: "你厌倦了朋友", 118 | require: ["朋友"], 119 | take: [ 120 | { 121 | name: "朋友" 122 | } 123 | ], 124 | gain: [ 125 | { 126 | name: "经历" 127 | } 128 | ], 129 | time: 10 130 | }, 131 | { 132 | level: [3], 133 | title: "你是一个年轻人", 134 | take: [ 135 | { 136 | name: "经历", 137 | number: 5 138 | }, 139 | { 140 | name: "金钱", 141 | number: 5 142 | }, 143 | { 144 | name: "破碎的心", 145 | number: 2 146 | } 147 | ], 148 | growth: true, 149 | time: 60 150 | }, 151 | { 152 | level: [3, 4], 153 | title: "你需要爱人", 154 | dismiss: ["爱人", 155 | "家庭"], 156 | gain: [ 157 | { 158 | name: "爱人" 159 | } 160 | ], 161 | time: 40 162 | }, 163 | { 164 | level: [3, 4], 165 | title: "爱人离开了你", 166 | require: ["爱人"], 167 | take: [ 168 | { 169 | name: "爱人" 170 | } 171 | ], 172 | gain: [ 173 | { 174 | name: "破碎的心" 175 | } 176 | ], 177 | time: 20 178 | }, 179 | { 180 | level: [3, 4], 181 | title: "你需要工作", 182 | gain: [ 183 | { 184 | name: "压力" 185 | }, 186 | { 187 | name: "金钱" 188 | } 189 | ], 190 | time: 30 191 | }, 192 | { 193 | level: [3, 4, 5], 194 | title: "你需要放松", 195 | require: ["压力"], 196 | take: [ 197 | { 198 | name: "压力" 199 | } 200 | ], 201 | time: 30 202 | }, 203 | { 204 | level: [4], 205 | title: "你是一个中年人", 206 | take: [ 207 | { 208 | name: "经历", 209 | number: 5 210 | }, 211 | { 212 | name: "东西", 213 | number: 5 214 | }, 215 | { 216 | name: "朋友", 217 | number: 5 218 | } 219 | ], 220 | growth: true, 221 | time: 60 222 | }, 223 | { 224 | level: [4], 225 | title: "你需要结婚成家", 226 | require: ["爱人"], 227 | dismiss: ["家庭"], 228 | gain: [ 229 | { 230 | name: "家庭" 231 | } 232 | ], 233 | time: 60 234 | }, 235 | { 236 | level: [3, 4], 237 | title: "你需要换一个更好的工作", 238 | take: [ 239 | { 240 | name: "知识", 241 | number: 2 242 | } 243 | ], 244 | gain: [ 245 | { 246 | name: "经历", 247 | number: 2 248 | }, 249 | { 250 | name: "金钱", 251 | number: 3 252 | } 253 | ], 254 | time: 60 255 | }, 256 | { 257 | level: [3, 4], 258 | title: "你需要买更多东西", 259 | take: [ 260 | { 261 | name: "金钱" 262 | } 263 | ], 264 | gain: [ 265 | { 266 | name: "东西" 267 | } 268 | ], 269 | time: 20 270 | }, 271 | { 272 | level: [5], 273 | title: "你疾病缠身", 274 | take: [ 275 | { 276 | name: "金钱", 277 | number: 3 278 | } 279 | ], 280 | time: 60 281 | }, 282 | { 283 | level: [5], 284 | title: "你感到孤独", 285 | take: [ 286 | { 287 | name: "朋友", 288 | number: 2 289 | } 290 | ], 291 | time: 60 292 | }, 293 | { 294 | level: [5], 295 | title: "你感到无聊", 296 | take: [ 297 | { 298 | name: "记忆", 299 | number: 2 300 | } 301 | ], 302 | time: 60 303 | }, 304 | { 305 | level: [5], 306 | title: "你在逐渐死去", 307 | take: [ 308 | { 309 | name: "希望" 310 | }, 311 | { 312 | name: "生命" 313 | } 314 | ], 315 | growth: true, 316 | time: 600 317 | }, 318 | { 319 | level: [6], 320 | title: "你死了", 321 | take: [ 322 | { 323 | name: "你拥有的一切" 324 | } 325 | ], 326 | growth: true, 327 | reset: true, 328 | time: 600 329 | } 330 | ] 331 | } 332 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @import '@hackplan/uui/lib/index.css'; 2 | 3 | @import 'tailwindcss/base'; 4 | @import 'tailwindcss/components'; 5 | @import 'tailwindcss/utilities'; 6 | 7 | body { 8 | margin: 0; 9 | width: 100%; 10 | background: #f4f4f4; 11 | } 12 | 13 | img, svg { 14 | display: inline; 15 | vertical-align: baseline; 16 | } 17 | 18 | button:focus, div:focus, :focus { 19 | outline: none !important; 20 | } 21 | 22 | .UUI-Tag-Root .UUI-Tag-Content{ 23 | font-variant-numeric: tabular-nums; 24 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import * as serviceWorker from './serviceWorker'; 5 | import { create as createMobxPersit } from 'mobx-persist'; 6 | import { gameStore } from './store'; 7 | 8 | const hydrate = createMobxPersit({ 9 | jsonify: true, 10 | }) 11 | 12 | async function main() { 13 | try { 14 | await hydrate("mafan", gameStore) 15 | } catch (e) { 16 | localStorage.clear(); 17 | } 18 | 19 | ReactDOM.render( 20 | 21 | 22 | , 23 | document.getElementById('root') 24 | ); 25 | 26 | // If you want your app to work offline and load faster, you can change 27 | // unregister() to register() below. Note this comes with some pitfalls. 28 | // Learn more about service workers: https://bit.ly/CRA-PWA 29 | } 30 | 31 | void main().catch((e) => console.error(e)); 32 | 33 | serviceWorker.register(); -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' }, 105 | }) 106 | .then(response => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready 134 | .then(registration => { 135 | registration.unregister(); 136 | }) 137 | .catch(error => { 138 | console.error(error.message); 139 | }); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | import { action, observable } from 'mobx'; 3 | import { persist } from 'mobx-persist'; 4 | import { useObserver } from 'mobx-react-lite'; 5 | import { mafan } from './data' 6 | 7 | const updateStuff = (playerGoods, s, isTake) => { 8 | let number = 0; 9 | if (isTake) { 10 | number = -1 * (s.number || 1); 11 | } else { 12 | number = s.number || 1; 13 | } 14 | 15 | const stuffToUpdate = playerGoods.find(stuff => s.name === stuff.name) 16 | 17 | if (stuffToUpdate) { 18 | if (!stuffToUpdate.number) { 19 | stuffToUpdate.number = number + 1; 20 | } else { 21 | stuffToUpdate.number = stuffToUpdate.number + number; 22 | } 23 | if (stuffToUpdate.number === 0) { 24 | playerGoods.splice(playerGoods.findIndex(stuff => s.name === stuff.name), 1); 25 | } 26 | } else { 27 | playerGoods.push(s); 28 | } 29 | } 30 | 31 | const updateGoods = (player, problem) => { 32 | if (problem.gain) { 33 | problem.gain.map((i) => updateStuff(player, i)) 34 | } 35 | if (problem.take) { 36 | problem.take.map((i) => updateStuff(player, i, true)) 37 | } 38 | } 39 | 40 | export class DataStore { 41 | @persist @observable level = 0 42 | @persist('list') @observable goods = [] 43 | 44 | @action.bound 45 | solve(problem) { 46 | if (problem.dismiss) { 47 | if (problem.dismiss.some((item) => this.goods.some((g) => g.name === item))) { 48 | return 49 | } 50 | } 51 | 52 | if (problem.take) { 53 | if (problem.take.some((stuff) => { 54 | if (stuff.name === '你拥有的一切') { 55 | return false 56 | } 57 | const foundItem = this.goods.find((item) => item.name === stuff.name) 58 | if (foundItem && (foundItem.number || 1) >= (stuff.number || 1)) { 59 | return false 60 | } 61 | return true 62 | })) { 63 | return 64 | } 65 | } 66 | 67 | if (problem.reset) { 68 | this.level = 0; 69 | this.goods = []; 70 | return true; 71 | } 72 | updateGoods(this.goods, problem); 73 | if (problem.growth) { 74 | this.level += 1; 75 | } 76 | } 77 | 78 | useLevel = () => useObserver(() => this.level) 79 | useGoods = () => useObserver(() => this.goods) 80 | } 81 | 82 | export const gameStore = new DataStore() 83 | export const problems = mafan.problems 84 | export const levels = mafan.levels 85 | 86 | export const gameContext = createContext(gameStore); -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | prefix: '', 4 | important: false, 5 | separator: ':', 6 | theme: { 7 | screens: { 8 | sm: '640px', 9 | md: '768px', 10 | lg: '1024px', 11 | xl: '1280px', 12 | }, 13 | colors: { 14 | brand: '#fbb778', 15 | 'brand-dark': '#bf6315', 16 | 'fafafa': '#fafafa', 17 | 'f4f4f4': '#f4f4f4', 18 | 'f0f0f0': '#f0f0f0', 19 | 'e6e6e6': '#e6e6e6', 20 | '333333': '#333333', 21 | 'eeeeee': '#eeeeee', 22 | 'dddddd': '#dddddd', 23 | 'c1c1c1': '#C1C1C1', 24 | '363636': '#363636', 25 | '141414': '#141414', 26 | '272727': '#272727', 27 | '575757': '#575757', 28 | '4a4a4a': '#4A4A4A', 29 | '5a5a5a': '#5a5a5a', 30 | '2d2d2d': '#2d2d2d', 31 | '424242': '#424242', 32 | '888888': '#888888', 33 | '999999': '#999999', 34 | transparent: 'transparent', 35 | 36 | black: '#000', 37 | white: '#fff', 38 | 39 | gray: { 40 | 100: '#f7fafc', 41 | 200: '#edf2f7', 42 | 300: '#e2e8f0', 43 | 400: '#cbd5e0', 44 | 500: '#a0aec0', 45 | 600: '#718096', 46 | 700: '#4a5568', 47 | 800: '#2d3748', 48 | 900: '#1a202c', 49 | }, 50 | red: { 51 | 100: '#fff5f5', 52 | 200: '#fed7d7', 53 | 300: '#feb2b2', 54 | 400: '#fc8181', 55 | 500: '#f56565', 56 | 600: '#e53e3e', 57 | 700: '#c53030', 58 | 800: '#9b2c2c', 59 | 900: '#742a2a', 60 | }, 61 | orange: { 62 | 100: '#fffaf0', 63 | 200: '#feebc8', 64 | 300: '#fbd38d', 65 | 400: '#f6ad55', 66 | 500: '#ed8936', 67 | 600: '#dd6b20', 68 | 700: '#c05621', 69 | 800: '#9c4221', 70 | 900: '#7b341e', 71 | }, 72 | yellow: { 73 | 100: '#fffff0', 74 | 200: '#fefcbf', 75 | 300: '#faf089', 76 | 400: '#f6e05e', 77 | 500: '#ecc94b', 78 | 600: '#d69e2e', 79 | 700: '#b7791f', 80 | 800: '#975a16', 81 | 900: '#744210', 82 | }, 83 | green: { 84 | 100: '#f0fff4', 85 | 200: '#c6f6d5', 86 | 300: '#9ae6b4', 87 | 400: '#68d391', 88 | 500: '#48bb78', 89 | 600: '#38a169', 90 | 700: '#2f855a', 91 | 800: '#276749', 92 | 900: '#22543d', 93 | }, 94 | teal: { 95 | 100: '#e6fffa', 96 | 200: '#b2f5ea', 97 | 300: '#81e6d9', 98 | 400: '#4fd1c5', 99 | 500: '#38b2ac', 100 | 600: '#319795', 101 | 700: '#2c7a7b', 102 | 800: '#285e61', 103 | 900: '#234e52', 104 | }, 105 | blue: { 106 | 100: '#ebf8ff', 107 | 200: '#bee3f8', 108 | 300: '#90cdf4', 109 | 400: '#63b3ed', 110 | 500: '#4299e1', 111 | 600: '#3182ce', 112 | 700: '#2b6cb0', 113 | 800: '#2c5282', 114 | 900: '#2a4365', 115 | }, 116 | indigo: { 117 | 100: '#ebf4ff', 118 | 200: '#c3dafe', 119 | 300: '#a3bffa', 120 | 400: '#7f9cf5', 121 | 500: '#667eea', 122 | 600: '#5a67d8', 123 | 700: '#4c51bf', 124 | 800: '#434190', 125 | 900: '#3c366b', 126 | }, 127 | purple: { 128 | 100: '#faf5ff', 129 | 200: '#e9d8fd', 130 | 300: '#d6bcfa', 131 | 400: '#b794f4', 132 | 500: '#9f7aea', 133 | 600: '#805ad5', 134 | 700: '#6b46c1', 135 | 800: '#553c9a', 136 | 900: '#44337a', 137 | }, 138 | pink: { 139 | 100: '#fff5f7', 140 | 200: '#fed7e2', 141 | 300: '#fbb6ce', 142 | 400: '#f687b3', 143 | 500: '#ed64a6', 144 | 600: '#d53f8c', 145 | 700: '#b83280', 146 | 800: '#97266d', 147 | 900: '#702459', 148 | }, 149 | }, 150 | spacing: { 151 | px: '1px', 152 | '0': '0', 153 | '1': '0.25rem', 154 | '2': '0.5rem', 155 | '3': '0.75rem', 156 | '4': '1rem', 157 | '5': '1.25rem', 158 | '6': '1.5rem', 159 | '8': '2rem', 160 | '10': '2.5rem', 161 | '12': '3rem', 162 | '16': '4rem', 163 | '20': '5rem', 164 | '24': '6rem', 165 | '32': '8rem', 166 | '40': '10rem', 167 | '48': '12rem', 168 | '56': '14rem', 169 | '64': '16rem', 170 | }, 171 | backgroundColor: theme => theme('colors'), 172 | backgroundPosition: { 173 | bottom: 'bottom', 174 | center: 'center', 175 | left: 'left', 176 | 'left-bottom': 'left bottom', 177 | 'left-top': 'left top', 178 | right: 'right', 179 | 'right-bottom': 'right bottom', 180 | 'right-top': 'right top', 181 | top: 'top', 182 | }, 183 | backgroundSize: { 184 | auto: 'auto', 185 | cover: 'cover', 186 | contain: 'contain', 187 | }, 188 | borderColor: theme => ({ 189 | ...theme('colors'), 190 | default: theme('colors.gray.300', 'currentColor'), 191 | }), 192 | borderRadius: { 193 | none: '0', 194 | sm: '0.125rem', 195 | default: '0.25rem', 196 | lg: '0.5rem', 197 | full: '9999px', 198 | }, 199 | borderWidth: { 200 | default: '1px', 201 | '0': '0', 202 | '2': '2px', 203 | '4': '4px', 204 | '8': '8px', 205 | }, 206 | boxShadow: { 207 | default: '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)', 208 | md: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)', 209 | lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)', 210 | xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)', 211 | '2xl': '0 25px 50px -12px rgba(0, 0, 0, 0.25)', 212 | inner: 'inset 0 2px 4px 0 rgba(0, 0, 0, 0.06)', 213 | outline: '0 0 0 3px rgba(66, 153, 225, 0.5)', 214 | none: 'none', 215 | }, 216 | container: {}, 217 | cursor: { 218 | auto: 'auto', 219 | default: 'default', 220 | pointer: 'pointer', 221 | wait: 'wait', 222 | text: 'text', 223 | move: 'move', 224 | 'not-allowed': 'not-allowed', 225 | }, 226 | fill: { 227 | current: 'currentColor', 228 | }, 229 | flex: { 230 | '1': '1 1 0%', 231 | auto: '1 1 auto', 232 | initial: '0 1 auto', 233 | none: 'none', 234 | }, 235 | flexGrow: { 236 | '0': '0', 237 | default: '1', 238 | }, 239 | flexShrink: { 240 | '0': '0', 241 | default: '1', 242 | }, 243 | fontFamily: { 244 | sans: [ 245 | '-apple-system', 246 | 'BlinkMacSystemFont', 247 | '"Segoe UI"', 248 | 'Roboto', 249 | '"Helvetica Neue"', 250 | 'Arial', 251 | '"Noto Sans"', 252 | 'sans-serif', 253 | '"Apple Color Emoji"', 254 | '"Segoe UI Emoji"', 255 | '"Segoe UI Symbol"', 256 | '"Noto Color Emoji"', 257 | ], 258 | serif: [ 259 | 'Georgia', 260 | 'Cambria', 261 | '"Times New Roman"', 262 | 'Times', 263 | 'serif', 264 | ], 265 | mono: [ 266 | 'Menlo', 267 | 'Monaco', 268 | 'Consolas', 269 | '"Liberation Mono"', 270 | '"Courier New"', 271 | 'monospace', 272 | ], 273 | }, 274 | fontSize: { 275 | xs: '0.75rem', 276 | sm: '0.875rem', 277 | base: '1rem', 278 | lg: '1.125rem', 279 | xl: '1.25rem', 280 | '2xl': '1.5rem', 281 | '3xl': '1.875rem', 282 | '4xl': '2.25rem', 283 | '5xl': '3rem', 284 | '6xl': '4rem', 285 | }, 286 | fontWeight: { 287 | hairline: '100', 288 | thin: '200', 289 | light: '300', 290 | normal: '400', 291 | medium: '500', 292 | semibold: '600', 293 | bold: '700', 294 | extrabold: '800', 295 | black: '900', 296 | }, 297 | height: theme => ({ 298 | auto: 'auto', 299 | ...theme('spacing'), 300 | full: '100%', 301 | screen: '100vh', 302 | }), 303 | inset: { 304 | '0': '0', 305 | auto: 'auto', 306 | }, 307 | letterSpacing: { 308 | tighter: '-0.05em', 309 | tight: '-0.025em', 310 | normal: '0', 311 | wide: '0.025em', 312 | wider: '0.05em', 313 | widest: '0.1em', 314 | }, 315 | lineHeight: { 316 | none: '1', 317 | tight: '1.25', 318 | snug: '1.375', 319 | normal: '1.5', 320 | relaxed: '1.625', 321 | loose: '2', 322 | }, 323 | listStyleType: { 324 | none: 'none', 325 | disc: 'disc', 326 | decimal: 'decimal', 327 | }, 328 | margin: (theme, { negative }) => ({ 329 | auto: 'auto', 330 | ...theme('spacing'), 331 | ...negative(theme('spacing')), 332 | }), 333 | maxHeight: { 334 | full: '100%', 335 | screen: '100vh', 336 | }, 337 | maxWidth: { 338 | xs: '20rem', 339 | sm: '24rem', 340 | md: '28rem', 341 | lg: '32rem', 342 | xl: '36rem', 343 | '2xl': '42rem', 344 | '3xl': '48rem', 345 | '4xl': '56rem', 346 | '5xl': '64rem', 347 | '6xl': '72rem', 348 | full: '100%', 349 | }, 350 | minHeight: { 351 | '0': '0', 352 | full: '100%', 353 | screen: '100vh', 354 | }, 355 | minWidth: { 356 | '0': '0', 357 | full: '100%', 358 | }, 359 | objectPosition: { 360 | bottom: 'bottom', 361 | center: 'center', 362 | left: 'left', 363 | 'left-bottom': 'left bottom', 364 | 'left-top': 'left top', 365 | right: 'right', 366 | 'right-bottom': 'right bottom', 367 | 'right-top': 'right top', 368 | top: 'top', 369 | }, 370 | opacity: { 371 | '0': '0', 372 | '25': '0.25', 373 | '50': '0.5', 374 | '75': '0.75', 375 | '100': '1', 376 | }, 377 | order: { 378 | first: '-9999', 379 | last: '9999', 380 | none: '0', 381 | '1': '1', 382 | '2': '2', 383 | '3': '3', 384 | '4': '4', 385 | '5': '5', 386 | '6': '6', 387 | '7': '7', 388 | '8': '8', 389 | '9': '9', 390 | '10': '10', 391 | '11': '11', 392 | '12': '12', 393 | }, 394 | padding: theme => theme('spacing'), 395 | placeholderColor: theme => theme('colors'), 396 | stroke: { 397 | current: 'currentColor', 398 | }, 399 | textColor: theme => theme('colors'), 400 | width: theme => ({ 401 | auto: 'auto', 402 | ...theme('spacing'), 403 | '1/2': '50%', 404 | '1/3': '33.333333%', 405 | '2/3': '66.666667%', 406 | '1/4': '25%', 407 | '2/4': '50%', 408 | '3/4': '75%', 409 | '1/5': '20%', 410 | '2/5': '40%', 411 | '3/5': '60%', 412 | '4/5': '80%', 413 | '1/6': '16.666667%', 414 | '2/6': '33.333333%', 415 | '3/6': '50%', 416 | '4/6': '66.666667%', 417 | '5/6': '83.333333%', 418 | '1/12': '8.333333%', 419 | '2/12': '16.666667%', 420 | '3/12': '25%', 421 | '4/12': '33.333333%', 422 | '5/12': '41.666667%', 423 | '6/12': '50%', 424 | '7/12': '58.333333%', 425 | '8/12': '66.666667%', 426 | '9/12': '75%', 427 | '10/12': '83.333333%', 428 | '11/12': '91.666667%', 429 | full: '100%', 430 | screen: '100vw', 431 | }), 432 | zIndex: { 433 | auto: 'auto', 434 | '0': '0', 435 | '10': '10', 436 | '20': '20', 437 | '30': '30', 438 | '40': '40', 439 | '50': '50', 440 | }, 441 | }, 442 | variants: { 443 | accessibility: ['responsive', 'focus'], 444 | alignContent: ['responsive'], 445 | alignItems: ['responsive'], 446 | alignSelf: ['responsive'], 447 | appearance: ['responsive'], 448 | backgroundAttachment: ['responsive'], 449 | backgroundColor: ['responsive', 'hover', 'focus', 'disabled', 'dark', 'dark-hover', 'dark-group-hover'], 450 | backgroundPosition: ['responsive'], 451 | backgroundRepeat: ['responsive'], 452 | backgroundSize: ['responsive'], 453 | borderCollapse: ['responsive'], 454 | borderColor: ['responsive', 'hover', 'focus', 'dark', 'dark-focus', 'dark-focus-within'], 455 | borderRadius: ['responsive'], 456 | borderStyle: ['responsive'], 457 | borderWidth: ['responsive', 'last'], 458 | boxShadow: ['responsive', 'hover', 'focus'], 459 | cursor: ['responsive'], 460 | display: ['responsive', 'group-hover'], 461 | fill: ['responsive'], 462 | flex: ['responsive'], 463 | flexDirection: ['responsive'], 464 | flexGrow: ['responsive'], 465 | flexShrink: ['responsive'], 466 | flexWrap: ['responsive'], 467 | float: ['responsive'], 468 | fontFamily: ['responsive'], 469 | fontSize: ['responsive'], 470 | fontSmoothing: ['responsive'], 471 | fontStyle: ['responsive'], 472 | fontWeight: ['responsive', 'hover', 'focus'], 473 | height: ['responsive', 'empty'], 474 | inset: ['responsive'], 475 | justifyContent: ['responsive'], 476 | letterSpacing: ['responsive'], 477 | lineHeight: ['responsive'], 478 | listStylePosition: ['responsive'], 479 | listStyleType: ['responsive'], 480 | margin: ['responsive'], 481 | maxHeight: ['responsive'], 482 | maxWidth: ['responsive'], 483 | minHeight: ['responsive'], 484 | minWidth: ['responsive'], 485 | objectFit: ['responsive'], 486 | objectPosition: ['responsive'], 487 | opacity: ['responsive', 'hover', 'focus'], 488 | order: ['responsive'], 489 | outline: ['responsive', 'focus'], 490 | overflow: ['responsive'], 491 | padding: ['responsive'], 492 | placeholderColor: ['responsive', 'focus'], 493 | pointerEvents: ['responsive'], 494 | position: ['responsive'], 495 | resize: ['responsive'], 496 | stroke: ['responsive'], 497 | tableLayout: ['responsive'], 498 | textAlign: ['responsive'], 499 | textColor: ['responsive', 'hover', 'focus', 'dark', 'dark-hover', 'dark-active'], 500 | textDecoration: ['responsive', 'hover', 'focus'], 501 | textTransform: ['responsive'], 502 | userSelect: ['responsive'], 503 | verticalAlign: ['responsive'], 504 | visibility: ['responsive'], 505 | whitespace: ['responsive'], 506 | width: ['responsive'], 507 | wordBreak: ['responsive'], 508 | zIndex: ['responsive'], 509 | }, 510 | corePlugins: {}, 511 | plugins: [ 512 | function ({ addVariant, e }) { 513 | addVariant('last', ({ modifySelectors, separator }) => { 514 | modifySelectors(({ className }) => { 515 | return `.${e(`last${separator}${className}`)}:last-child` 516 | }) 517 | }) 518 | addVariant('empty', ({ modifySelectors, separator }) => { 519 | modifySelectors(({ className }) => { 520 | return `.${e(`empty${separator}${className}`)}:empty` 521 | }) 522 | }) 523 | addVariant('important', ({ container }) => { 524 | container.walkRules(rule => { 525 | rule.selector = `.\\!${rule.selector.slice(1)}` 526 | rule.walkDecls(decl => { 527 | decl.important = true 528 | }) 529 | }) 530 | }) 531 | addVariant('disabled', ({ modifySelectors, separator }) => { 532 | modifySelectors(({ className }) => { 533 | return `.${e(`disabled${separator}${className}`)}:disabled` 534 | }) 535 | }) 536 | addVariant('dark', ({ modifySelectors, separator }) => { 537 | modifySelectors(({ className }) => { 538 | return `.mode-dark .${e(`dark${separator}${className}`)}`; 539 | }); 540 | }); 541 | 542 | addVariant('dark-hover', ({ modifySelectors, separator }) => { 543 | modifySelectors(({ className }) => { 544 | return `.mode-dark .${e(`dark-hover${separator}${className}`)}:hover`; 545 | }); 546 | }); 547 | 548 | addVariant('dark-focus', ({ modifySelectors, separator }) => { 549 | modifySelectors(({ className }) => { 550 | return `.mode-dark .${e(`dark-focus${separator}${className}`)}:focus`; 551 | }); 552 | }); 553 | 554 | addVariant('dark-active', ({ modifySelectors, separator }) => { 555 | modifySelectors(({ className }) => { 556 | return `.mode-dark .${e(`dark-active${separator}${className}`)}:active`; 557 | }); 558 | }); 559 | 560 | addVariant('dark-group-hover', ({ modifySelectors, separator }) => { 561 | modifySelectors(({ className }) => { 562 | return `.mode-dark .group:hover .${e(`dark-group-hover${separator}${className}`)}`; 563 | }); 564 | }); 565 | 566 | addVariant('dark-focus-within', ({ modifySelectors, separator }) => { 567 | modifySelectors(({ className }) => { 568 | return `.mode-dark .${e(`dark-focus-within${separator}${className}`)}:focus-within`; 569 | }); 570 | }); 571 | } 572 | ] 573 | } 574 | --------------------------------------------------------------------------------