├── public ├── robots.txt ├── favicon.ico ├── manifest.json └── index.html ├── src ├── static │ ├── images │ │ ├── wolf.jpg │ │ ├── hunter.jpg │ │ ├── idiot.jpg │ │ ├── knight.jpg │ │ ├── witch.jpg │ │ ├── predictor.jpg │ │ └── villager.jpg │ └── audio │ │ ├── step_1.mp3 │ │ ├── step_10.mp3 │ │ ├── step_11.mp3 │ │ ├── step_12.mp3 │ │ ├── step_13.mp3 │ │ ├── step_14.mp3 │ │ ├── step_15.mp3 │ │ ├── step_16.mp3 │ │ ├── step_17.mp3 │ │ ├── step_18.mp3 │ │ ├── step_19.mp3 │ │ ├── step_2.mp3 │ │ ├── step_20.mp3 │ │ ├── step_21.mp3 │ │ ├── step_22.mp3 │ │ ├── step_3.mp3 │ │ ├── step_4.mp3 │ │ ├── step_5.mp3 │ │ ├── step_6.mp3 │ │ ├── step_7.mp3 │ │ ├── step_8.mp3 │ │ └── step_9.mp3 ├── App.test.js ├── App.js ├── index.css ├── index.js ├── App.css ├── constants │ └── Role.js ├── locales │ ├── index.js │ └── languages │ │ ├── zh-TW.json │ │ └── en-US.json ├── logo.svg ├── components │ ├── CheckRole │ │ └── CheckRole.js │ ├── Setting │ │ └── Setting.js │ ├── Home │ │ └── Home.js │ └── Game │ │ └── Game.js └── serviceWorker.js ├── .gitignore ├── .vscode └── settings.json ├── package.json └── README.md /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JaeyoLin/wolf_judge/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/static/images/wolf.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JaeyoLin/wolf_judge/HEAD/src/static/images/wolf.jpg -------------------------------------------------------------------------------- /src/static/audio/step_1.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JaeyoLin/wolf_judge/HEAD/src/static/audio/step_1.mp3 -------------------------------------------------------------------------------- /src/static/audio/step_10.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JaeyoLin/wolf_judge/HEAD/src/static/audio/step_10.mp3 -------------------------------------------------------------------------------- /src/static/audio/step_11.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JaeyoLin/wolf_judge/HEAD/src/static/audio/step_11.mp3 -------------------------------------------------------------------------------- /src/static/audio/step_12.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JaeyoLin/wolf_judge/HEAD/src/static/audio/step_12.mp3 -------------------------------------------------------------------------------- /src/static/audio/step_13.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JaeyoLin/wolf_judge/HEAD/src/static/audio/step_13.mp3 -------------------------------------------------------------------------------- /src/static/audio/step_14.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JaeyoLin/wolf_judge/HEAD/src/static/audio/step_14.mp3 -------------------------------------------------------------------------------- /src/static/audio/step_15.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JaeyoLin/wolf_judge/HEAD/src/static/audio/step_15.mp3 -------------------------------------------------------------------------------- /src/static/audio/step_16.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JaeyoLin/wolf_judge/HEAD/src/static/audio/step_16.mp3 -------------------------------------------------------------------------------- /src/static/audio/step_17.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JaeyoLin/wolf_judge/HEAD/src/static/audio/step_17.mp3 -------------------------------------------------------------------------------- /src/static/audio/step_18.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JaeyoLin/wolf_judge/HEAD/src/static/audio/step_18.mp3 -------------------------------------------------------------------------------- /src/static/audio/step_19.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JaeyoLin/wolf_judge/HEAD/src/static/audio/step_19.mp3 -------------------------------------------------------------------------------- /src/static/audio/step_2.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JaeyoLin/wolf_judge/HEAD/src/static/audio/step_2.mp3 -------------------------------------------------------------------------------- /src/static/audio/step_20.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JaeyoLin/wolf_judge/HEAD/src/static/audio/step_20.mp3 -------------------------------------------------------------------------------- /src/static/audio/step_21.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JaeyoLin/wolf_judge/HEAD/src/static/audio/step_21.mp3 -------------------------------------------------------------------------------- /src/static/audio/step_22.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JaeyoLin/wolf_judge/HEAD/src/static/audio/step_22.mp3 -------------------------------------------------------------------------------- /src/static/audio/step_3.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JaeyoLin/wolf_judge/HEAD/src/static/audio/step_3.mp3 -------------------------------------------------------------------------------- /src/static/audio/step_4.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JaeyoLin/wolf_judge/HEAD/src/static/audio/step_4.mp3 -------------------------------------------------------------------------------- /src/static/audio/step_5.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JaeyoLin/wolf_judge/HEAD/src/static/audio/step_5.mp3 -------------------------------------------------------------------------------- /src/static/audio/step_6.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JaeyoLin/wolf_judge/HEAD/src/static/audio/step_6.mp3 -------------------------------------------------------------------------------- /src/static/audio/step_7.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JaeyoLin/wolf_judge/HEAD/src/static/audio/step_7.mp3 -------------------------------------------------------------------------------- /src/static/audio/step_8.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JaeyoLin/wolf_judge/HEAD/src/static/audio/step_8.mp3 -------------------------------------------------------------------------------- /src/static/audio/step_9.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JaeyoLin/wolf_judge/HEAD/src/static/audio/step_9.mp3 -------------------------------------------------------------------------------- /src/static/images/hunter.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JaeyoLin/wolf_judge/HEAD/src/static/images/hunter.jpg -------------------------------------------------------------------------------- /src/static/images/idiot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JaeyoLin/wolf_judge/HEAD/src/static/images/idiot.jpg -------------------------------------------------------------------------------- /src/static/images/knight.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JaeyoLin/wolf_judge/HEAD/src/static/images/knight.jpg -------------------------------------------------------------------------------- /src/static/images/witch.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JaeyoLin/wolf_judge/HEAD/src/static/images/witch.jpg -------------------------------------------------------------------------------- /src/static/images/predictor.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JaeyoLin/wolf_judge/HEAD/src/static/images/predictor.jpg -------------------------------------------------------------------------------- /src/static/images/villager.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JaeyoLin/wolf_judge/HEAD/src/static/images/villager.jpg -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Home from './components/Home/Home'; 4 | import './App.css'; 5 | 6 | import './locales/index'; 7 | 8 | function App() { 9 | return ( 10 |
11 | 12 |
13 | ); 14 | } 15 | 16 | export default App; 17 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Wolf", 3 | "name": "Wolf", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100vh; 3 | } 4 | body { 5 | margin: 0; 6 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 7 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 8 | sans-serif; 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | height: 100vh; 12 | background-color: #f5f5f5; 13 | } 14 | 15 | code { 16 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 17 | monospace; 18 | } 19 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | 9 | // If you want your app to work offline and load faster, you can change 10 | // unregister() to register() below. Note this comes with some pitfalls. 11 | // Learn more about service workers: https://bit.ly/CRA-PWA 12 | // serviceWorker.unregister(); 13 | serviceWorker.register(); 14 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | height: 100vh; 4 | } 5 | 6 | .App-logo { 7 | animation: App-logo-spin infinite 20s linear; 8 | height: 40vmin; 9 | pointer-events: none; 10 | } 11 | 12 | .App-header { 13 | background-color: #282c34; 14 | min-height: 100vh; 15 | display: flex; 16 | flex-direction: column; 17 | align-items: center; 18 | justify-content: center; 19 | font-size: calc(10px + 2vmin); 20 | color: white; 21 | } 22 | 23 | .App-link { 24 | color: #61dafb; 25 | } 26 | 27 | @keyframes App-logo-spin { 28 | from { 29 | transform: rotate(0deg); 30 | } 31 | to { 32 | transform: rotate(360deg); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "terminal.external.osxExec": "iTerm.app", 3 | "terminal.integrated.fontFamily": "Source Code Pro for Powerline", 4 | "terminal.integrated.fontSize": 14, 5 | "editor.tokenColorCustomizations": { 6 | "[Material Theme Darker High Contrast]": { 7 | "comments": "#229977", 8 | }, 9 | }, 10 | // "editor.fontFamily": "'Fira Code', 'Source Code Pro', Consolas, 'Microsoft JhengHei', 'Courier New', monospace", 11 | "editor.lineHeight": 24, 12 | "editor.fontLigatures": true, 13 | // "explorer.decorations.badges": false 14 | "workbench.sideBar.location": "right", 15 | "colorize.languages": [ 16 | "javascript", 17 | ], 18 | "editor.minimap.enabled": false, 19 | "editor.fontSize": 14, 20 | "editor.renderWhitespace": "all" 21 | } -------------------------------------------------------------------------------- /src/constants/Role.js: -------------------------------------------------------------------------------- 1 | export const WOLF = { 2 | key: 'wolf', 3 | isGood: false, 4 | isGod: false, 5 | src: 'wolf.jpg', 6 | }; 7 | 8 | export const PREDICTOR = { 9 | key: 'predictor', 10 | isGood: true, 11 | isGod: true, 12 | src: 'predictor.jpg', 13 | }; 14 | 15 | export const WITCH = { 16 | key: 'witch', 17 | isGood: true, 18 | isGod: true, 19 | src: 'witch.jpg', 20 | }; 21 | 22 | export const HUNTER = { 23 | key: 'hunter', 24 | isGood: true, 25 | isGod: true, 26 | src: 'hunter.jpg', 27 | }; 28 | 29 | export const KNIGHT = { 30 | key: 'knight', 31 | isGood: true, 32 | isGod: true, 33 | src: 'knight.jpg', 34 | }; 35 | 36 | export const idiot = { 37 | key: 'idiot', 38 | isGood: true, 39 | isGod: true, 40 | src: 'idiot.jpg', 41 | }; 42 | 43 | export const VILLAGER = { 44 | key: 'villager', 45 | isGood: true, 46 | isGod: false, 47 | src: 'villager.jpg', 48 | }; 49 | -------------------------------------------------------------------------------- /src/locales/index.js: -------------------------------------------------------------------------------- 1 | import i18n from 'i18next'; 2 | import { initReactI18next } from "react-i18next"; 3 | 4 | import enUS from './languages/en-US.json'; 5 | import zhTW from './languages/zh-TW.json'; 6 | 7 | // console.log('zhTW', zhTW); 8 | 9 | i18n 10 | .use(initReactI18next) 11 | .init({ 12 | // we init with resources 13 | resources: { 14 | 'en-US': { 15 | translations: enUS, 16 | }, 17 | 'zh-TW': { 18 | translations: zhTW, 19 | }, 20 | }, 21 | lng: localStorage.getItem('language') || 'zh-TW', 22 | fallbackLng: 'zh-TW', 23 | debug: true, 24 | 25 | // have a common namespace used around the full app 26 | ns: ['translations'], 27 | defaultNS: 'translations', 28 | 29 | // keySeparator: false, // we use content as keys 30 | 31 | interpolation: { 32 | escapeValue: false, // not needed for react!! 33 | formatSeparator: ',', 34 | }, 35 | 36 | react: { 37 | wait: true, 38 | }, 39 | }); 40 | 41 | export default i18n; 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wolf_judge", 3 | "version": "0.1.0", 4 | "private": true, 5 | "homepage": "https://jaeyolin.github.io/wolf_judge/", 6 | "dependencies": { 7 | "react": "^16.9.0", 8 | "react-dom": "^16.9.0", 9 | "react-scripts": "3.1.1" 10 | }, 11 | "scripts": { 12 | "start": "react-scripts start", 13 | "build": "react-scripts build", 14 | "test": "react-scripts test", 15 | "eject": "react-scripts eject", 16 | "predeploy": "npm run build", 17 | "deploy": "gh-pages -d build" 18 | }, 19 | "eslintConfig": { 20 | "extends": "react-app" 21 | }, 22 | "browserslist": { 23 | "production": [ 24 | ">0.2%", 25 | "not dead", 26 | "not op_mini all" 27 | ], 28 | "development": [ 29 | "last 1 chrome version", 30 | "last 1 firefox version", 31 | "last 1 safari version" 32 | ] 33 | }, 34 | "devDependencies": { 35 | "@material-ui/core": "^4.3.2", 36 | "@material-ui/icons": "^4.2.1", 37 | "gh-pages": "^2.1.1", 38 | "i18next": "^17.0.11", 39 | "react-i18next": "^10.12.2", 40 | "react-sound": "^1.2.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 狼人殺自動法官 2 | 3 | 你約了一些朋友想玩狼人殺嗎? 一個人要去當法官是不是又人不夠開局了. 4 | 5 | 這是一個沒有這麼多朋友, 也沒人想當法官, 又想玩狼人殺的法官輔助工具. 6 | 7 | ## 網頁 8 | 9 | - [請點我](https://jaeyolin.github.io/wolf_judge/) 10 | 11 | ## 功能特色 12 | 13 | - 不用實體卡片就可以使用, 工具內建隨機抽牌功能. 14 | - 自動主持人 (Google 小姐) 15 | - 第一次網頁載入後, 可離線使用. 16 | 17 | ## 角色 18 | 19 | - 預言家 20 | - 女巫 21 | - 獵人 22 | - 騎士 23 | - 白癡 24 | - 狼人 25 | 26 | >未來應該不會更新其他角色, 也不會加入警長競選規則, 我沒這麼多朋友可以玩到這麼多角色. 27 | >警長競選規則可以自己加進去玩, 跟自動法官沒什麼關係. 28 | 29 | ## 使用方式 30 | 31 | - 手機開啟此網頁. 32 | - 設定好角色. 33 | - 傳閱手機並查看自己的角色. 34 | - 手機放在中間, 大家伸出一隻手放在手機旁邊. 35 | - 遊戲開始. 36 | - 天黑請閉眼 37 | - 主持人會叫各種角色睜眼, 在手機螢幕上操作各功能. 38 | 39 | >網頁跑過一次應該就會了. 40 | >第一晚有人出局可以請出局玩家當主持人, 或繼續使用自動法官, 給出局的玩家一點參與感吧. 41 | >第一天沒人出局就繼續使用自動法官吧. 42 | 43 | ## 騎士決鬥 44 | 45 | - 要發動騎士技能的人請點選騎士決鬥. 46 | - 有使用騎士並且技能未發動可以點選決鬥. 47 | - 點選決鬥後會顯示誰是騎士, 所以不怕有人亂跳騎士, 破壞遊戲體驗. 48 | - 選擇一個要決鬥的對象, 系統會判定是不是狼人. 49 | 50 | ## 白癡 51 | 52 | - 被放逐時, 如果該玩家為白癡, 系統會顯示該玩家為白癡牌, 放逐無效. 53 | - 已亮身份的白癡牌白天一樣有發言權, 但無投票權. 54 | - 白癡牌在第二次放逐是可以的, 如果你們想放逐他. 55 | - 白癡牌可以被毒殺, 如果女巫想毒的話. 56 | 57 | ## 狼人自爆 58 | 59 | - 有狼人想自爆的話, 在放逐的時候選擇該玩家號碼就可以了, 可不可以指刀你們事先說好就可以, 自由心證. 60 | 61 | ## 勝利判斷與條件 62 | 63 | - 白天睜眼, 白天投票, 獵人開槍, 騎士決鬥都會判斷遊戲是否結束 64 | - 正義聯盟獲勝: 所有狼人出局. 65 | - 邪惡陣營獲勝: 所有好人出局. 66 | - 有開啟屠邊局, 會判斷全部的神或全部的民出局, 狼人獲勝. 67 | 68 | ## 已實作功能 69 | 70 | - 預言家每晚都會睜眼查驗角色. 71 | - 女巫晚上不能一次使用兩種藥. 72 | - 獵人被毒殺不可開槍, 每晚都會睜眼看可不可以開槍. 73 | - 有使用的角色死掉時, 主持人會自動播放該角色相關台詞. 74 | 75 | ## 未實作功能 76 | 77 | - 女巫是否可以自救, 因為各玩法就不加入工具判斷了, 自己玩的時候講好就好, 自由心證. 78 | - 狼人是否可以連續兩天刀同一個人, 理由跟上面一樣, 自由心證. 79 | - 有使用騎士, 主持人每晚都會講一次台詞, 讓出局的玩家知道誰是騎士. 80 | - 有使用白癡, 主持人每晚都會講一次台詞, 讓出局的玩家知道誰是白癡. 81 | - 預言家可查驗自己, 你想查的話. 82 | - 女巫可毒自己, 你想毒的話. 83 | - 騎士可決鬥自己, 你想決鬥的話. 84 | -------------------------------------------------------------------------------- /src/locales/languages/zh-TW.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "狼人殺法官", 3 | "gaming": "法官主持中...", 4 | "restart": "重新開始", 5 | "resetting": "重新設定", 6 | "player_number": "遊戲人數", 7 | "wolf_number": "狼人數量", 8 | "wolf": "狼人", 9 | "predictor": "預言家", 10 | "witch": "女巫", 11 | "hunter": "獵人", 12 | "villager": "村民", 13 | "knight": "騎士", 14 | "idiot": "白癡", 15 | "yes": "是", 16 | "no": "否", 17 | "finished": "完成", 18 | "start": "開始遊戲", 19 | "sit_number": "{{index}} 號玩家", 20 | "check_role": "請點選, 並確認身份.", 21 | "is_checked": "該玩家已確認身份.", 22 | "confirm": "確認", 23 | "your_role": "你的角色是...", 24 | "dead_person": "{{index}} 號玩家被殺死.", 25 | "is_use_save": "此回合已使用解藥, 不能使用毒藥", 26 | "wolf_kill": "狼人請殺人", 27 | "witch_save": "你要使用解藥嗎?", 28 | "witch_poison": "你要使用毒藥嗎?", 29 | "predictor_select": "你要查驗的對象是?", 30 | "role_result": "這位玩家的身份是", 31 | "is_wolf": "狼人", 32 | "not_wolf": "好人", 33 | "yesterday_dead": "昨夜的情況是", 34 | "christmas_eve": "平安夜", 35 | "dead_player": "{{ returnMessage }} 號玩家死亡", 36 | "dead_message": "遊戲訊息", 37 | "n_day": "第 {{day}} 天白天: ", 38 | "n_night" : "第 {{day}} 天夜晚: ", 39 | "start_vote": "開始放逐投票", 40 | "give_up": "放棄", 41 | "give_up_vote": "棄票", 42 | "save_used": "你已使用解藥", 43 | "poison_used": "你已使用毒藥", 44 | "game_over": "遊戲結束", 45 | "good_win": "正義聯盟獲勝", 46 | "bad_win": "邪惡陣營獲勝", 47 | "hunter_shoot": "獵人請發動技能", 48 | "could_shoot": "獵人是否可以開槍", 49 | "can_shoot": "可以", 50 | "cant_shoot": "不可以", 51 | "hunter_shoot_player": "獵人射殺: {{index}}", 52 | "last_words": "請發表遺言", 53 | "to_night": "天黑請閉眼", 54 | "is_kill_kind": "屠邊局", 55 | "bad_win_kind": "邪惡陣營獲勝, 屠邊成功.", 56 | "knight_fight": "騎士決鬥", 57 | "fight_result": "決鬥結果", 58 | "no_is_wolf": "{{index}} 是狼人", 59 | "no_is_not_wolf": "{{index}} 不是狼人, 騎士以死謝罪.", 60 | "idiot_result": "放逐無效", 61 | "no_is_idiot": "{{index}} 是白癡", 62 | "is_show_message": "是否顯示夜晚訊息", 63 | "is_mirror": "彈出視窗鏡像模式" 64 | } -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 14 | 18 | 19 | 28 | Wolf Judge 29 | 30 | 31 | 32 |
33 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/locales/languages/en-US.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Wolf Judge", 3 | "gaming": "Judge hosting...", 4 | "restart": "Restart", 5 | "resetting": "Re Setting", 6 | "player_number": "Player Number", 7 | "wolf_number": "Wolf Number", 8 | "wolf": "Wolf", 9 | "predictor": "Predictor", 10 | "witch": "Witch", 11 | "hunter": "Hunter", 12 | "villager": "Villager", 13 | "knight": "Knight", 14 | "idiot": "idiot", 15 | "yes": "Yes", 16 | "no": "No", 17 | "finished": "Finished", 18 | "start": "Start Game", 19 | "sit_number": "Player No. {{index}}", 20 | "check_role": "Click and check the role.", 21 | "is_checked": "The player is checked.", 22 | "confirm": "Confirm", 23 | "your_role": "Your role is...", 24 | "dead_person": "Player No. {{index}} is dead.", 25 | "is_use_save": "Could't use two skill in the term.", 26 | "wolf_kill": "Kill someone", 27 | "witch_save": "Use Antidote?", 28 | "witch_poison": "Use Poison?", 29 | "predictor_select": "Check", 30 | "role_result": "The player is", 31 | "is_wolf": "Bad guy", 32 | "not_wolf": "Good guy", 33 | "yesterday_dead": "Last night is", 34 | "christmas_eve": "Safety night", 35 | "dead_player": "Player NO.{{ returnMessage }} was dead.", 36 | "dead_message": "Game Message", 37 | "n_day": "Night {{day}}: ", 38 | "n_night" : "Day {{day}}: ", 39 | "start_vote": "Start Voting", 40 | "give_up": "Give up", 41 | "give_up_vote": "Give up", 42 | "save_used": "You used it.", 43 | "poison_used": "You used it.", 44 | "game_over": "Game Over", 45 | "good_win": "Good guy win", 46 | "bad_win": "Bad guy win", 47 | "hunter_shoot": "Shoot", 48 | "could_shoot": "Can you shoot?", 49 | "can_shoot": "You can", 50 | "cant_shoot": "You can't", 51 | "hunter_shoot_player": "Hunter shooted: {{index}}", 52 | "last_words": "Publish last words", 53 | "to_night": "Close your eyes", 54 | "is_kill_kind": "Kill kind", 55 | "bad_win_kind": "Bad guy win", 56 | "knight_fight": "Kinght fight", 57 | "fight_result": "Fight Result", 58 | "no_is_wolf": "{{index}} is the wolf.", 59 | "no_is_not_wolf": "{{index}} is not the wolf. Knight is dead.", 60 | "idiot_result": "idiot Result", 61 | "no_is_idiot": "{{index}} is the idiot.", 62 | "is_show_message": "Is show messages?", 63 | "is_mirror": "Is mirror?" 64 | } -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/components/CheckRole/CheckRole.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/alt-text */ 2 | import React, { 3 | useState, 4 | } from 'react'; 5 | import { useTranslation } from "react-i18next"; 6 | import { makeStyles } from '@material-ui/core/styles'; 7 | import Paper from '@material-ui/core/Paper'; 8 | import Typography from '@material-ui/core/Typography'; 9 | import Dialog from '@material-ui/core/Dialog'; 10 | import DialogActions from '@material-ui/core/DialogActions'; 11 | import DialogContent from '@material-ui/core/DialogContent'; 12 | import DialogContentText from '@material-ui/core/DialogContentText'; 13 | import DialogTitle from '@material-ui/core/DialogTitle'; 14 | import Button from '@material-ui/core/Button'; 15 | import Divider from '@material-ui/core/Divider'; 16 | 17 | import wolf from '../../static/images/wolf.jpg'; 18 | import predictor from '../../static/images/predictor.jpg'; 19 | import witch from '../../static/images/witch.jpg'; 20 | import hunter from '../../static/images/hunter.jpg'; 21 | import knight from '../../static/images/knight.jpg'; 22 | import idiot from '../../static/images/idiot.jpg'; 23 | import villager from '../../static/images/villager.jpg'; 24 | 25 | const useStyles = makeStyles(theme => ({ 26 | root: { 27 | padding: theme.spacing(3, 2), 28 | margin: '10px', 29 | }, 30 | isChecked: { 31 | color: 'red', 32 | }, 33 | role: { 34 | textAlign: 'center', 35 | fontSize: '30px', 36 | }, 37 | })); 38 | 39 | const RoleCard = (props) => { 40 | const { sit } = props; 41 | const { t } = useTranslation(); 42 | const classes = useStyles(); 43 | 44 | const [isWatch, setIsWatch] = useState(false); 45 | const [isOpen, setIsOpen] = useState(false); 46 | 47 | const handleClick = () => { 48 | if (!isWatch) { 49 | setIsOpen(true); 50 | } 51 | } 52 | 53 | const handleClose = () => { 54 | setIsOpen(false); 55 | setIsWatch(true); 56 | } 57 | 58 | let src = null; 59 | switch(sit.role.key) { 60 | case 'wolf': 61 | src = wolf; 62 | break; 63 | case 'predictor': 64 | src = predictor; 65 | break; 66 | case 'witch': 67 | src = witch; 68 | break; 69 | case 'hunter': 70 | src = hunter; 71 | break; 72 | case 'knight': 73 | src = knight; 74 | break; 75 | case 'idiot': 76 | src = idiot; 77 | break; 78 | case 'villager': 79 | src = villager; 80 | break; 81 | default: 82 | break; 83 | }; 84 | 85 | return ( 86 | <> 87 | 88 | 89 | { 90 | t('sit_number', { index: sit.index }) 91 | // sit.role.key 92 | } 93 | 94 | 95 | { (isWatch) ? ( 96 | 97 | { t('is_checked') } 98 | 99 | ) : t('check_role') } 100 | 101 | 102 | 103 | 109 | {t('your_role')} 110 | 111 | 112 |
113 | { t(sit.role.key) } 114 |
115 | 116 |
117 |
118 | 119 | 122 | 123 |
124 | 125 | ); 126 | }; 127 | 128 | const CheckRole = (props) => { 129 | const { t } = useTranslation(); 130 | const { list, handleStartGame } = props; 131 | // const classes = useStyles(); 132 | 133 | return ( 134 | <> 135 | { 136 | list.map((sit, index) => { 137 | return ( 138 |
139 | 142 |
143 | ) 144 | }) 145 | } 146 | 147 | 148 | 149 | ); 150 | }; 151 | 152 | export default CheckRole; 153 | -------------------------------------------------------------------------------- /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.1/8 is 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 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/components/Setting/Setting.js: -------------------------------------------------------------------------------- 1 | import React, { 2 | Fragment 3 | } from 'react'; 4 | import { useTranslation } from "react-i18next"; 5 | import { makeStyles } from '@material-ui/core/styles'; 6 | import Button from '@material-ui/core/Button'; 7 | import FormControl from '@material-ui/core/FormControl'; 8 | import Divider from '@material-ui/core/Divider'; 9 | import InputLabel from '@material-ui/core/InputLabel'; 10 | import NativeSelect from '@material-ui/core/NativeSelect'; 11 | 12 | const useStyles = makeStyles(theme => ({ 13 | root: { 14 | display: 'flex', 15 | }, 16 | group: { 17 | margin: theme.spacing(1, 0), 18 | }, 19 | formControl: { 20 | margin: theme.spacing(1), 21 | minWidth: 200, 22 | }, 23 | selectEmpty: { 24 | marginTop: theme.spacing(2), 25 | }, 26 | })); 27 | 28 | const Setting = (props) => { 29 | const classes = useStyles(); 30 | const { t } = useTranslation(); 31 | const { 32 | playerNumber, 33 | setPlayerNumber, 34 | wolfNumber, 35 | setWolfNumber, 36 | isUsePredictor, 37 | setIsUsePredictor, 38 | isUseWitch, 39 | setIsUseWitch, 40 | isUseHunter, 41 | setIsUseHunter, 42 | handleStart, 43 | isKillKind, 44 | setIsKillKind, 45 | isUseKnight, 46 | setIsUseKnight, 47 | isUseidiot, 48 | setIsUseidiot, 49 | isMirror, 50 | setIsMirror, 51 | } = props; 52 | 53 | return ( 54 | 55 |
56 | 57 | {t('player_number')} 58 | {setPlayerNumber(e.target.value * 1)}} 62 | > 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 |
73 | 74 |
75 | 76 | {t('wolf_number')} 77 | {setWolfNumber(e.target.value * 1)}} 81 | > 82 | 83 | 84 | 85 | 86 | 87 | 88 |
89 | 90 |
91 | 92 | {t('is_kill_kind')} 93 | {setIsKillKind(e.target.value === 'true')}} 97 | > 98 | 99 | 100 | 101 | 102 |
103 | 104 |
105 | 106 | {t('predictor')} 107 | {setIsUsePredictor(e.target.value === 'true')}} 111 | > 112 | 113 | 114 | 115 | 116 |
117 | 118 |
119 | 120 | {t('witch')} 121 | {setIsUseWitch(e.target.value === 'true')}} 125 | > 126 | 127 | 128 | 129 | 130 |
131 | 132 |
133 | 134 | {t('hunter')} 135 | {setIsUseHunter(e.target.value === 'true')}} 139 | > 140 | 141 | 142 | 143 | 144 |
145 | 146 |
147 | 148 | {t('knight')} 149 | {setIsUseKnight(e.target.value === 'true')}} 153 | > 154 | 155 | 156 | 157 | 158 |
159 | 160 |
161 | 162 | {t('idiot')} 163 | {setIsUseidiot(e.target.value === 'true')}} 167 | > 168 | 169 | 170 | 171 | 172 |
173 | 174 |
175 | 176 | {t('is_mirror')} 177 | {setIsMirror(e.target.value === 'true')}} 181 | > 182 | 183 | 184 | 185 | 186 |
187 | 188 | 189 | 190 |
191 | ); 192 | }; 193 | 194 | export default Setting; 195 | -------------------------------------------------------------------------------- /src/components/Home/Home.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-extra-label */ 2 | import React, { 3 | useState, 4 | } from 'react'; 5 | import { useTranslation } from "react-i18next"; 6 | import Typography from '@material-ui/core/Typography'; 7 | import Container from '@material-ui/core/Container'; 8 | import { makeStyles } from '@material-ui/core/styles'; 9 | import AppBar from '@material-ui/core/AppBar'; 10 | import Toolbar from '@material-ui/core/Toolbar'; 11 | import IconButton from '@material-ui/core/IconButton'; 12 | import MenuIcon from '@material-ui/icons/Menu'; 13 | import Select from '@material-ui/core/Select'; 14 | import Drawer from '@material-ui/core/Drawer'; 15 | import Divider from '@material-ui/core/Divider'; 16 | import List from '@material-ui/core/List'; 17 | import ListItem from '@material-ui/core/ListItem'; 18 | import ListItemIcon from '@material-ui/core/ListItemIcon'; 19 | import ListItemText from '@material-ui/core/ListItemText'; 20 | import CachedIcon from '@material-ui/icons/Cached'; 21 | 22 | import { 23 | WOLF, 24 | PREDICTOR, 25 | WITCH, 26 | HUNTER, 27 | KNIGHT, 28 | idiot, 29 | VILLAGER, 30 | } from '../../constants/Role'; 31 | import Setting from '../Setting/Setting'; 32 | import CheckRole from '../CheckRole/CheckRole'; 33 | import Game from '../Game/Game'; 34 | 35 | const drawerWidth = 240; 36 | 37 | const createArray = (len, itm) => { 38 | let arr1 = [itm], 39 | arr2 = []; 40 | while (len > 0) { 41 | if (len & 1) arr2 = arr2.concat(arr1); 42 | arr1 = arr1.concat(arr1); 43 | len >>>= 1; 44 | } 45 | return arr2; 46 | } 47 | 48 | const useStyles = makeStyles(theme => ({ 49 | container: { 50 | paddingLeft: '0px', 51 | paddingRight: '0px', 52 | height: '100vh', 53 | }, 54 | toolbar: theme.mixins.toolbar, 55 | drawer: { 56 | width: drawerWidth, 57 | }, 58 | drawerPaper: { 59 | width: drawerWidth, 60 | }, 61 | root: { 62 | flexGrow: 1, 63 | }, 64 | menuButton: { 65 | marginRight: theme.spacing(2), 66 | }, 67 | title: { 68 | flexGrow: 1, 69 | textAlign: 'left', 70 | }, 71 | list: { 72 | width: 250, 73 | }, 74 | fullList: { 75 | width: 'auto', 76 | }, 77 | })); 78 | 79 | const Home = (props) => { 80 | const [step, setStep] = useState(0); 81 | const [list, setList] = useState([]); 82 | const [playerNumber, setPlayerNumber] = useState(6); 83 | const [wolfNumber, setWolfNumber] = useState(2); 84 | const [isUsePredictor, setIsUsePredictor] = useState(false); 85 | const [isUseWitch, setIsUseWitch] = useState(true); 86 | const [isUseHunter, setIsUseHunter] = useState(false); 87 | const [isUseKnight, setIsUseKnight] = useState(false); 88 | const [isUseidiot, setIsUseidiot] = useState(false); 89 | const [isKillKind, setIsKillKind] = useState(false); // 屠邊局判斷 90 | const [isMirror, setIsMirror] = useState(true); // modal 是否開啟鏡像 91 | 92 | const [isOpenDrawer, setIsOpenDrawer] = useState(false); 93 | const { t, i18n } = useTranslation(); 94 | const classes = useStyles(); 95 | 96 | const handleChange = (value) => { 97 | i18n.changeLanguage(value); 98 | } 99 | 100 | const toggleDrawer = (isOpen) => event => { 101 | if (event.type === 'keydown' && (event.key === 'Tab' || event.key === 'Shift')) { 102 | return; 103 | } 104 | setIsOpenDrawer(isOpen); 105 | }; 106 | 107 | const handleRestart = () => { 108 | window.location.reload(); 109 | } 110 | 111 | const getRaddomIndex = () => { 112 | return Math.floor(Math.random() * playerNumber); 113 | } 114 | 115 | const handleStart = () => { 116 | const list = createArray(playerNumber, null); 117 | 118 | // 狼人位置 119 | for (let i = 0 ; i < wolfNumber ; i += 1) { 120 | let index = null; 121 | 122 | beginning: while(true) { 123 | index = getRaddomIndex(); 124 | if(list[index] !== null) { 125 | continue beginning; 126 | } else { 127 | break; 128 | } 129 | } 130 | 131 | list[index] = { 132 | index: index + 1, 133 | role: WOLF, 134 | }; 135 | } 136 | 137 | // 預言家位置 138 | if (isUsePredictor) { 139 | let index = null; 140 | 141 | beginning: while(true) { 142 | index = getRaddomIndex(); 143 | if(list[index] !== null) { 144 | continue beginning; 145 | } else { 146 | break; 147 | } 148 | } 149 | 150 | list[index] = { 151 | index: index + 1, 152 | role: PREDICTOR, 153 | }; 154 | } 155 | 156 | // 女巫位置 157 | if (isUseWitch) { 158 | let index = null; 159 | 160 | beginning: while(true) { 161 | index = getRaddomIndex(); 162 | if(list[index] !== null) { 163 | continue beginning; 164 | } else { 165 | break; 166 | } 167 | } 168 | 169 | list[index] = { 170 | index: index + 1, 171 | role: WITCH, 172 | }; 173 | } 174 | 175 | // 獵人位置 176 | if (isUseHunter) { 177 | let index = null; 178 | 179 | beginning: while(true) { 180 | index = getRaddomIndex(); 181 | if(list[index] !== null) { 182 | continue beginning; 183 | } else { 184 | break; 185 | } 186 | } 187 | 188 | list[index] = { 189 | index: index + 1, 190 | role: HUNTER, 191 | }; 192 | } 193 | 194 | // 騎士位置 195 | if (isUseKnight) { 196 | let index = null; 197 | 198 | beginning: while(true) { 199 | index = getRaddomIndex(); 200 | if(list[index] !== null) { 201 | continue beginning; 202 | } else { 203 | break; 204 | } 205 | } 206 | 207 | list[index] = { 208 | index: index + 1, 209 | role: KNIGHT, 210 | }; 211 | } 212 | 213 | // 白癡位置 214 | if (isUseidiot) { 215 | let index = null; 216 | 217 | beginning: while(true) { 218 | index = getRaddomIndex(); 219 | if(list[index] !== null) { 220 | continue beginning; 221 | } else { 222 | break; 223 | } 224 | } 225 | 226 | list[index] = { 227 | index: index + 1, 228 | role: idiot, 229 | }; 230 | } 231 | 232 | // 村民位置 233 | list.forEach((sit, index) => { 234 | if (sit === null) { 235 | list[index] = { 236 | index: index + 1, 237 | role: VILLAGER, 238 | }; 239 | } 240 | }) 241 | 242 | setList(list); 243 | setStep(1); 244 | } 245 | 246 | const handleStartGame = () => { 247 | setStep(2); 248 | } 249 | 250 | return ( 251 |
252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | { t('title') } 261 | 262 | 275 | 276 | 277 | 286 |
287 | 288 | 289 | 290 | 291 | 292 | 293 | {/*} 294 | 295 | 296 | 297 | 298 | {*/} 299 | 300 | 301 | 302 | { 303 | (step === 0) && ( 304 | 325 | ) 326 | } 327 | { 328 | (step === 1) && ( 329 | 333 | ) 334 | } 335 | { 336 | (step === 2) && ( 337 | 351 | ) 352 | } 353 | 354 | 355 |
356 | ); 357 | } 358 | 359 | export default Home 360 | -------------------------------------------------------------------------------- /src/components/Game/Game.js: -------------------------------------------------------------------------------- 1 | import React, { 2 | useState, 3 | useMemo, 4 | useCallback, 5 | } from 'react'; 6 | import Sound from 'react-sound'; 7 | import Dialog from '@material-ui/core/Dialog'; 8 | import DialogActions from '@material-ui/core/DialogActions'; 9 | import DialogContent from '@material-ui/core/DialogContent'; 10 | import DialogContentText from '@material-ui/core/DialogContentText'; 11 | import DialogTitle from '@material-ui/core/DialogTitle'; 12 | import Button from '@material-ui/core/Button'; 13 | import Avatar from '@material-ui/core/Avatar'; 14 | import Grid from '@material-ui/core/Grid'; 15 | import { makeStyles } from '@material-ui/core/styles'; 16 | import { 17 | pink, 18 | } from '@material-ui/core/colors'; 19 | import Divider from '@material-ui/core/Divider'; 20 | import CheckIcon from '@material-ui/icons/Check'; 21 | import CloseIcon from '@material-ui/icons/Close'; 22 | import FormControlLabel from '@material-ui/core/FormControlLabel'; 23 | import Switch from '@material-ui/core/Switch'; 24 | 25 | import { useTranslation } from "react-i18next"; 26 | 27 | import { 28 | WOLF, 29 | PREDICTOR, 30 | WITCH, 31 | HUNTER, 32 | KNIGHT, 33 | idiot, 34 | VILLAGER, 35 | } from '../../constants/Role'; 36 | 37 | import step1 from '../../static/audio/step_1.mp3'; // 天黑請閉眼 38 | import step2 from '../../static/audio/step_2.mp3'; // 狼人現身請睜眼 39 | import step3 from '../../static/audio/step_3.mp3'; // 狼人確認彼此的身份 40 | import step4 from '../../static/audio/step_4.mp3'; // 狼人請殺人 41 | import step5 from '../../static/audio/step_5.mp3'; // 狼人請閉眼 42 | import step6 from '../../static/audio/step_6.mp3'; // 女巫請睜眼 43 | import step7 from '../../static/audio/step_7.mp3'; // 這位玩家被殺死了 44 | import step8 from '../../static/audio/step_8.mp3'; // 你要使用解藥嗎 45 | import step9 from '../../static/audio/step_9.mp3'; // 你要使用毒藥嗎 46 | import step10 from '../../static/audio/step_10.mp3'; // 你要毒誰呢 47 | import step11 from '../../static/audio/step_11.mp3'; // 女巫請閉眼 48 | import step12 from '../../static/audio/step_12.mp3'; // 預言家請睜眼 49 | import step13 from '../../static/audio/step_13.mp3'; // 你要查驗的對象是 50 | import step14 from '../../static/audio/step_14.mp3'; // 他的身份是 51 | import step15 from '../../static/audio/step_15.mp3'; // 預言家請閉眼 52 | import step16 from '../../static/audio/step_16.mp3'; // 獵人請睜眼 53 | import step17 from '../../static/audio/step_17.mp3'; // 獵人請閉眼 54 | import step18 from '../../static/audio/step_18.mp3'; // 騎士請睜眼 55 | import step19 from '../../static/audio/step_19.mp3'; // 騎士請閉眼 56 | import step20 from '../../static/audio/step_20.mp3'; // 白癡請睜眼 57 | import step21 from '../../static/audio/step_21.mp3'; // 白癡請閉眼 58 | import step22 from '../../static/audio/step_22.mp3'; // 天亮請睜眼 59 | 60 | /** 61 | * IS_DEBUG 62 | * 是否開啟 console.log 資訊 63 | * 64 | */ 65 | const IS_DEBUG = true; 66 | 67 | /** 68 | * DAY_TYPE 69 | * 白天, 晚上 70 | * 71 | */ 72 | const DAY_TYPE = { 73 | DAY: 'DAY', 74 | NIGHT: 'NIGHT', 75 | } 76 | 77 | const AudioSound = React.memo((props) => { 78 | const { 79 | url, 80 | onFinishedPlaying, 81 | } = props; 82 | 83 | return ( 84 | 91 | ); 92 | }); 93 | 94 | const useStyles = makeStyles({ 95 | avatar: { 96 | margin: 7, 97 | color: '#fff', 98 | backgroundColor: '#4DB6AC', 99 | width: '40px', 100 | height: '40px', 101 | }, 102 | pinkAvatar: { 103 | margin: 7, 104 | color: '#fff', 105 | backgroundColor: pink[500], 106 | width: '40px', 107 | height: '40px', 108 | }, 109 | dead: { 110 | margin: 7, 111 | color: '#fff', 112 | backgroundColor: '#9E9E9E', 113 | width: '40px', 114 | height: '40px', 115 | }, 116 | number: { 117 | fontSize: '25px', 118 | }, 119 | good: { 120 | fontSize: '30px', 121 | }, 122 | bad: { 123 | fontSize: '30px', 124 | color: 'red', 125 | }, 126 | // greenAvatar: { 127 | // margin: 10, 128 | // color: '#fff', 129 | // backgroundColor: green[500], 130 | // }, 131 | }); 132 | 133 | const Game = (props) => { 134 | const classes = useStyles(); 135 | const { 136 | list, 137 | isUsePredictor, 138 | isUseWitch, 139 | isUseHunter, 140 | playerNumber, 141 | wolfNumber, 142 | isKillKind, 143 | isUseKnight, 144 | isUseidiot, 145 | isMirror, 146 | } = props; 147 | 148 | if (IS_DEBUG) { 149 | // console.log('list', list); 150 | console.log('===== Role List ====='); 151 | list.forEach(tmp => { console.log(`${tmp.index} - ${tmp.role.key}`); }); 152 | console.log('====================='); 153 | } 154 | 155 | const { t } = useTranslation(); 156 | const [step, setStep] = useState(1); 157 | const [isOpenWolfKill, setIsOpenWolfKill] = useState(false); 158 | const [deadNumber, setDeadNumber] = useState(null); // 狼人晚上殺人的 159 | const [witchDeadNumber, setWitchDeadNumber] = useState(null); // 女巫毒的角色 160 | const [isOpenWitchSave, setIsOpenWitchSave] = useState(false); // 解藥詢問 視窗 161 | const [isOpenWitchPoison, setIsOpenWitchPoison] = useState(false); // 毒藥詢問 視窗 162 | const [isUse, setIsUse] = useState(false); // 女巫一晚只能使用一種藥 163 | const [isUseSave, setIsUseSave] = useState(false); // 是否已使用解藥 164 | const [isUsePoison, setIsUsePoison] = useState(false); // 是否已使用毒藥 165 | const [isOpenPredictor, setIsOpenPredictor] = useState(false); // 預言家選擇身份 視窗 166 | const [predictorSelect, setPredictorSelect] = useState(null); // 預言家選擇查驗的身份 167 | const [isOpenRole, setIsOpenRols] = useState(false); // 預言家查看身份 視窗 168 | const [isOpenResult, setIsOpenResult] = useState(false); // 晚上結果 169 | const [day, setDay] = useState(1); // 第幾天 170 | const [dayType, setDayType] = useState(DAY_TYPE.NIGHT); // 白天晚上 171 | const [messages, setMessages] = useState([]); // 遊戲訊息 172 | const [isOpenVote, setIsOpenVote] = useState(false); // 投票視窗 173 | const [selectVote, setSelectVote] = useState(null); // 選擇投票的人 174 | const [dead, setDead] = useState([]); // 死亡的人 175 | const [isOpenGameResult, setIsOpenGameResult] = useState(false); // 是否開啟遊戲結束畫面 176 | const [gameResultMessage, setGameResultMessage] = useState(''); // 遊戲結束訊息 177 | const [isUseHunterSkill, setIsUseHunterSkill] = useState(false); // 獵人是否已使用技能 178 | const [hunterSelect, setHunterSelect] = useState(null); // 獵人選擇 179 | const [isOpenHunter, setIsOpenHunter] = useState(false); // 是否開啟獵人視窗 180 | const [isKillByWitch, setIsKillByWitch] = useState(false); // 獵人是否被毒殺 181 | const [isOpenHunterShoot, setIsOpenHunterShoot] = useState(false); // 是否開啟獵人槍殺訊息 182 | const [isPredictorDead, setIsPredictorDead] = useState(false); // 預言家是否死亡 183 | const [isWitchDead, setIsWitchDead] = useState(false); // 女巫是否死亡 184 | const [isHunterDead, setIsHunterDead] = useState(false); // 獵人是否死亡 185 | const [isOpenLastWords, setIsOpenLastWords] = useState(false); // 遺言視窗 186 | const [isKnightDead, setIsKnightDead] = useState(false); // 騎士是否死亡 187 | const [isidiotDead, setIsidiotDead] = useState(false); // 白癡是否死亡 188 | 189 | const [isUseKnightSkill, setIsUseKnightSkill] = useState(false); // 騎士是否已實用技能 190 | const [isOpenKnight, setIsOpenKnight] = useState(false); // 開啟騎士選擇對象視窗 191 | const [knightSelect, setKnightSelect] = useState(null); // 騎士選擇 192 | const [isOpenKnightResult, setIsOpenKnightResult] = useState(false); // 騎士驗人結果 193 | const [isOpenidiotResult, setIsOpenidiotResult] = useState(false); // 放逐白癡結果 194 | const [isUseidiotSkill, setIsUseidiotSkill] = useState(false); // 白癡是否使用技能 195 | 196 | const [isShowMessage, setIsShowMessage] = useState(false); // 是否顯示夜晚訊息 197 | 198 | // console.log('isKillByWitch', isKillByWitch); 199 | // console.log('isUsePoison', isUsePoison); 200 | 201 | const handleSongFinishedPlaying = useCallback(() => { 202 | // setStep(step + 1); 203 | switch(step) { 204 | case 1: 205 | setStep(2); 206 | break; 207 | case 2: 208 | setStep(3); 209 | break; 210 | case 3: 211 | setStep(4); 212 | break; 213 | case 4: 214 | // setStep(2); 215 | setIsOpenWolfKill(true); 216 | break; 217 | case 5: 218 | // 是否有使用女巫 219 | if (isUseWitch) { 220 | setStep(6); 221 | } else { 222 | // 是否使用預言家 223 | if (isUsePredictor) { 224 | setStep(12); 225 | } else { 226 | if (isUseHunter) { 227 | // 是否使用獵人 228 | setStep(16); 229 | } else { 230 | if (isUseKnight) { 231 | setStep(18); 232 | } else { 233 | if (isUseidiot) { 234 | setStep(20); 235 | } else { 236 | setStep(22); 237 | } 238 | } 239 | } 240 | } 241 | } 242 | break; 243 | case 6: 244 | setStep(7); 245 | break; 246 | case 7: 247 | if (!isWitchDead) { 248 | setIsOpenWitchSave(true); 249 | } 250 | setStep(8); 251 | break; 252 | case 8: 253 | if (isWitchDead) { 254 | setTimeout(() => { 255 | setStep(9) 256 | }, 2000); 257 | } 258 | break; 259 | case 9: 260 | if (!isWitchDead) { 261 | setIsOpenWitchPoison(true); 262 | } 263 | setStep(10); 264 | break; 265 | case 10: 266 | if (isWitchDead) { 267 | setTimeout(() => { 268 | setStep(11) 269 | }, 2000); 270 | } 271 | break; 272 | case 11: 273 | // 是否使用預言家 274 | if (isUsePredictor) { 275 | setStep(12); 276 | } else { 277 | if (isUseHunter) { 278 | // 是否使用獵人 279 | setStep(16); 280 | } else { 281 | if (isUseKnight) { 282 | setStep(18); 283 | } else { 284 | if (isUseidiot) { 285 | setStep(20); 286 | } else { 287 | setStep(22); 288 | } 289 | } 290 | } 291 | } 292 | // setStep(12); 293 | break; 294 | case 12: 295 | setStep(13); 296 | break; 297 | case 13: 298 | if (!isPredictorDead) { 299 | setIsOpenPredictor(true); 300 | } else { 301 | setTimeout(() => { 302 | setStep(14) 303 | }, 2000); 304 | } 305 | break; 306 | case 14: 307 | // setStep(15); 308 | if (isPredictorDead) { 309 | setTimeout(() => { 310 | setStep(15) 311 | }, 2000); 312 | } 313 | break; 314 | case 15: 315 | if (isUseHunter) { 316 | // 是否使用獵人 317 | setStep(16); 318 | } else { 319 | if (isUseKnight) { 320 | setStep(18); 321 | } else { 322 | if (isUseidiot) { 323 | setStep(20); 324 | } else { 325 | setStep(22); 326 | } 327 | } 328 | } 329 | break; 330 | case 16: 331 | if (!isHunterDead) { 332 | setIsOpenHunterShoot(true); 333 | } else { 334 | setTimeout(() => { 335 | setStep(17) 336 | }, 2000); 337 | } 338 | break; 339 | case 17: 340 | if (isUseKnight) { 341 | setStep(18); 342 | } else { 343 | if (isUseidiot) { 344 | setStep(20); 345 | } else { 346 | setStep(22); 347 | } 348 | } 349 | break; 350 | case 18: 351 | setStep(19); 352 | break; 353 | case 19: 354 | if (isUseidiot) { 355 | setStep(20); 356 | } else { 357 | setStep(22); 358 | } 359 | break; 360 | case 20: 361 | setStep(21); 362 | break; 363 | case 21: 364 | setStep(22); 365 | break; 366 | case 22: 367 | setIsOpenResult(true); 368 | break; 369 | default: 370 | } 371 | }, [step]); 372 | 373 | /** 374 | * handleCloseWolfKill 375 | * 狼人殺人視窗 376 | * 377 | */ 378 | const handleCloseWolfKill = () => { 379 | setIsOpenWolfKill(false); 380 | setStep(5); 381 | } 382 | 383 | /** 384 | * handleWitchSave 385 | * 女巫是否使用解藥 386 | * 387 | * @param {bool} isSave - true: 使用, false: 不使用 388 | */ 389 | const handleWitchSave = (isSave) => { 390 | setIsOpenWitchSave(false); 391 | setStep(9); 392 | 393 | if (isSave) { 394 | // 使用解藥 395 | setTimeout(() => { 396 | setIsUse(true); 397 | setIsUseSave(true); 398 | setDeadNumber(null); 399 | }, 500); 400 | } 401 | } 402 | 403 | /** 404 | * handleWitchPoison 405 | * 關閉女巫是否使用毒藥 406 | * 407 | * @param {bool} isPoison - true: 使用, false: 不使用 408 | */ 409 | const handleWitchPoison = (isPoison) => { 410 | setIsOpenWitchPoison(false); 411 | setStep(11); 412 | 413 | if (!isUsePoison) { 414 | setTimeout(() => { 415 | setIsUsePoison(isPoison); 416 | }, 500); 417 | } 418 | 419 | if (!isPoison) { 420 | setWitchDeadNumber(null); 421 | } else { 422 | // 使用毒藥, 判斷是不是毒到獵人 423 | if (IS_DEBUG) { 424 | console.log('witchDeadNumber.role.key', witchDeadNumber.role.key); 425 | } 426 | if (witchDeadNumber !== null && witchDeadNumber.role.key === HUNTER.key) { 427 | setIsKillByWitch(true); 428 | } 429 | } 430 | } 431 | 432 | /** 433 | * handlePredictor 434 | * 關閉預言家詢問視窗 435 | * 436 | */ 437 | const handlePredictor = () => { 438 | setIsOpenPredictor(false); 439 | setIsOpenRols(true); 440 | setStep(14); 441 | } 442 | 443 | /** 444 | * handleCloseCheckRole 445 | * 關閉預言家查驗身份結果 446 | * 447 | */ 448 | const handleCloseCheckRole = () => { 449 | setIsOpenRols(false); 450 | setStep(15); 451 | } 452 | 453 | /** 454 | * audioSrc 455 | * 取得音效檔 456 | * 457 | */ 458 | const audioSrc = useMemo(() => { 459 | let returnSrc = null; 460 | 461 | switch(step) { 462 | case 1: 463 | returnSrc = step1; 464 | break; 465 | case 2: 466 | returnSrc = step2; 467 | break; 468 | case 3: 469 | returnSrc = step3; 470 | break; 471 | case 4: 472 | returnSrc = step4; 473 | break; 474 | case 5: 475 | returnSrc = step5; 476 | break; 477 | case 6: 478 | returnSrc = step6; 479 | break; 480 | case 7: 481 | returnSrc = step7; 482 | break; 483 | case 8: 484 | returnSrc = step8; 485 | break; 486 | case 9: 487 | returnSrc = step9; 488 | break; 489 | case 10: 490 | returnSrc = step10; 491 | break; 492 | case 11: 493 | returnSrc = step11; 494 | break; 495 | case 12: 496 | returnSrc = step12; 497 | break; 498 | case 13: 499 | returnSrc = step13; 500 | break; 501 | case 14: 502 | returnSrc = step14; 503 | break; 504 | case 15: 505 | returnSrc = step15; 506 | break; 507 | case 16: 508 | returnSrc = step16; 509 | break; 510 | case 17: 511 | returnSrc = step17; 512 | break; 513 | case 18: 514 | returnSrc = step18; 515 | break; 516 | case 19: 517 | returnSrc = step19; 518 | break; 519 | case 20: 520 | returnSrc = step20; 521 | break; 522 | case 21: 523 | returnSrc = step21; 524 | break; 525 | case 22: 526 | returnSrc = step22; 527 | break; 528 | default: 529 | break; 530 | } 531 | 532 | return returnSrc; 533 | }, [step]); 534 | 535 | /** 536 | * generateResultMessage 537 | * 組出當晚死亡訊息 538 | * 539 | */ 540 | const generateResultMessage = () => { 541 | let returnMessage = null; 542 | if (deadNumber === null && witchDeadNumber === null) { 543 | returnMessage = t('christmas_eve'); 544 | return returnMessage; 545 | } else { 546 | let tmp = []; 547 | if (deadNumber !== null) { 548 | tmp.push(deadNumber.index); 549 | } 550 | if (witchDeadNumber !== null && deadNumber !== null && witchDeadNumber.index !== deadNumber.index) { 551 | tmp.push(witchDeadNumber.index); 552 | } 553 | 554 | // 重新排序 555 | tmp.sort((a, b) => { 556 | return a - b; 557 | }); 558 | 559 | // 每晚最多只會有兩位玩家死掉 560 | tmp.forEach((number, index) => { 561 | returnMessage += number; 562 | if (tmp.length === 2 && index !== tmp.length - 1) { 563 | returnMessage += ', '; 564 | } 565 | }); 566 | } 567 | 568 | return t('dead_player', { returnMessage }); 569 | } 570 | 571 | /** 572 | * handleCloseResult 573 | * 關閉晚上結果 574 | * 判斷是否結束遊戲 575 | * 判斷是否有獵人 576 | * 577 | */ 578 | const handleCloseResult = () => { 579 | // 設定成白天 580 | // setDayType(DAY_TYPE.DAY); 581 | 582 | // 關閉晚上結果 583 | setIsOpenResult(false); 584 | 585 | // 更新遊戲訊息 586 | setMessages([ 587 | ...messages, 588 | `${t('n_night', { day })}${generateResultMessage()}` 589 | ]); 590 | 591 | const tmpArray = []; 592 | 593 | // 狼人殺死的人 594 | if (deadNumber !== null) { 595 | tmpArray.push(deadNumber); 596 | // setDead([ 597 | // ...dead, 598 | // deadNumber, 599 | // ]); 600 | } 601 | 602 | // 女巫毒的人 603 | if (isUseWitch && witchDeadNumber !== null && deadNumber.index !== witchDeadNumber.index) { 604 | tmpArray.push(witchDeadNumber); 605 | } 606 | 607 | // 更新已死亡的人 608 | const tmpDead = [ 609 | ...dead, 610 | ...tmpArray, 611 | ]; 612 | if (tmpArray.length > 0) { 613 | setDead(tmpDead); 614 | } 615 | 616 | const result = checkGameFinished(tmpDead); 617 | if (result.isFinished) { 618 | setIsOpenGameResult(true); 619 | setGameResultMessage(result.message); 620 | } else { 621 | // 檢查獵人是否死亡 622 | const isHunter = checkHunter(tmpDead); 623 | 624 | if (isHunter) { 625 | setIsOpenHunter(true); 626 | } else { 627 | setTimeout(() => { 628 | initSelect(false); 629 | }, 500); 630 | } 631 | } 632 | } 633 | 634 | /** 635 | * generateSelectPicker 636 | * 組出選擇頭像 component 637 | * 638 | * @param {string} role - 角色 639 | */ 640 | const generateSelectPicker = (role) => { 641 | let returnComp = null; 642 | let selectValue = null; 643 | let selectFunc = null; 644 | 645 | switch(role) { 646 | // 狼人 647 | case WOLF.key: 648 | selectValue = deadNumber; 649 | selectFunc = setDeadNumber; 650 | break; 651 | // 女巫 652 | case WITCH.key: 653 | selectValue = witchDeadNumber; 654 | selectFunc = setWitchDeadNumber; 655 | break; 656 | // 預言家 657 | case PREDICTOR.key: 658 | selectValue = predictorSelect; 659 | selectFunc = setPredictorSelect; 660 | break; 661 | // 獵人 662 | case HUNTER.key: 663 | selectValue = hunterSelect; 664 | selectFunc = setHunterSelect; 665 | break; 666 | // 騎士 667 | case KNIGHT.key: 668 | selectValue = knightSelect; 669 | selectFunc = setKnightSelect; 670 | break; 671 | // 一般投票 672 | default: 673 | selectValue = selectVote; 674 | selectFunc = setSelectVote; 675 | } 676 | 677 | returnComp = ( 678 | 679 | { 680 | list.map(sit => { 681 | let className = classes.avatar; 682 | 683 | if (selectValue) { 684 | if (selectValue.index === sit.index) { 685 | className = classes.pinkAvatar; 686 | }; 687 | } 688 | 689 | // 判斷該玩家是否死亡 690 | const idDead = dead.some(tmp => tmp.index === sit.index); 691 | 692 | return ( 693 | <> 694 | { 695 | (idDead) ? ( 696 | 697 | 698 | { sit.index } 699 | 700 | 701 | ) : ( 702 | {selectFunc(sit)}}> 703 | 704 | { sit.index } 705 | 706 | 707 | ) 708 | } 709 | 710 | ); 711 | }) 712 | } 713 | 714 | ); 715 | 716 | return returnComp; 717 | } 718 | 719 | /** 720 | * handleVote 721 | * 投票結果 722 | * 723 | * @param {bool} isVote - 是否有投票, false: 放棄 724 | */ 725 | const handleVote = (isVote) => { 726 | setDayType(DAY_TYPE.DAY); 727 | 728 | // 關閉投票視窗 729 | setIsOpenVote(false); 730 | 731 | if (isVote) { 732 | // 判斷被放逐的是不是白癡 733 | if (selectVote.role.key === idiot.key && isUseidiotSkill === false) { 734 | setMessages([ 735 | ...messages, 736 | `${t('n_day', { day })}${t('no_is_idiot', { index: selectVote.index })}, ${t('idiot_result')}`, 737 | ]); 738 | 739 | setIsUseidiotSkill(true); 740 | setIsOpenidiotResult(true); 741 | } else { 742 | const tmpDead = [ 743 | ...dead, 744 | selectVote, 745 | ] 746 | if (isVote) { 747 | setDead(tmpDead); 748 | 749 | setMessages([ 750 | ...messages, 751 | `${t('n_day', { day })}${selectVote.index}`, 752 | ]); 753 | } else { 754 | setMessages([ 755 | ...messages, 756 | `${t('n_day', { day })}${t('give_up_vote')}`, 757 | ]); 758 | } 759 | 760 | const result = checkGameFinished(tmpDead); 761 | if (result.isFinished) { 762 | setIsOpenGameResult(true); 763 | setGameResultMessage(result.message); 764 | } else { 765 | // 檢查獵人是否死亡 766 | const isHunter = checkHunter(tmpDead); 767 | 768 | if (isHunter) { 769 | setIsOpenHunter(true); 770 | } else { 771 | // initSelect(true); 772 | setIsOpenLastWords(true); 773 | } 774 | } 775 | } 776 | } else { 777 | // initSelect(true); 778 | setIsOpenLastWords(true); 779 | } 780 | } 781 | 782 | /** 783 | * checkHunter 784 | * 檢查獵人是否死亡 785 | * 786 | * @param {array} dead - 死掉的人 787 | */ 788 | const checkHunter = (dead) => { 789 | let isHunter = false; 790 | 791 | // 有使用獵人並未發動技能 792 | // console.log('isUseHunter', isUseHunter); 793 | // console.log('isUseHunterSkill', isUseHunterSkill); 794 | // console.log('isKillByWitch', isKillByWitch); 795 | if (isUseHunter && !isUseHunterSkill && !isKillByWitch && !isHunterDead) { 796 | isHunter = dead.some(tmp => tmp.role.key === HUNTER.key); 797 | } 798 | // console.log('isHunter', isHunter); 799 | return isHunter; 800 | } 801 | 802 | /** 803 | * checkGameFinished 804 | * 檢查遊戲是否結束 805 | * 806 | * @param {array} - 死亡的人 807 | */ 808 | const checkGameFinished = (dead) => { 809 | if (isUsePredictor) { 810 | if (dead.some(tmp => tmp.role.key === PREDICTOR.key)) { 811 | setIsPredictorDead(true); 812 | } 813 | } 814 | 815 | if (isUseWitch) { 816 | if (dead.some(tmp => tmp.role.key === WITCH.key)) { 817 | setIsWitchDead(true); 818 | } 819 | } 820 | 821 | if (isUseHunter) { 822 | if (dead.some(tmp => tmp.role.key === HUNTER.key)) { 823 | setIsHunterDead(true); 824 | } 825 | } 826 | 827 | if (isUseKnight) { 828 | if (dead.some(tmp => tmp.role.key === KNIGHT.key)) { 829 | setIsKnightDead(true); 830 | } 831 | } 832 | 833 | if (isUseidiot) { 834 | if (dead.some(tmp => tmp.role.key === idiot.key)) { 835 | setIsidiotDead(true); 836 | } 837 | } 838 | 839 | if (IS_DEBUG) { 840 | console.log('dead', dead); 841 | console.log('wolfNumber', wolfNumber); 842 | } 843 | 844 | // 判斷好人是否獲勝 845 | let deadWolf = 0; 846 | dead.forEach((dead) => { 847 | if (dead.role.key === WOLF.key) { 848 | deadWolf += 1; 849 | } 850 | }); 851 | 852 | if (IS_DEBUG) { 853 | console.log('deadWolf', deadWolf); 854 | } 855 | 856 | if (deadWolf === wolfNumber) { 857 | return { 858 | isFinished: true, 859 | message: t('good_win'), 860 | } 861 | } 862 | 863 | // 判斷壞人是否獲勝 864 | if (IS_DEBUG) { 865 | console.log('playerNumber', playerNumber); 866 | } 867 | 868 | if ((playerNumber - dead.length) === (wolfNumber - deadWolf)) { 869 | return { 870 | isFinished: true, 871 | message: t('bad_win'), 872 | } 873 | } 874 | 875 | // if ((playerNumber - dead.length) <= (wolfNumber - deadWolf) * 2) { 876 | // return { 877 | // isFinished: true, 878 | // message: t('bad_win'), 879 | // } 880 | // } 881 | 882 | // 判斷是否有屠邊局, 壞人是否屠邊成功 883 | if (IS_DEBUG) { 884 | console.log('isKillKind', isKillKind); 885 | } 886 | if (isKillKind) { 887 | let gods = 0; // 神 888 | let villagers = 0; // 民 889 | let deadGods = 0; // 死掉的神 890 | let deadVillagers = 0; // 死掉的民 891 | 892 | // 玩家 893 | list.forEach((tmp) => { 894 | if (tmp.role.isGood) { 895 | if (tmp.role.isGod) { 896 | gods += 1; 897 | } else { 898 | villagers += 1; 899 | } 900 | } 901 | }); 902 | 903 | // 死掉的人 904 | dead.forEach((tmp) => { 905 | if (tmp.role.isGood) { 906 | if (tmp.role.isGod) { 907 | deadGods += 1; 908 | } else { 909 | deadVillagers += 1; 910 | } 911 | } 912 | }); 913 | 914 | if (IS_DEBUG) { 915 | console.log('gods', gods); 916 | console.log('villagers', villagers); 917 | console.log('deadGods', deadGods); 918 | console.log('deadVillagers', deadVillagers); 919 | } 920 | 921 | if ((deadGods !== 0 && gods === deadGods) || (deadVillagers !== 0 && villagers === deadVillagers)) { 922 | return { 923 | isFinished: true, 924 | message: t('bad_win_kind'), 925 | } 926 | } 927 | } 928 | 929 | return { 930 | isFinished: false, 931 | message: '', 932 | } 933 | } 934 | 935 | /** 936 | * handleShoot 937 | * 獵人射殺 938 | * 939 | * @param {bool} isShoot - 是否射殺 940 | */ 941 | const handleShoot = (isShoot) => { 942 | setIsOpenHunter(false); 943 | setIsUseHunterSkill(true); 944 | // console.log('isShoot', isShoot); 945 | if (isShoot) { 946 | const tmpDead = [ 947 | ...dead, 948 | hunterSelect, 949 | ]; 950 | 951 | setDead(tmpDead); 952 | setMessages([ 953 | ...messages, 954 | t('hunter_shoot_player', { index: hunterSelect.index }) 955 | ]); 956 | const result = checkGameFinished(tmpDead); 957 | 958 | if (result.isFinished) { 959 | setIsOpenGameResult(true); 960 | setGameResultMessage(result.message); 961 | } else { 962 | // console.log('11', dayType); 963 | if (dayType === DAY_TYPE.DAY) { 964 | initSelect(true); 965 | } else { 966 | initSelect(false); 967 | } 968 | } 969 | } else { 970 | // console.log('22', dayType); 971 | if (dayType === DAY_TYPE.DAY) { 972 | initSelect(true); 973 | } else { 974 | initSelect(false); 975 | } 976 | } 977 | } 978 | 979 | /** 980 | * initSelect 981 | * 初始化選擇 982 | * 983 | * @param {bool} isNextDay - 是否進入下一天 984 | */ 985 | const initSelect = (isNextDay) => { 986 | // 清空全部選擇 987 | setDeadNumber(null); // 狼人 988 | setWitchDeadNumber(null); // 女巫毒殺 989 | setPredictorSelect(null); // 預言家選擇 990 | setSelectVote(null); // 投票選擇 991 | setIsUse(false); 992 | setHunterSelect(null); // 獵人選擇 993 | setKnightSelect(null); // 騎士選擇 994 | setIsShowMessage(false); 995 | 996 | if (isNextDay) { 997 | // 進入下一天 998 | setDay(day + 1); 999 | 1000 | // 進入晚上 1001 | setDayType(DAY_TYPE.NIGHT); 1002 | 1003 | // 進入 Step 1 1004 | setStep(1); 1005 | } else { 1006 | setDayType(DAY_TYPE.DAY); 1007 | } 1008 | } 1009 | 1010 | /** 1011 | * handleGameOver 1012 | * 遊戲結束 1013 | * 1014 | */ 1015 | const handleGameOver = () => { 1016 | // window.location.href = '/'; 1017 | window.location.reload(); 1018 | } 1019 | 1020 | /** 1021 | * handleCloseHunter 1022 | * 關閉獵人視窗 1023 | * 1024 | */ 1025 | const handleCloseHunter = () => { 1026 | setIsOpenHunterShoot(false); 1027 | setStep(17); 1028 | } 1029 | 1030 | const getWolfs = () => { 1031 | //list.filter(role => role.role.key === WOLF.key) 1032 | let wolfsInfo = ''; 1033 | const wolfs = list.filter(role => role.role.key === WOLF.key); 1034 | wolfs.forEach((tmp, index) => { 1035 | wolfsInfo += tmp.index; 1036 | if (wolfs.length - 1 !== index) { 1037 | wolfsInfo += ', '; 1038 | } 1039 | }); 1040 | return wolfsInfo; 1041 | }; 1042 | 1043 | const getVillages = () => { 1044 | let villagesInfo = ''; 1045 | const villages = list.filter(role => role.role.key === VILLAGER.key); 1046 | villages.forEach((tmp, index) => { 1047 | villagesInfo += tmp.index; 1048 | if (villages.length - 1 !== index) { 1049 | villagesInfo += ', '; 1050 | } 1051 | }); 1052 | return villagesInfo; 1053 | }; 1054 | 1055 | /** 1056 | * handleCloseLastWords 1057 | * 關閉遺言視窗 1058 | * 1059 | */ 1060 | const handleCloseLastWords = () => { 1061 | setIsOpenLastWords(false); 1062 | initSelect(true); 1063 | }; 1064 | 1065 | /** 1066 | * handleFight 1067 | * 決鬥結果 1068 | * 1069 | */ 1070 | const handleFight = () => { 1071 | setIsOpenKnight(false); 1072 | setIsOpenKnightResult(true); 1073 | } 1074 | 1075 | /** 1076 | * handleCloseFight 1077 | * 關閉決鬥結果 1078 | * 1079 | */ 1080 | const handleCloseFight = () => { 1081 | setIsUseKnightSkill(true); 1082 | setIsOpenKnightResult(false); 1083 | let tmpDead = []; 1084 | 1085 | if (knightSelect !== null && knightSelect.role.key === WOLF.key) { 1086 | tmpDead = [ 1087 | ...dead, 1088 | knightSelect, 1089 | ]; 1090 | 1091 | setMessages([ 1092 | ...messages, 1093 | t('no_is_wolf', { index: knightSelect.index }) 1094 | ]); 1095 | } else { 1096 | tmpDead = [ 1097 | ...dead, 1098 | list.find(tmp => tmp.role.key === KNIGHT.key), 1099 | ]; 1100 | 1101 | setMessages([ 1102 | ...messages, 1103 | t('no_is_not_wolf', { index: knightSelect.index }) 1104 | ]); 1105 | } 1106 | 1107 | if (IS_DEBUG) { 1108 | console.log('handleCloseFight tmpDead', tmpDead); 1109 | } 1110 | 1111 | setDead(tmpDead); 1112 | const result = checkGameFinished(tmpDead); 1113 | 1114 | if (result.isFinished) { 1115 | setIsOpenGameResult(true); 1116 | setGameResultMessage(result.message); 1117 | } else { 1118 | initSelect(true); 1119 | } 1120 | } 1121 | 1122 | /** 1123 | * handleCloseidiot 1124 | * 關閉白癡結果 1125 | * 1126 | */ 1127 | const handleCloseidiot =() => { 1128 | setIsOpenidiotResult(false); 1129 | initSelect(true); 1130 | } 1131 | 1132 | /** 1133 | * React - render 1134 | * 1135 | */ 1136 | return ( 1137 | <> 1138 |
1139 | { t('gaming') } 1140 |
1141 | 1142 |
1143 | { t('dead_message') } 1144 |
1145 | 1146 |
1147 |
    1148 | { 1149 | messages.map(message =>
  • { message }
  • ) 1150 | } 1151 |
1152 |
1153 | 1154 |
1155 | { 1156 | (dayType === DAY_TYPE.DAY) && ( 1157 | <> 1158 | 1159 | 1167 | 1168 | 1169 | 1170 | ) 1171 | } 1172 |
1173 | 1174 | 1178 | 1179 | { /* Wolf Kill Start */ } 1180 | 1187 | { 1188 | (isMirror) && ( 1189 |
1190 | {t('wolf_kill')} 1191 | 1192 | 1193 | { generateSelectPicker(WOLF.key) } 1194 | 1195 | 1196 | 1197 | 1201 | 1202 |
1203 | ) 1204 | } 1205 |
1206 | {t('wolf_kill')} 1207 | 1208 | 1209 | { generateSelectPicker(WOLF.key) } 1210 | 1211 | 1212 | 1213 | 1217 | 1218 |
1219 |
1220 | { /* Wolf Kill End */ } 1221 | 1222 | { /* Witch Save Start */ } 1223 | 1230 | { 1231 | (isMirror) && ( 1232 |
1233 | {t('witch_save')} 1234 | 1235 | 1236 | { 1237 | (isUseSave) ? ( 1238 | {t('save_used')} 1239 | ) : ( 1240 | 1241 | { t('dead_person', { index: (deadNumber) ? deadNumber.index : null }) } 1242 | 1243 | ) 1244 | } 1245 | 1246 | 1247 | 1248 | 1252 | 1256 | 1257 |
1258 | ) 1259 | } 1260 |
1261 | {t('witch_save')} 1262 | 1263 | 1264 | { 1265 | (isUseSave) ? ( 1266 | {t('save_used')} 1267 | ) : ( 1268 | 1269 | { t('dead_person', { index: (deadNumber) ? deadNumber.index : null }) } 1270 | 1271 | ) 1272 | } 1273 | 1274 | 1275 | 1276 | 1280 | 1284 | 1285 |
1286 |
1287 | { /* Witch Save End */ } 1288 | 1289 | {/* Witch Poison Start */} 1290 | 1296 | { 1297 | (isMirror) && ( 1298 |
1299 | {t('witch_poison')} 1300 | 1301 | 1302 | { 1303 | (isUsePoison) ? ( 1304 | // 你已使用毒藥 1305 | {t('poison_used')} 1306 | ) : ( 1307 | (isUse) ? ( 1308 | // 此回合已使用解藥, 不能使用毒藥 1309 | {t('is_use_save')} 1310 | ) : ( 1311 | generateSelectPicker(WITCH.key) 1312 | ) 1313 | ) 1314 | } 1315 | 1316 | 1317 | 1318 | 1322 | 1326 | 1327 |
1328 | ) 1329 | } 1330 |
1331 | {t('witch_poison')} 1332 | 1333 | 1334 | { 1335 | (isUsePoison) ? ( 1336 | // 你已使用毒藥 1337 | {t('poison_used')} 1338 | ) : ( 1339 | (isUse) ? ( 1340 | // 此回合已使用解藥, 不能使用毒藥 1341 | {t('is_use_save')} 1342 | ) : ( 1343 | generateSelectPicker(WITCH.key) 1344 | ) 1345 | ) 1346 | } 1347 | 1348 | 1349 | 1350 | 1354 | 1358 | 1359 |
1360 |
1361 | {/* Witch Poison End */} 1362 | 1363 | {/* Predictor Start */} 1364 | 1370 | { 1371 | (isMirror) && ( 1372 |
1373 | {t('predictor_select')} 1374 | 1375 | 1376 | { generateSelectPicker(PREDICTOR.key) } 1377 | 1378 | 1379 | 1380 | 1384 | 1385 |
1386 | ) 1387 | } 1388 |
1389 | {t('predictor_select')} 1390 | 1391 | 1392 | { generateSelectPicker(PREDICTOR.key) } 1393 | 1394 | 1395 | 1396 | 1400 | 1401 |
1402 |
1403 | {/* Predictor End */} 1404 | 1405 | {/* Check Role Start */} 1406 | 1412 | { 1413 | (isMirror) && ( 1414 |
1415 | {t('role_result')} 1416 | 1417 | 1418 | { 1419 | (predictorSelect && predictorSelect.role.key === 'wolf') ? ( 1420 | {t('is_wolf')} 1421 | ) : ( 1422 | {t('not_wolf')} 1423 | ) 1424 | } 1425 | 1426 | 1427 | 1428 | 1432 | 1433 |
1434 | ) 1435 | } 1436 |
1437 | {t('role_result')} 1438 | 1439 | 1440 | { 1441 | (predictorSelect && predictorSelect.role.key === 'wolf') ? ( 1442 | {t('is_wolf')} 1443 | ) : ( 1444 | {t('not_wolf')} 1445 | ) 1446 | } 1447 | 1448 | 1449 | 1450 | 1454 | 1455 |
1456 |
1457 | {/* Check Role End */} 1458 | 1459 | {/* Result Start*/} 1460 | 1466 | { 1467 | (isMirror) && ( 1468 |
1469 | {t('yesterday_dead')} 1470 | 1471 | { 1476 | setIsShowMessage(e.target.checked); 1477 | }} 1478 | /> 1479 | } 1480 | label={t('is_show_message')} 1481 | /> 1482 | { 1483 | (isShowMessage) && ( 1484 | 1485 | 1486 | { generateResultMessage() } 1487 | 1488 | 1489 | ) 1490 | } 1491 | 1492 | 1493 | 1497 | 1498 |
1499 | ) 1500 | } 1501 |
1502 | {t('yesterday_dead')} 1503 | 1504 | { 1509 | setIsShowMessage(e.target.checked); 1510 | }} 1511 | /> 1512 | } 1513 | label={t('is_show_message')} 1514 | /> 1515 | { 1516 | (isShowMessage) && ( 1517 | 1518 | 1519 | { generateResultMessage() } 1520 | 1521 | 1522 | ) 1523 | } 1524 | 1525 | 1526 | 1530 | 1531 |
1532 |
1533 | {/* Result End*/} 1534 | 1535 | { /* Vote Start */ } 1536 | 1542 | { 1543 | (isMirror) && ( 1544 |
1545 | {t('start_vote')} 1546 | 1547 | 1548 | { generateSelectPicker('') } 1549 | 1550 | 1551 | 1552 | 1556 | 1560 | 1561 |
1562 | ) 1563 | } 1564 |
1565 | {t('start_vote')} 1566 | 1567 | 1568 | { generateSelectPicker('') } 1569 | 1570 | 1571 | 1572 | 1576 | 1580 | 1581 |
1582 |
1583 | { /* Vote End */ } 1584 | 1585 | { /* Game Result Start */ } 1586 | 1592 | { 1593 | (isMirror) && ( 1594 |
1595 | {t('game_over')} 1596 | 1597 | 1598 | { 1599 | {gameResultMessage} 1600 | } 1601 |
    1602 |
  • {`${t('wolf')}: ${getWolfs()}`}
  • 1603 | { 1604 | (isUsePredictor) && (
  • {`${t('predictor')}: ${list.find(role => role.role.key === PREDICTOR.key).index}`}
  • ) 1605 | } 1606 | { 1607 | (isUseWitch) && (
  • {`${t('witch')}: ${list.find(role => role.role.key === WITCH.key).index}`}
  • ) 1608 | } 1609 | { 1610 | (isUseHunter) && (
  • {`${t('hunter')}: ${list.find(role => role.role.key === HUNTER.key).index}`}
  • ) 1611 | } 1612 | { 1613 | (isUseKnight) && (
  • {`${t('knight')}: ${list.find(role => role.role.key === KNIGHT.key).index}`}
  • ) 1614 | } 1615 | { 1616 | (isUseidiot) && (
  • {`${t('idiot')}: ${list.find(role => role.role.key === idiot.key).index}`}
  • ) 1617 | } 1618 |
  • {`${t('villager')}: ${getVillages()}`}
  • 1619 |
1620 |
1621 |
1622 | 1623 | 1627 | 1628 |
1629 | ) 1630 | } 1631 |
1632 | {t('game_over')} 1633 | 1634 | 1635 | { 1636 | {gameResultMessage} 1637 | } 1638 |
    1639 |
  • {`${t('wolf')}: ${getWolfs()}`}
  • 1640 | { 1641 | (isUsePredictor) && (
  • {`${t('predictor')}: ${list.find(role => role.role.key === PREDICTOR.key).index}`}
  • ) 1642 | } 1643 | { 1644 | (isUseWitch) && (
  • {`${t('witch')}: ${list.find(role => role.role.key === WITCH.key).index}`}
  • ) 1645 | } 1646 | { 1647 | (isUseHunter) && (
  • {`${t('hunter')}: ${list.find(role => role.role.key === HUNTER.key).index}`}
  • ) 1648 | } 1649 | { 1650 | (isUseKnight) && (
  • {`${t('knight')}: ${list.find(role => role.role.key === KNIGHT.key).index}`}
  • ) 1651 | } 1652 | { 1653 | (isUseidiot) && (
  • {`${t('idiot')}: ${list.find(role => role.role.key === idiot.key).index}`}
  • ) 1654 | } 1655 |
  • {`${t('villager')}: ${getVillages()}`}
  • 1656 |
1657 |
1658 |
1659 | 1660 | 1664 | 1665 |
1666 |
1667 | { /* Game Result End */ } 1668 | 1669 | { /* Hunter Select Start */ } 1670 | 1676 | { 1677 | (isMirror) && ( 1678 |
1679 | {t('hunter_shoot')} 1680 | 1681 | 1682 | { generateSelectPicker(HUNTER.key) } 1683 | 1684 | 1685 | 1686 | 1690 | 1694 | 1695 |
1696 | ) 1697 | } 1698 |
1699 | {t('hunter_shoot')} 1700 | 1701 | 1702 | { generateSelectPicker(HUNTER.key) } 1703 | 1704 | 1705 | 1706 | 1710 | 1714 | 1715 |
1716 |
1717 | { /* Hunter Select End */ } 1718 | 1719 | {/* Hunter Could Shoot Start */} 1720 | 1726 | { 1727 | (isMirror) && ( 1728 |
1729 | {t('could_shoot')} 1730 | 1731 | 1732 | { 1733 | (isKillByWitch) ? ( 1734 | {t('cant_shoot')} 1735 | ) : ( 1736 | {t('can_shoot')} 1737 | ) 1738 | } 1739 | 1740 | 1741 | 1742 | 1746 | 1747 |
1748 | ) 1749 | } 1750 |
1751 | {t('could_shoot')} 1752 | 1753 | 1754 | { 1755 | (isKillByWitch) ? ( 1756 | {t('cant_shoot')} 1757 | ) : ( 1758 | {t('can_shoot')} 1759 | ) 1760 | } 1761 | 1762 | 1763 | 1764 | 1768 | 1769 |
1770 |
1771 | {/* Hunter Could Shoot End */} 1772 | 1773 | {/* Last Words Start */} 1774 | 1780 | { 1781 | (isMirror) && ( 1782 |
1783 | {t('last_words')} 1784 | 1785 | 1786 | ... 1787 | 1788 | 1789 | 1790 | 1793 | 1794 |
1795 | ) 1796 | } 1797 |
1798 | {t('last_words')} 1799 | 1800 | 1801 | ... 1802 | 1803 | 1804 | 1805 | 1808 | 1809 |
1810 |
1811 | {/* Last Words End */} 1812 | 1813 | {/* Knight Start */} 1814 | 1820 | { 1821 | (isMirror) && ( 1822 |
1823 | {t('knight_fight')} 1824 | 1825 | 1826 | { 1827 | (isUseKnight) && (
  • {`${t('knight')}: ${list.find(role => role.role.key === KNIGHT.key).index}`}
  • ) 1828 | } 1829 | { generateSelectPicker(KNIGHT.key) } 1830 |
    1831 |
    1832 | 1833 | 1837 | 1838 |
    1839 | ) 1840 | } 1841 |
    1842 | {t('knight_fight')} 1843 | 1844 | 1845 | { 1846 | (isUseKnight) && (
  • {`${t('knight')}: ${list.find(role => role.role.key === KNIGHT.key).index}`}
  • ) 1847 | } 1848 | { generateSelectPicker(KNIGHT.key) } 1849 |
    1850 |
    1851 | 1852 | 1856 | 1857 |
    1858 |
    1859 | {/* Knight End */} 1860 | 1861 | {/* Knight Result Start */} 1862 | 1868 | { 1869 | (isMirror) && ( 1870 |
    1871 | {t('fight_result')} 1872 | 1873 | 1874 | { 1875 | (knightSelect !== null) ? ( 1876 | (knightSelect.role.key === WOLF.key) ? ( 1877 | {t('no_is_wolf', { index: knightSelect.index })} 1878 | ) : ( 1879 | {t('no_is_not_wolf', { index: knightSelect.index })} 1880 | ) 1881 | ) : ( 1882 | null 1883 | ) 1884 | } 1885 | 1886 | 1887 | 1888 | 1892 | 1893 |
    1894 | ) 1895 | } 1896 |
    1897 | {t('fight_result')} 1898 | 1899 | 1900 | { 1901 | (knightSelect !== null) ? ( 1902 | (knightSelect.role.key === WOLF.key) ? ( 1903 | {t('no_is_wolf', { index: knightSelect.index })} 1904 | ) : ( 1905 | {t('no_is_not_wolf', { index: knightSelect.index })} 1906 | ) 1907 | ) : ( 1908 | null 1909 | ) 1910 | } 1911 | 1912 | 1913 | 1914 | 1918 | 1919 |
    1920 |
    1921 | {/* Knight Result End */} 1922 | 1923 | {/* idiot Result Start */} 1924 | 1930 | { 1931 | (isMirror) && ( 1932 |
    1933 | {t('idiot_result')} 1934 | 1935 | 1936 | { 1937 | (selectVote !== null) ? ( 1938 | t('no_is_idiot', { index: selectVote.index }) 1939 | ) : ( 1940 | null 1941 | ) 1942 | } 1943 | 1944 | 1945 | 1946 | 1950 | 1951 |
    1952 | ) 1953 | } 1954 |
    1955 | {t('idiot_result')} 1956 | 1957 | 1958 | { 1959 | (selectVote !== null) ? ( 1960 | t('no_is_idiot', { index: selectVote.index }) 1961 | ) : ( 1962 | null 1963 | ) 1964 | } 1965 | 1966 | 1967 | 1968 | 1972 | 1973 |
    1974 |
    1975 | {/* idiot Result End */} 1976 | 1977 | ); 1978 | }; 1979 | 1980 | export default Game; 1981 | --------------------------------------------------------------------------------