Showing preview only (212K chars total). Download the full file or copy to clipboard to get everything.
Repository: xiong35/werewolf
Branch: main
Commit: 26a77c0f7e4b
Files: 134
Total size: 169.3 KB
Directory structure:
gitextract_v_63r64i/
├── .vscode/
│ └── settings.json
├── LICENSE
├── README.md
├── docs/
│ ├── 笔记.md
│ └── 需求文档.md
├── reload.sh
├── werewolf-backend/
│ ├── .eslintrc
│ ├── .gitignore
│ ├── .vscode/
│ │ └── be.code-snippets
│ ├── ecosystem.config.js
│ ├── nodemon.json
│ ├── package.json
│ ├── src/
│ │ ├── handlers/
│ │ │ └── http/
│ │ │ ├── gameAct.ts
│ │ │ ├── gameActHandlers/
│ │ │ │ ├── BeforeDayDiscuss.ts
│ │ │ │ ├── DayDiscuss.ts
│ │ │ │ ├── ExileVote.ts
│ │ │ │ ├── ExileVoteCheck.ts
│ │ │ │ ├── GuardProtect.ts
│ │ │ │ ├── HunterCheck.ts
│ │ │ │ ├── HunterShoot.ts
│ │ │ │ ├── LeaveMsg.ts
│ │ │ │ ├── SeerCheck.ts
│ │ │ │ ├── SheriffAssign.ts
│ │ │ │ ├── SheriffAssignCheck.ts
│ │ │ │ ├── SheriffElect.ts
│ │ │ │ ├── SheriffSpeach.ts
│ │ │ │ ├── SheriffVote.ts
│ │ │ │ ├── SheriffVoteCheck.ts
│ │ │ │ ├── WitchAct.ts
│ │ │ │ ├── WolfKill.ts
│ │ │ │ ├── WolfKillCheck.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── validateIdentity.ts
│ │ │ ├── gameBegin.ts
│ │ │ ├── gameGetHint/
│ │ │ │ ├── getWolfs.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── witchGetDie.ts
│ │ │ │ └── wolfKill.ts
│ │ │ ├── gameStatus.ts
│ │ │ ├── roomCreate.ts
│ │ │ ├── roomInit.ts
│ │ │ └── roomJoin.ts
│ │ ├── index.ts
│ │ ├── middleware/
│ │ │ ├── auth.ts
│ │ │ └── handleError.ts
│ │ ├── models/
│ │ │ ├── PlayerModel.ts
│ │ │ └── RoomModel.ts
│ │ ├── routes/
│ │ │ ├── gameRoutes.ts
│ │ │ ├── index.ts
│ │ │ └── roomRoutes.ts
│ │ ├── utils/
│ │ │ ├── checkGameOver.ts
│ │ │ ├── getVoteResult.ts
│ │ │ └── renderHintNPlayers.ts
│ │ └── ws/
│ │ └── index.ts
│ └── tsconfig.json
└── werewolf-frontend/
├── .eslintrc.js
├── .gitignore
├── .vscode/
│ └── settings.json
├── README.md
├── index.html
├── package.json
├── shared/
│ ├── .vscode/
│ │ ├── WS Msg Model.code-snippets
│ │ └── http Msg Model.code-snippets
│ ├── GameDefs.ts
│ ├── ModelDefs.ts
│ ├── WSEvents.ts
│ ├── WSMsg/
│ │ ├── ChangeStatus.ts
│ │ ├── GameEnd.ts
│ │ ├── RoomExile.ts
│ │ ├── RoomJoin.ts
│ │ └── ShowMsg.ts
│ ├── constants.ts
│ └── httpMsg/
│ ├── CharacterAct.ts
│ ├── CreateRoomMsg.ts
│ ├── GameStatusMsg.ts
│ ├── InitRoomMsg.ts
│ ├── JoinRoomMsg.ts
│ ├── SeerCheckMsg.ts
│ └── _httpResTemplate.ts
├── src/
│ ├── App.vue
│ ├── components/
│ │ ├── Avatar.vue
│ │ ├── Btn.vue
│ │ ├── Dialog.vue
│ │ ├── PlayActions/
│ │ │ ├── ActionBtn.vue
│ │ │ ├── commonAction.ts
│ │ │ ├── index.vue
│ │ │ └── renderActionList.ts
│ │ ├── PlayBottomActions.vue
│ │ ├── PlayCharacter.vue
│ │ ├── PlayEventList.vue
│ │ ├── PlayEventTile.vue
│ │ ├── PlayEvents.vue
│ │ ├── PlayMemo.vue
│ │ ├── RoomCharacterTile.vue
│ │ ├── RoomPlayerList.vue
│ │ ├── UseBorder.vue
│ │ └── UseMenu.vue
│ ├── http/
│ │ ├── _request.ts
│ │ ├── action.ts
│ │ ├── gameGetHint.ts
│ │ ├── gameStatus.ts
│ │ └── room.ts
│ ├── index.css
│ ├── main.js
│ ├── normalize.css
│ ├── pages/
│ │ ├── CreateRoom.vue
│ │ ├── Home.vue
│ │ ├── JoinRoom.vue
│ │ ├── Play.vue
│ │ ├── Review.vue
│ │ ├── ReviewDetail.vue
│ │ └── WaitRoom.vue
│ ├── reactivity/
│ │ ├── computeGameEvents.ts
│ │ ├── createRoom.ts
│ │ ├── dialog.ts
│ │ ├── game.ts
│ │ ├── joinRoom.ts
│ │ ├── playAction.ts
│ │ ├── playPage.ts
│ │ ├── record.ts
│ │ └── theme.ts
│ ├── router.ts
│ ├── socket/
│ │ ├── changeStatus.ts
│ │ ├── gameBegin.ts
│ │ ├── gameEnd.ts
│ │ ├── index.ts
│ │ ├── roomJoin.ts
│ │ └── showWSMsg.ts
│ ├── utils/
│ │ ├── setObj.ts
│ │ ├── token.ts
│ │ └── votes.ts
│ └── werewolf.d.ts
├── tsconfig.json
└── vite.config.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .vscode/settings.json
================================================
{
"workbench.colorCustomizations": {
"activityBar.activeBackground": "#73dd26",
"activityBar.activeBorder": "#3e83e1",
"activityBar.background": "#73dd26",
"activityBar.foreground": "#15202b",
"activityBar.inactiveForeground": "#15202b99",
"activityBarBadge.background": "#3e83e1",
"activityBarBadge.foreground": "#e7e7e7",
"statusBar.background": "#5cb41c",
"statusBar.border": "#5cb41c",
"statusBar.foreground": "#15202b",
"statusBarItem.hoverBackground": "#458815",
"tab.activeBorder": "#73dd26",
"titleBar.activeBackground": "#5cb41c",
"titleBar.activeForeground": "#15202b",
"titleBar.border": "#5cb41c",
"titleBar.inactiveBackground": "#5cb41c99",
"titleBar.inactiveForeground": "#15202b99",
"sash.hoverBorder": "#73dd26",
"statusBarItem.remoteBackground": "#5cb41c",
"statusBarItem.remoteForeground": "#15202b"
},
"peacock.color": "#5cb41c"
}
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2021 xiong35
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
<div align="center" id="top">
<img width="100" src="werewolf-frontend/public/wolf.png" alt="Werewolf Logo" />
<!-- <a href="https://werewolf.netlify.app">Demo</a> -->
</div>
<h1 align="center">狼人杀</h1>
<p align="center">
<img alt="Repository size" src="https://img.shields.io/github/repo-size/xiong35/werewolf?color=56BEB8">
<img alt="License" src="https://img.shields.io/github/license/xiong35/werewolf?color=56BEB8">
</p>
<h3 align="center"><a href="http://werewolf.xiong35.cn/">在线使用地址: http://werewolf.xiong35.cn</a></h3>
<p align="center">
<a href="#dart-简介">简介</a>   |  
<a href="#sparkles-features">Features</a>   |  
<a href="#rocket-技术栈">技术栈</a>   |  
<a href="#deciduous_tree-目录结构">目录结构</a>   |  
<a href="#white_check_mark-使用指南">使用指南</a>   |  
<a href="#memo-license">License</a>   |  
<a href="#hugs-thanks">Thanks</a>   |  
<a href="https://github.com/xiong35" target="_blank">作者</a>
</p>
<br>
## :dart: 简介
一款线下狼人杀软件.
为了免去线下打狼时必须带牌和需要主持人的麻烦, 我制作了一款狼人杀网站, 希望给你带来方便 ;P
## :sparkles: Features
### 特色
:heavy_check_mark: 无需带卡牌, 全自动发牌
:heavy_check_mark: 简洁而不失优美的 UI
:heavy_check_mark: 无需主持人, 全自动游戏
:heavy_check_mark: 部署于网站, 适配全设备, 随时可以开一把
:heavy_check_mark: 可配置各个角色人数
:heavy_check_mark: 可进行警长竞选
:heavy_check_mark: 现阶段支持守卫, 猎人, 预言家, 女巫, 村民, 狼人 6 种角色
:heavy_check_mark: 可在游戏中查看事件表、编写备忘
:heavy_check_mark: 即是中途不小心退出游戏, 也可刷新页面直接重连
:heavy_check_mark: 可查看历史对局
:heavy_check_mark: 数据完全安全, 杜绝任何形式的作弊
### 不足
🥺 **不支持**实时语音交流, 仅供线下面基使用
## :rocket: 技术栈
本项目主要运用以下技术:
- [Koa](https://koajs.com/)
- [socket.io](https://socket.io/)
- [Vue3](https://vue3js.cn/)
- [TypeScript](https://www.typescriptlang.org/)
## :deciduous_tree: 目录结构
```txt
├── docs # 随手写的笔记
├── LICENSE # 开源许可证
├── reload.sh # 重启项目的脚本
├── werewolf-backend # **后端相关代码**
│ ├── dist # 打包生成的文件目录
│ ├── package.json
│ └── src # *代码目录*
│ ├── handlers # 具体逻辑
│ ├── index.ts # 项目入口
│ ├── middleware # 中间件
│ ├── models # 基于公用定义封装的后端模型定义
│ ├── routes # koa 路由
│ ├── utils # 工具函数
│ └── ws # web socket 相关逻辑
└── werewolf-frontend # **前端相关代码**
├── dist # 打包生成的文件目录
├── public
│ └── assets # 存放静态资源的目录
├── shared # 前后端公用的模型定义, 接口定义, 数据定义等
└── src # *代码目录*
├── components # 可复用的组件
├── http # 网络相关模块
├── pages # 路由组件
├── reactivity # 抽离出的响应式数据及相关处理
├── router.ts # 前端路由
├── socket # web socket 相关逻辑
└── utils # 工具函数
```
## :white_check_mark: 使用指南
### 开发者
使用前 :checkered_flag:, 拥有 git 和 node 环境
```bash
# Clone this project
$ git clone https://github.com/xiong35/werewolf
# Access backend
$ cd werewolf/werewolf-backend
# Install dependencies
$ npm i
# Run the project
$ npm run dev
# Access frontend
$ cd ../werewolf-frontend
# Install dependencies
$ npm i
# Run the project
$ npm run dev
```
### 使用者
直接访问[网址](http://werewolf.xiong35.cn/)即可!
## :memo: License
本项目使用 MIT 证书. 查看 [LICENSE](LICENSE) 以了解详情.
Made with :heart: by <a href="https://github.com/xiong35" target="_blank">xiong35</a>
 
## :hugs: Thanks
<div>Icons made by <a href="https://www.freepik.com" title="Freepik">Freepik</a> from <a href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com</a></div><div>Icons made by <a href="https://www.flaticon.com/authors/wanicon" title="wanicon">wanicon</a> from <a href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com</a></div>
 
<a href="#top">Back to top</a>
================================================
FILE: docs/笔记.md
================================================
当后端某个状态的定时器到点了
1. 通知所有人下一个状态是什么, 前端设置状态
2. 同时告知所有人存活情况(在进入白天时还会**通知今晚谁死了**)
3. 若是玩家参与的多人共同操作状态结束了
1. 进入查看结果的状态
2. 向后端发起请求询问其结果
---
1. 上一个状态结束, 即将进入 **A** 状态
2. 调用定时器中的回调, 即 endOf**A**
1. 在其中调用 setTimerNSendMsg
1. 在其中调用 nextStateOf**A**, 得到下一个 *B* 状态
2. 设置 *A* 状态结束时调用 endOf*b*
3. 正式进入 **A** 状态
---
// TODO 在 localstorage 里存下来每局的记录, 并增加复盘
// TODO 设置可选的上帝
// TODO 检查是不是每一个角色的 characterStatus 都被设置了
---
================================================
FILE: docs/需求文档.md
================================================
# 需求文档
- [ ] 编写需求文档
## 狼人杀流程
> 每人都只有 15 s 操作, 可提前结束, 死人操作时间随机
每次死人检查是不是警长
1. 设置神职, 设置人数, 设置屠边屠城
2. 发身份, 看身份
3. 第一天
1. 进入黑夜
2. **狼人**相认, 杀人
3. **预言家**睁眼, 验人
4. **女巫**睁眼
1. 看谁死了
2. 可以救自己
3. 只能用一瓶
5. **守卫**保人(如果有守卫)
6. **猎人**看自己是不是还活着
7. 天亮了
8. 15s 选择是否上警
9. 轮流发言
10. 15s 投票
11. 宣布昨晚结果, 猎人开枪?
12. 讨论(都点结束讨论则结束)
13. 15s 投票
14. 宣布谁死了
4. 以后每晚上
15. 进入黑夜
16. **狼人**相认, 杀人
17. **预言家**睁眼, 验人
18. **女巫**睁眼
1. 如果有**灵药**, 看谁死了
2. 不能救自己
3. 只能用一瓶
19. **守卫**保人(如果有守卫), **不能重复**
20. **猎人**看自己是不是还活着
21. 天亮了
22. 宣布昨晚结果, 猎人开枪?
23. 讨论(都点结束讨论则结束)
24. 15s 投票
25. 宣布谁死了
## 前端 ##
### 首页
- [ ] 显示 logo 和几个按钮
- [ ] 创建房间, 加入游戏, 查看规则
### 加入房间
- [ ] 输入房间号
- [ ] 输入游戏昵称
- [ ] url 中包含了房间号和密码, 检测自动填充
### 创建房间
- [ ] 设定人数
- [ ] 设定是否要上帝
- [ ] 设定是否需要密码
- [ ] 获得房主 token, 存到 localStorage
### 等待界面
- [ ] 等待玩家加入
- [ ] 房主可 t 人, 房主有分享二维码
- [ ] 显示所有人头像
### 游戏内
- [ ] 显示所有人头像和存活状态, 长按查看死因
- [ ] 用 websocket 接受信息
- [ ] ui 分白天黑夜(直接 hue rotate?), 显示天数
- [ ] 显示自己能进行的操作
- 狼人投票
- 显示狼队友
- 守卫保人
- 不能重复保
- 预言家验人
- 女巫用药
- [ ]
- 猎人看状态
- 村民投票
- 参与警长竞选
- 上警
- 退水
- 狼人自爆
- 发送通知
- [ ] 显示事件时间表?
- [ ] 备忘录(速记谁有嫌疑)
### 结束页
- [ ] 显示具体事件表
- [ ] 显示身份
### http
- [ ] 编写 api 文档
## 后端 ##
- [ ] 设计模型
- [ ] 设计鉴权机制
- [ ] 拆分逻辑(设计模式)
================================================
FILE: reload.sh
================================================
#!/bin/bash
git pull
cd werewolf-frontend
npm i
npm run build
cd ../werewolf-backend
npm i
npm run build
npm run stop
npm run pro
================================================
FILE: werewolf-backend/.eslintrc
================================================
{
"extends": "koa"
}
================================================
FILE: werewolf-backend/.gitignore
================================================
node_modules
t.*
dist/
log/
================================================
FILE: werewolf-backend/.vscode/be.code-snippets
================================================
{
"create a http handler": {
"scope": "typescript",
"prefix": "h#",
"body": [
"import { Middleware } from \"koa\";",
"import $1 from \"../../models/$1Model\";",
"",
"import {",
" $3,",
" $4,",
"} from \"../../../../werewolf-frontend/shared/httpMsg/$2\";",
"",
"const ${TM_FILENAME/(.*)\\..+$/$1/}: Middleware = async (ctx) => {",
" const req = ctx.request.body as $3;",
"",
" const ret: $4 = {",
" status: 200,",
" msg: \"ok\",",
" data: {}",
" };",
"",
" ctx.body = ret;",
"};",
"",
"export default ${TM_FILENAME/(.*)\\..+$/$1/};",
],
"description": "create a http handler"
},
"debug": {
"scope": "typescript",
"prefix": "log#",
"body": [
"// console.log('# ${TM_FILENAME/(.*)\\..+$/$1/}', $1);",
],
"description": "create a http handler"
}
}
================================================
FILE: werewolf-backend/ecosystem.config.js
================================================
const { name } = require("./package.json");
const path = require("path");
module.exports = {
apps: [
{
name,
script: path.resolve(
__dirname,
"./dist/werewolf-backend/src/index.js"
),
instances: 1,
autorestart: false,
watch: true,
env_production: {
NODE_ENV: "production",
},
},
],
};
================================================
FILE: werewolf-backend/nodemon.json
================================================
{
"watch": [
"src"
],
"ext": "ts",
"exec": "ts-node -r tsconfig-paths/register src/index.ts"
}
================================================
FILE: werewolf-backend/package.json
================================================
{
"name": "werewolf-backend",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "nodemon",
"build": "rm -rf dist && tsc",
"pro": "npx pm2 start ecosystem.config.js --env production -o /var/www/game/werewolf/werewolf-backend/log/pm2.log -e /var/www/game/werewolf/werewolf-backend/log/pm2-err.log",
"stop": "npx pm2 stop ecosystem.config.js"
},
"author": "",
"license": "MIT",
"dependencies": {
"@koa/cors": "^3.1.0",
"koa": "^2.13.1",
"koa-body": "^4.2.0",
"koa-logger": "^3.2.1",
"koa-router": "^10.0.0",
"socket.io": "^4.0.0"
},
"devDependencies": {
"@types/koa": "^2.13.1",
"@types/koa-bodyparser": "^4.2.2",
"@types/koa-logger": "^3.1.1",
"@types/koa-router": "^7.4.1",
"@types/koa__cors": "^3.0.2",
"@types/node": "^14.14.35",
"@types/socket.io": "^2.1.13",
"eslint-config-koa": "^2.0.2",
"eslint-config-standard": "^16.0.2",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.3.1",
"eslint-plugin-standard": "^5.0.0",
"nodemon": "^1.19.0",
"pm2": "^3.5.0",
"ts-node": "^8.1.0",
"tsconfig-paths": "^3.8.0",
"typescript": "^4.2.3"
}
}
================================================
FILE: werewolf-backend/src/handlers/http/gameAct.ts
================================================
import { Middleware } from "koa";
import { IDHeaderName, RoomNumberHeaderName } from "../../../../werewolf-frontend/shared/constants";
import CharacterAct from "../../../../werewolf-frontend/shared/httpMsg/CharacterAct";
import { createError } from "../../middleware/handleError";
import { Player } from "../../models/PlayerModel";
import { Room } from "../../models/RoomModel";
import { status2Handler } from "./gameActHandlers";
import { validateIdentity } from "./gameActHandlers/validateIdentity";
/**
* handle character action
*/
const gameAct: Middleware = async (ctx) => {
const req = ctx.request.body as CharacterAct;
const roomNumber = ctx.get(RoomNumberHeaderName);
const playerID = ctx.get(IDHeaderName);
const room = Room.getRoom(roomNumber);
const player = room.getPlayerById(playerID);
const isValidate = validateIdentity(room, player);
if (isValidate !== true) {
createError({ status: 401, msg: isValidate });
}
const gameStatus = room.curStatus;
// TODO check character
// TODO validate request
// console.log("# gameAct", { gameStatus });
// strategy pattern
ctx.body = await status2Handler[
gameStatus
]?.handleHttpInTheState?.(room, player, req.target, ctx);
};
export default gameAct;
================================================
FILE: werewolf-backend/src/handlers/http/gameActHandlers/BeforeDayDiscuss.ts
================================================
import { Context } from "koa";
import io from "../../../";
import { GameStatus, TIMEOUT } from "../../../../../werewolf-frontend/shared/GameDefs";
import { index } from "../../../../../werewolf-frontend/shared/ModelDefs";
import { Events } from "../../../../../werewolf-frontend/shared/WSEvents";
import { ChangeStatusMsg } from "../../../../../werewolf-frontend/shared/WSMsg/ChangeStatus";
import { ShowMsg } from "../../../../../werewolf-frontend/shared/WSMsg/ShowMsg";
import { Player } from "../../../models/PlayerModel";
import { Room } from "../../../models/RoomModel";
import { renderHintNPlayers } from "../../../utils/renderHintNPlayers";
import { GameActHandler, Response, startCurrentState } from "./";
import { DayDiscussHandler } from "./DayDiscuss";
import { LeaveMsgHandler } from "./LeaveMsg";
export const BeforeDayDiscussHandler: GameActHandler = {
curStatus: GameStatus.BEFORE_DAY_DISCUSS,
async handleHttpInTheState(
room: Room,
player: Player,
target: index,
ctx: Context
) {
// TODO 真正设置 isAlive 字段
return {
status: 200,
msg: "ok",
data: { target },
};
},
startOfState(room: Room) {
if (room.curStatus !== this.curStatus) {
room.gameStatus.push(this.curStatus);
}
// 当执行到这里的时候, 意味着刚刚进入白天
// 如果现在day还是偶数(在夜晚)就++(设置为白天)
if (room.currentDay % 2 === 0) room.currentDay++;
// 此时应该进行夜晚的结算并通知所有人获得晚上的消息了
// 将夜晚死的人的 isAlive 设为false
const dyingPlayers = room.players.filter((p) => {
// 女巫救活了就没有 p.die?.fromCharacter 字段
const isKilledLastNight =
p.die?.at === room.currentDay - 1 && !p.die?.saved;
return isKilledLastNight;
});
dyingPlayers.forEach((p) => (p.isAlive = false));
// 守卫保的人和女巫救的人会设置 die = null, 故不会被设置为死亡
clearTimeout(room.timer);
if (dyingPlayers.length === 0) {
// 平安夜
io.to(room.roomNumber).emit(Events.SHOW_MSG, {
innerHTML: "昨晚是个平安夜",
showTime: TIMEOUT[GameStatus.BEFORE_DAY_DISCUSS],
} as ShowMsg);
} else {
// 死人了
if (room.currentDay === 1) {
// 第一晚有遗言
io.to(room.roomNumber).emit(Events.SHOW_MSG, {
innerHTML: renderHintNPlayers(
"以下为昨晚死亡的玩家, 请发表遗言",
dyingPlayers.map((p) => p.index)
),
showTime: TIMEOUT[GameStatus.BEFORE_DAY_DISCUSS],
} as ShowMsg);
room.players.forEach((p) => (p.isDying = false)); //先把所有人置空
dyingPlayers.forEach((p) => (p.isDying = true)); // 设置昨晚死的人正在留遗言
} else {
// 以后晚上死亡无遗言
io.to(room.roomNumber).emit(Events.SHOW_MSG, {
innerHTML: renderHintNPlayers(
"以下为昨晚死亡的玩家, 不能发表遗言",
dyingPlayers.map((p) => p.index)
),
showTime: TIMEOUT[GameStatus.BEFORE_DAY_DISCUSS],
} as ShowMsg);
}
}
startCurrentState(this, room, dyingPlayers);
},
async endOfState(room: Room, dyingPlayers: Player[]) {
if (dyingPlayers.length) {
// 如果死人了, 依次进行 遗言发表检查, 猎人开枪检查, 警长传递警徽检查
// 死亡操作都结束后进入白天发言环节
room.nextStateOfDieCheck = GameStatus.DAY_DISCUSS;
room.curDyingPlayer = dyingPlayers[0];
LeaveMsgHandler.startOfState(room);
} else {
// 如果没死人就进入白天讨论阶段
room.players.forEach((p) => (p.canBeVoted = p.isAlive));
room.toFinishPlayers = new Set(
room.players
.filter((p) => p.canBeVoted)
.map((p) => p.index)
);
// console.log(
// "# BeforeDayDiscuss",
// "room.toFinishPlayers",
// room.toFinishPlayers
// );
DayDiscussHandler.startOfState(room);
}
},
};
================================================
FILE: werewolf-backend/src/handlers/http/gameActHandlers/DayDiscuss.ts
================================================
import { Context } from "koa";
import { GameStatus, TIMEOUT } from "../../../../../werewolf-frontend/shared/GameDefs";
import { index } from "../../../../../werewolf-frontend/shared/ModelDefs";
import { Events } from "../../../../../werewolf-frontend/shared/WSEvents";
import { ChangeStatusMsg } from "../../../../../werewolf-frontend/shared/WSMsg/ChangeStatus";
import { Player } from "../../../models/PlayerModel";
import { Room } from "../../../models/RoomModel";
import { GameActHandler, Response, startCurrentState } from "./";
import { ExileVoteHandler } from "./ExileVote";
export const DayDiscussHandler: GameActHandler = {
curStatus: GameStatus.DAY_DISCUSS,
async handleHttpInTheState(
room: Room,
player: Player,
target: index,
ctx: Context
) {
room.toFinishPlayers.delete(player.index);
if (room.toFinishPlayers.size === 0) {
clearTimeout(room.timer);
DayDiscussHandler.endOfState(room);
}
return {
status: 200,
msg: "ok",
data: { target },
};
},
startOfState(room: Room) {
startCurrentState(this, room);
},
async endOfState(room: Room) {
room.nextStateOfDieCheck = GameStatus.WOLF_KILL;
ExileVoteHandler.startOfState(room);
},
};
================================================
FILE: werewolf-backend/src/handlers/http/gameActHandlers/ExileVote.ts
================================================
import { Context } from "koa";
import io from "../../..";
import { GameStatus, TIMEOUT } from "../../../../../werewolf-frontend/shared/GameDefs";
import { index } from "../../../../../werewolf-frontend/shared/ModelDefs";
import { Events } from "../../../../../werewolf-frontend/shared/WSEvents";
import { ChangeStatusMsg } from "../../../../../werewolf-frontend/shared/WSMsg/ChangeStatus";
import { createError } from "../../../middleware/handleError";
import { Player } from "../../../models/PlayerModel";
import { Room } from "../../../models/RoomModel";
import { getVoteResult, Vote } from "../../../utils/getVoteResult";
import { renderHintNPlayers } from "../../../utils/renderHintNPlayers";
import { GameActHandler, Response, startCurrentState } from "./";
import { DayDiscussHandler } from "./DayDiscuss";
import { ExileVoteCheckHandler } from "./ExileVoteCheck";
export const ExileVoteHandler: GameActHandler = {
curStatus: GameStatus.EXILE_VOTE,
async handleHttpInTheState(
room: Room,
player: Player,
target: index,
ctx: Context
) {
if (!room.getPlayerByIndex(target).canBeVoted)
createError({
status: 401,
msg: "此玩家不参与投票",
});
player.hasVotedAt[room.currentDay] = target;
return {
status: 200,
msg: "ok",
data: { target },
};
},
startOfState(room: Room) {
startCurrentState(this, room);
},
async endOfState(room: Room) {
const votes: Vote[] = room.players.map((p) => ({
from: p.index,
voteAt: p.hasVotedAt[room.currentDay],
}));
const highestVotes = getVoteResult(votes);
// 如果没有全部弃票
if (!highestVotes || highestVotes.length === 0) {
// 如果所有人都弃票
// 直接进入白天
io.to(room.roomNumber).emit(Events.SHOW_MSG, {
innerHTML: "所有人都弃票, 即将进入夜晚",
});
return ExileVoteCheckHandler.startOfState(
room,
GameStatus.WOLF_KILL
);
} else if (highestVotes.length === 1) {
// 如果有票数最高的人
// 此人被处死, 进入死亡结算
room.getPlayerByIndex(highestVotes[0]).isDying = true;
io.to(room.roomNumber).emit(Events.SHOW_MSG, {
innerHTML: renderHintNPlayers(
"被处死的玩家为:",
highestVotes
),
});
room.curDyingPlayer = room.getPlayerByIndex(highestVotes[0]);
room.curDyingPlayer.isDying = true;
room.curDyingPlayer.isAlive = false;
return ExileVoteCheckHandler.startOfState(
room,
GameStatus.LEAVE_MSG
);
} else {
// 如果多人平票
// 警长当 1.5 票
const sheriff = room.players.find((p) => p.isSheriff);
if (sheriff) {
const sheriffChoice = sheriff.hasVotedAt[room.currentDay];
if (highestVotes.includes(sheriffChoice)) {
// 虽然有平票, 但是警长选择的人在此之中, 则此人死亡
room.getPlayerByIndex(highestVotes[0]).isDying = true;
io.to(room.roomNumber).emit(Events.SHOW_MSG, {
innerHTML: renderHintNPlayers("被处死的玩家为:", [
sheriffChoice,
]),
});
room.curDyingPlayer = room.getPlayerByIndex(
highestVotes[0]
);
room.curDyingPlayer.isDying = true;
room.curDyingPlayer.isAlive = false;
return ExileVoteCheckHandler.startOfState(
room,
GameStatus.LEAVE_MSG
);
}
}
// 若最高票中无警长的影响
// 设置参与投票的人是他们几个
// 设置他们未结束发言
room.players.forEach(
(p) => (p.canBeVoted = highestVotes.includes(p.index))
);
// 告知所有人现在应该再依次投票
io.to(room.roomNumber).emit(Events.SHOW_MSG, {
innerHTML: renderHintNPlayers(
"平票的玩家如下, 请再次依次进行发言",
highestVotes
),
});
room.toFinishPlayers = new Set(highestVotes);
// 设置下一阶段为自由发言
return ExileVoteCheckHandler.startOfState(
room,
GameStatus.DAY_DISCUSS
);
}
},
};
================================================
FILE: werewolf-backend/src/handlers/http/gameActHandlers/ExileVoteCheck.ts
================================================
import { Context } from "koa";
import { GameStatus, TIMEOUT } from "../../../../../werewolf-frontend/shared/GameDefs";
import { index } from "../../../../../werewolf-frontend/shared/ModelDefs";
import { Events } from "../../../../../werewolf-frontend/shared/WSEvents";
import { ChangeStatusMsg } from "../../../../../werewolf-frontend/shared/WSMsg/ChangeStatus";
import { Player } from "../../../models/PlayerModel";
import { Room } from "../../../models/RoomModel";
import { GameActHandler, Response, startCurrentState, status2Handler } from "./";
export const ExileVoteCheckHandler: GameActHandler = {
curStatus: GameStatus.EXILE_VOTE_CHECK,
async handleHttpInTheState(
room: Room,
player: Player,
target: index,
ctx: Context
) {
return {
status: 200,
msg: "ok",
data: { target },
};
},
/**
* @param nextState 在确认完结果后进入哪个状态
*/
startOfState: function (room: Room, nextState: GameStatus) {
startCurrentState(this, room, nextState);
},
/**
* @param nextState 在确认完结果后进入哪个状态
*/
async endOfState(room: Room, nextState: GameStatus) {
status2Handler[nextState].startOfState(room);
},
};
================================================
FILE: werewolf-backend/src/handlers/http/gameActHandlers/GuardProtect.ts
================================================
import { Context } from "koa";
import io from "../../..";
import { GameStatus, TIMEOUT } from "../../../../../werewolf-frontend/shared/GameDefs";
import { index } from "../../../../../werewolf-frontend/shared/ModelDefs";
import { Events } from "../../../../../werewolf-frontend/shared/WSEvents";
import { ChangeStatusMsg } from "../../../../../werewolf-frontend/shared/WSMsg/ChangeStatus";
import { createError } from "../../../middleware/handleError";
import { Player } from "../../../models/PlayerModel";
import { Room } from "../../../models/RoomModel";
import { getVoteResult } from "../../../utils/getVoteResult";
import { GameActHandler, Response, startCurrentState } from "./";
import { BeforeDayDiscussHandler } from "./BeforeDayDiscuss";
import { HunterCheckHandler } from "./HunterCheck";
import { SheriffElectHandler } from "./SheriffElect";
export const GuardProtectHandler: GameActHandler = {
curStatus: GameStatus.GUARD_PROTECT,
async handleHttpInTheState(
room: Room,
player: Player,
target: index,
ctx: Context
) {
player.characterStatus.protects =
player.characterStatus.protects || [];
const protects = player.characterStatus.protects;
if (protects[room.currentDay - 2] === target && target) {
// 如果两天保了同一个人
createError({
status: 401,
msg: "不能连续两天守护相同的人",
});
} else {
protects[room.currentDay] = target;
const protectPlayer = room.getPlayerByIndex(target);
// console.log("# GuardProtect", { protectPlayer });
if (
protectPlayer.die?.at === room.currentDay &&
protectPlayer.die?.fromCharacter === "WEREWOLF"
) {
// 如果确实是今天被杀了
const witchStatus = room.players.find(
(p) => p.character === "WITCH"
)?.characterStatus;
if (
witchStatus?.MEDICINE?.usedAt === target &&
witchStatus?.MEDICINE?.usedDay === room.currentDay
) {
// 如果女巫恰好还救了, 就奶死了
protectPlayer.die = {
at: room.currentDay,
fromCharacter: "GUARD",
fromIndex: [player.index],
};
} else {
// 如果女巫没救
// 设置了此人未被狼人杀死
protectPlayer.die = null;
}
} // 如果今天没被杀, 无事发生
}
return {
status: 200,
msg: "ok",
data: { target },
};
},
startOfState(room: Room) {
// 如果没有守卫就直接开启猎人的阶段
if (!room.needingCharacters.includes("GUARD"))
return GuardProtectHandler.endOfState(room);
startCurrentState(this, room);
},
async endOfState(room: Room) {
if (room.currentDay === 0) {
return SheriffElectHandler.startOfState(room);
}
return BeforeDayDiscussHandler.startOfState(room);
},
};
================================================
FILE: werewolf-backend/src/handlers/http/gameActHandlers/HunterCheck.ts
================================================
import { Context } from "koa";
import io from "../../..";
import { GameStatus, TIMEOUT } from "../../../../../werewolf-frontend/shared/GameDefs";
import { index } from "../../../../../werewolf-frontend/shared/ModelDefs";
import { Events } from "../../../../../werewolf-frontend/shared/WSEvents";
import { ChangeStatusMsg } from "../../../../../werewolf-frontend/shared/WSMsg/ChangeStatus";
import { createError } from "../../../middleware/handleError";
import { Player } from "../../../models/PlayerModel";
import { Room } from "../../../models/RoomModel";
import { getVoteResult } from "../../../utils/getVoteResult";
import { GameActHandler, Response, startCurrentState } from "./";
import { BeforeDayDiscussHandler } from "./BeforeDayDiscuss";
import { HunterShootHandler } from "./HunterShoot";
import { SheriffAssignHandler } from "./SheriffAssign";
import { SheriffElectHandler } from "./SheriffElect";
export const HunterCheckHandler: GameActHandler = {
curStatus: GameStatus.HUNTER_CHECK,
async handleHttpInTheState(
room: Room,
player: Player,
target: index,
ctx: Context
) {
return {
status: 200,
msg: "ok",
data: { target },
};
},
startOfState(room: Room) {
startCurrentState(this, room);
},
async endOfState(room: Room) {
SheriffAssignHandler.startOfState(room);
},
};
================================================
FILE: werewolf-backend/src/handlers/http/gameActHandlers/HunterShoot.ts
================================================
import { Context } from "koa";
import io from "../../..";
import { GameStatus, TIMEOUT } from "../../../../../werewolf-frontend/shared/GameDefs";
import { index } from "../../../../../werewolf-frontend/shared/ModelDefs";
import { Events } from "../../../../../werewolf-frontend/shared/WSEvents";
import { ChangeStatusMsg } from "../../../../../werewolf-frontend/shared/WSMsg/ChangeStatus";
import { ShowMsg } from "../../../../../werewolf-frontend/shared/WSMsg/ShowMsg";
import { createError } from "../../../middleware/handleError";
import { Player } from "../../../models/PlayerModel";
import { Room } from "../../../models/RoomModel";
import { getVoteResult } from "../../../utils/getVoteResult";
import { renderHintNPlayers } from "../../../utils/renderHintNPlayers";
import { GameActHandler, Response, startCurrentState } from "./";
import { HunterCheckHandler } from "./HunterCheck";
import { SheriffAssignHandler } from "./SheriffAssign";
export const HunterShootHandler: GameActHandler = {
curStatus: GameStatus.HUNTER_SHOOT,
async handleHttpInTheState(
room: Room,
player: Player,
target: index,
ctx: Context
) {
// console.log("# HunterShoot", { player });
if (player.die?.fromCharacter === "WITCH") {
// 如果被女巫毒死了就不能开枪
createError({
msg: "你被女巫毒死, 无法开枪",
status: 401,
});
}
if (player.characterStatus.shootAt.player > 0)
createError({ msg: "你已经开过枪了", status: 401 });
const targetPlayer = room.getPlayerByIndex(target);
player.characterStatus.shootAt = {
day: room.currentDay,
player: target,
};
targetPlayer.isAlive = false;
targetPlayer.isDying = true;
targetPlayer.die = {
at: room.currentDay,
fromCharacter: "HUNTER",
fromIndex: [player.index],
};
return {
status: 200,
msg: "ok",
data: { target },
};
},
startOfState(room) {
// 玩家死亡后依次进行以下检查
// 遗言发表检查, 猎人开枪检查, 警长传递警徽检查
if (!showHunter(room)) {
// console.log("# HunterShoot", "not show hunter");
HunterShootHandler.endOfState(room, false);
} else {
// console.log("# HunterShoot", "show hunter");
startCurrentState(this, room, true);
}
},
async endOfState(room, showHunter: boolean) {
if (!showHunter) {
// 无猎人? 直接取消这两个阶段
// console.log("# HunterShoot", "really not show hunter");
return SheriffAssignHandler.startOfState(room);
}
const shotByHunter = room.players.find(
(p) => p.die?.fromCharacter === "HUNTER"
);
if (!shotByHunter) {
// 到点了未选择则不进行操作, 直接进入警长传警徽阶段, 或者无猎人
io.to(room.roomNumber).emit(Events.SHOW_MSG, {
innerHTML: "死者不是猎人或选择不开枪",
} as ShowMsg);
HunterCheckHandler.startOfState(room);
} else {
// 如果死人了, 通知死人了
io.to(room.roomNumber).emit(Events.SHOW_MSG, {
innerHTML: renderHintNPlayers("猎人开枪射杀了", [
shotByHunter.index,
]),
} as ShowMsg);
HunterCheckHandler.startOfState(room);
}
},
};
/**
* 是否需要让大家等猎人开枪
* 如果猎人开过枪或者无猎人就不需要进行此阶段了
*/
function showHunter(room: Room): boolean {
// console.log("# HunterShoot", { room });
if (!room.needingCharacters.includes("HUNTER")) return false;
const hunter = room.players.find(
(p) => p.character === "HUNTER"
);
// console.log("# HunterShoot", {
// hunter: hunter?.characterStatus?.shootAt,
// });
if (!hunter) return false;
if (hunter.characterStatus?.shootAt?.player > 0) return false;
return true;
}
================================================
FILE: werewolf-backend/src/handlers/http/gameActHandlers/LeaveMsg.ts
================================================
import { Context } from "koa";
import io from "../../..";
import { GameStatus, TIMEOUT } from "../../../../../werewolf-frontend/shared/GameDefs";
import { index } from "../../../../../werewolf-frontend/shared/ModelDefs";
import { Events } from "../../../../../werewolf-frontend/shared/WSEvents";
import { ChangeStatusMsg } from "../../../../../werewolf-frontend/shared/WSMsg/ChangeStatus";
import { Player } from "../../../models/PlayerModel";
import { Room } from "../../../models/RoomModel";
import { getVoteResult } from "../../../utils/getVoteResult";
import { GameActHandler, Response, startCurrentState } from "./";
import { HunterShootHandler } from "./HunterShoot";
export const LeaveMsgHandler: GameActHandler = {
curStatus: GameStatus.LEAVE_MSG,
async handleHttpInTheState(
room: Room,
player: Player,
target: index,
ctx: Context
) {
// 结束发言
LeaveMsgHandler.endOfState(room);
return {
status: 200,
msg: "ok",
data: { target },
};
},
startOfState(room) {
// 此阶段必须有 room.nextStateOfDieCheck, 否则无法进行后续状态
if (!room.nextStateOfDieCheck)
throw new Error("未设置死亡结算后去到的状态");
// 玩家死亡后依次进行以下检查
// 遗言发表检查, 猎人开枪检查, 警长传递警徽检查
if (
room.currentDay === 1 ||
room.nextStateOfDieCheck === GameStatus.WOLF_KILL
) {
// 如果是第一夜或者是放逐投票死的就有遗言
// 进入留遗言环节
startCurrentState(this, room);
} else {
// 否则无遗言, 结束当前阶段
LeaveMsgHandler.endOfState(room);
}
},
async endOfState(room) {
HunterShootHandler.startOfState(room);
},
};
================================================
FILE: werewolf-backend/src/handlers/http/gameActHandlers/SeerCheck.ts
================================================
import { Context } from "koa";
import io from "../../..";
import { GameStatus, TIMEOUT } from "../../../../../werewolf-frontend/shared/GameDefs";
import { HttpRes } from "../../../../../werewolf-frontend/shared/httpMsg/_httpResTemplate";
import { SeerCheckData } from "../../../../../werewolf-frontend/shared/httpMsg/SeerCheckMsg";
import { index } from "../../../../../werewolf-frontend/shared/ModelDefs";
import { Events } from "../../../../../werewolf-frontend/shared/WSEvents";
import { ChangeStatusMsg } from "../../../../../werewolf-frontend/shared/WSMsg/ChangeStatus";
import { createError } from "../../../middleware/handleError";
import { Player } from "../../../models/PlayerModel";
import { Room } from "../../../models/RoomModel";
import { getVoteResult } from "../../../utils/getVoteResult";
import { GameActHandler, Response, startCurrentState, status2Handler } from "./";
import { WitchActHandler } from "./WitchAct";
export const SeerCheckHandler: GameActHandler = {
curStatus: GameStatus.SEER_CHECK,
async handleHttpInTheState(
room: Room,
player: Player,
target: index,
ctx: Context
) {
const targetPlayer = room.getPlayerByIndex(target);
if (!targetPlayer)
createError({ status: 400, msg: "未找到此玩家" });
if (player.characterStatus?.checks?.[room.currentDay])
createError({ status: 400, msg: "一天只能查验一次" });
const isWolf = targetPlayer.character === "WEREWOLF";
player.characterStatus.checks =
player.characterStatus.checks || [];
player.characterStatus.checks[room.currentDay] = {
index: target,
isWerewolf: isWolf,
};
const ret: HttpRes<SeerCheckData> = {
data: {
isWolf,
},
msg: "ok",
status: 200,
};
return ret;
},
startOfState(room: Room) {
// 如果没有预言家就直接结束此阶段
if (!room.needingCharacters.includes("SEER"))
return SeerCheckHandler.endOfState(room);
startCurrentState(this, room);
},
async endOfState(room: Room) {
WitchActHandler.startOfState(room);
},
};
================================================
FILE: werewolf-backend/src/handlers/http/gameActHandlers/SheriffAssign.ts
================================================
import { Context } from "koa";
import io from "../../..";
import { GameStatus, TIMEOUT } from "../../../../../werewolf-frontend/shared/GameDefs";
import { index } from "../../../../../werewolf-frontend/shared/ModelDefs";
import { Events } from "../../../../../werewolf-frontend/shared/WSEvents";
import { ChangeStatusMsg } from "../../../../../werewolf-frontend/shared/WSMsg/ChangeStatus";
import { ShowMsg } from "../../../../../werewolf-frontend/shared/WSMsg/ShowMsg";
import { Player } from "../../../models/PlayerModel";
import { Room } from "../../../models/RoomModel";
import { getVoteResult } from "../../../utils/getVoteResult";
import { renderHintNPlayers } from "../../../utils/renderHintNPlayers";
import {
GameActHandler, gotoNextStateAfterHandleDie, Response, startCurrentState, status2Handler
} from "./";
import { LeaveMsgHandler } from "./LeaveMsg";
import { SheriffAssignCheckHandler } from "./SheriffAssignCheck";
export const SheriffAssignHandler: GameActHandler = {
curStatus: GameStatus.SHERIFF_ASSIGN,
async handleHttpInTheState(
room: Room,
player: Player,
target: index,
ctx: Context
) {
const targetPlayer = room.getPlayerByIndex(target);
targetPlayer.isSheriff = true;
player.isSheriff = false;
player.sheriffVotes[room.currentDay] = target;
return {
status: 200,
msg: "ok",
data: { target },
};
},
startOfState(room) {
// 玩家死亡后依次进行以下检查
// 遗言发表检查, 猎人开枪检查, 警长传递警徽检查
if (
!room.players.find((p) => p.isSheriff) ||
!room.curDyingPlayer.isSheriff
) {
// 死者不是警长或无警长, 直接结束
return SheriffAssignHandler.endOfState(room, false);
}
startCurrentState(this, room);
},
async endOfState(room, showSheriff: boolean = true) {
if (!showSheriff) {
// 无警长就直接清算
return gotoNextStateAfterHandleDie(room);
} else {
// TODO 通知发表遗言的时间
// 去除现在死的玩家的警长身份
room.curDyingPlayer.isSheriff = false;
const nextSheriff = room.players.find((p) => p.isSheriff);
if (!nextSheriff) {
io.to(room.roomNumber).emit(Events.SHOW_MSG, {
innerHTML: "上任警长选择不传警徽, 现在没有警长了",
} as ShowMsg);
} else {
io.to(room.roomNumber).emit(Events.SHOW_MSG, {
innerHTML: renderHintNPlayers("下一任警长为", [
nextSheriff.index,
]),
} as ShowMsg);
}
SheriffAssignCheckHandler.startOfState(room);
}
},
};
================================================
FILE: werewolf-backend/src/handlers/http/gameActHandlers/SheriffAssignCheck.ts
================================================
import { Context } from "koa";
import io from "../../..";
import { GameStatus, TIMEOUT } from "../../../../../werewolf-frontend/shared/GameDefs";
import { index } from "../../../../../werewolf-frontend/shared/ModelDefs";
import { Events } from "../../../../../werewolf-frontend/shared/WSEvents";
import { ChangeStatusMsg } from "../../../../../werewolf-frontend/shared/WSMsg/ChangeStatus";
import { Player } from "../../../models/PlayerModel";
import { Room } from "../../../models/RoomModel";
import { getVoteResult } from "../../../utils/getVoteResult";
import { GameActHandler, gotoNextStateAfterHandleDie, Response, startCurrentState } from "./";
export const SheriffAssignCheckHandler: GameActHandler = {
curStatus: GameStatus.SHERIFF_ASSIGN_CHECK,
async handleHttpInTheState(
room: Room,
player: Player,
target: index,
ctx: Context
) {
return {
status: 200,
msg: "ok",
data: { target },
};
},
startOfState(room: Room) {
startCurrentState(this, room);
},
async endOfState(room: Room) {
gotoNextStateAfterHandleDie(room);
},
};
================================================
FILE: werewolf-backend/src/handlers/http/gameActHandlers/SheriffElect.ts
================================================
import { Context } from "koa";
import io from "../../..";
import { GameStatus, TIMEOUT } from "../../../../../werewolf-frontend/shared/GameDefs";
import { index } from "../../../../../werewolf-frontend/shared/ModelDefs";
import { Events } from "../../../../../werewolf-frontend/shared/WSEvents";
import { ChangeStatusMsg } from "../../../../../werewolf-frontend/shared/WSMsg/ChangeStatus";
import { Player } from "../../../models/PlayerModel";
import { Room } from "../../../models/RoomModel";
import { getVoteResult } from "../../../utils/getVoteResult";
import { renderHintNPlayers } from "../../../utils/renderHintNPlayers";
import { GameActHandler, Response, startCurrentState } from "./";
import { BeforeDayDiscussHandler } from "./BeforeDayDiscuss";
import { SheriffSpeachHandler } from "./SheriffSpeach";
export const SheriffElectHandler: GameActHandler = {
curStatus: GameStatus.SHERIFF_ELECT,
async handleHttpInTheState(
room: Room,
player: Player,
target: index,
ctx: Context
) {
// 加入参与竞选的人
player.canBeVoted = true;
return {
status: 200,
msg: "ok",
data: {},
};
},
startOfState(room: Room) {
room.currentDay++;
startCurrentState(this, room);
},
async endOfState(room: Room) {
const electingPlayers = room.players.filter(
(p) => p.canBeVoted
);
if (!electingPlayers || electingPlayers.length === 0) {
// 无人竞选就直接到天亮
return BeforeDayDiscussHandler.startOfState(room);
} else if (electingPlayers.length === 1) {
// 只有一人竞选就把警长给他
electingPlayers[0].isSheriff = true;
io.to(room.roomNumber).emit(Events.SHOW_MSG, {
innerHTML: renderHintNPlayers(
"仅有此玩家参选, 直接成为警长",
[electingPlayers[0].index]
),
});
// TODO 连续让前端显示文字, 后一次会覆盖前一次, 需要前端修改弹窗逻辑
return BeforeDayDiscussHandler.startOfState(room);
} else {
// 有多人参选
// 设置参选警长的人都未结束发言
room.toFinishPlayers = new Set(
electingPlayers.map((p) => p.index)
);
io.to(room.roomNumber).emit(Events.SHOW_MSG, {
innerHTML: renderHintNPlayers(
"参选警长的玩家如下, 请依次进行发言",
electingPlayers.map((p) => p.index)
),
});
return SheriffSpeachHandler.startOfState(room);
}
},
};
================================================
FILE: werewolf-backend/src/handlers/http/gameActHandlers/SheriffSpeach.ts
================================================
import { Context } from "koa";
import io from "../../..";
import { GameStatus, TIMEOUT } from "../../../../../werewolf-frontend/shared/GameDefs";
import { index } from "../../../../../werewolf-frontend/shared/ModelDefs";
import { Events } from "../../../../../werewolf-frontend/shared/WSEvents";
import { ChangeStatusMsg } from "../../../../../werewolf-frontend/shared/WSMsg/ChangeStatus";
import { createError } from "../../../middleware/handleError";
import { Player } from "../../../models/PlayerModel";
import { Room } from "../../../models/RoomModel";
import { getVoteResult } from "../../../utils/getVoteResult";
import { GameActHandler, Response, startCurrentState } from "./";
import { SheriffVoteHandler } from "./SheriffVote";
export const SheriffSpeachHandler: GameActHandler = {
curStatus: GameStatus.SHERIFF_SPEECH,
async handleHttpInTheState(
room: Room,
player: Player,
target: index,
ctx: Context
) {
// 结束自己的发言
room.toFinishPlayers.delete(player.index);
// 如果所有人都发言完毕, 进入警长投票环节
if (room.toFinishPlayers.size === 0) {
SheriffVoteHandler.startOfState(room);
}
return {
status: 200,
msg: "ok",
data: { target },
};
},
startOfState(room: Room) {
startCurrentState(this, room);
},
async endOfState(room: Room) {
SheriffVoteHandler.startOfState(room);
},
};
// TODO 在24h后删除房间
================================================
FILE: werewolf-backend/src/handlers/http/gameActHandlers/SheriffVote.ts
================================================
import { Context } from "koa";
import io from "../../..";
import { GameStatus, TIMEOUT } from "../../../../../werewolf-frontend/shared/GameDefs";
import { index } from "../../../../../werewolf-frontend/shared/ModelDefs";
import { Events } from "../../../../../werewolf-frontend/shared/WSEvents";
import { ChangeStatusMsg } from "../../../../../werewolf-frontend/shared/WSMsg/ChangeStatus";
import { createError } from "../../../middleware/handleError";
import { Player } from "../../../models/PlayerModel";
import { Room } from "../../../models/RoomModel";
import { getVoteResult } from "../../../utils/getVoteResult";
import { renderHintNPlayers } from "../../../utils/renderHintNPlayers";
import { GameActHandler, Response, startCurrentState } from "./";
import { SheriffSpeachHandler } from "./SheriffSpeach";
import { SheriffVoteCheckHandler } from "./SheriffVoteCheck";
export const SheriffVoteHandler: GameActHandler = {
curStatus: GameStatus.SHERIFF_VOTE,
async handleHttpInTheState(
room: Room,
player: Player,
target: index,
ctx: Context
) {
if (!room.getPlayerByIndex(target)?.canBeVoted)
createError({ status: 400, msg: "选择的玩家未参与竞选" });
if (player.canBeVoted)
createError({ status: 400, msg: "参选者不能投票" });
player.sheriffVotes[0] = target;
return {
status: 200,
msg: "ok",
data: { target },
};
},
startOfState(room: Room) {
startCurrentState(this, room);
},
async endOfState(room: Room) {
const votes = room.players.map((p) => ({
from: p.index,
voteAt: p.sheriffVotes[0],
}));
// 找到警长人选
const highestVotes = getVoteResult(votes);
// console.log("# SheriffVote", { votes });
// console.log("# SheriffVote", { highestVotes });
// 如果没有全部弃票
if (!highestVotes || highestVotes.length === 0) {
// 如果所有人都弃票
// 直接进入白天
io.to(room.roomNumber).emit(Events.SHOW_MSG, {
innerHTML: "所有人都弃票, 即将进入自由发言阶段",
});
return SheriffVoteCheckHandler.startOfState(room);
} else if (highestVotes.length === 1) {
// 如果有票数最高的人
// 此人当选, 进入白天
room.getPlayerByIndex(highestVotes[0]).isSheriff = true;
io.to(room.roomNumber).emit(Events.SHOW_MSG, {
innerHTML: renderHintNPlayers(
"当选警长的玩家为:",
highestVotes
),
});
return SheriffVoteCheckHandler.startOfState(room);
} else {
// 如果多人平票
room.toFinishPlayers = new Set();
// 设置参与警长竞选的人是他们几个
room.players.forEach((p) => {
if (highestVotes.includes(p.index)) {
p.canBeVoted = true;
room.toFinishPlayers.add(p.index); // 设置他们未结束发言
} else p.canBeVoted = false;
// 设置所有人警长投票为空
p.sheriffVotes[0] = undefined;
});
// 告知所有人现在应该再依次投票
io.to(room.roomNumber).emit(Events.SHOW_MSG, {
innerHTML: renderHintNPlayers(
"竞争警长的玩家如下, 请再次依次进行发言",
highestVotes
),
});
// 设置下一阶段为警长发言
return SheriffSpeachHandler.startOfState(room);
}
},
};
================================================
FILE: werewolf-backend/src/handlers/http/gameActHandlers/SheriffVoteCheck.ts
================================================
import { Context } from "koa";
import io from "../../..";
import { GameStatus, TIMEOUT } from "../../../../../werewolf-frontend/shared/GameDefs";
import { index } from "../../../../../werewolf-frontend/shared/ModelDefs";
import { Events } from "../../../../../werewolf-frontend/shared/WSEvents";
import { ChangeStatusMsg } from "../../../../../werewolf-frontend/shared/WSMsg/ChangeStatus";
import { Player } from "../../../models/PlayerModel";
import { Room } from "../../../models/RoomModel";
import { getVoteResult } from "../../../utils/getVoteResult";
import { GameActHandler, Response, startCurrentState } from "./";
import { BeforeDayDiscussHandler } from "./BeforeDayDiscuss";
export const SheriffVoteCheckHandler: GameActHandler = {
curStatus: GameStatus.SHERIFF_VOTE_CHECK,
async handleHttpInTheState(
room: Room,
player: Player,
target: index,
ctx: Context
) {
return {
status: 200,
msg: "ok",
data: { target },
};
},
startOfState(room: Room) {
startCurrentState(this, room);
},
async endOfState(room: Room) {
BeforeDayDiscussHandler.startOfState(room);
},
};
================================================
FILE: werewolf-backend/src/handlers/http/gameActHandlers/WitchAct.ts
================================================
import { Context } from "koa";
import io from "../../..";
import { GameStatus, TIMEOUT } from "../../../../../werewolf-frontend/shared/GameDefs";
import { index } from "../../../../../werewolf-frontend/shared/ModelDefs";
import { Events } from "../../../../../werewolf-frontend/shared/WSEvents";
import { ChangeStatusMsg } from "../../../../../werewolf-frontend/shared/WSMsg/ChangeStatus";
import { createError } from "../../../middleware/handleError";
import { Player } from "../../../models/PlayerModel";
import { Room } from "../../../models/RoomModel";
import { getVoteResult } from "../../../utils/getVoteResult";
import { GameActHandler, Response, startCurrentState } from "./";
import { GuardProtectHandler } from "./GuardProtect";
export const WitchActHandler: GameActHandler = {
curStatus: GameStatus.WITCH_ACT,
async handleHttpInTheState(
room: Room,
player: Player,
target: index,
ctx: Context
) {
if (
player.characterStatus?.MEDICINE?.usedDay ===
room.currentDay ||
player.characterStatus?.POISON?.usedDay === room.currentDay
) {
createError({
msg: "一天只能使用一瓶药",
status: 401,
});
}
// 正编号代表救人, 负编号代表杀人
if (target < 0) {
// 杀人
room.getPlayerByIndex(-target).die = {
at: room.currentDay,
fromCharacter: "WITCH",
fromIndex: [player.index],
};
player.characterStatus.POISON = {
usedAt: -target,
usedDay: room.currentDay,
};
} else {
// 救人
const savedPlayer = room.getPlayerByIndex(target);
if (
savedPlayer.die?.fromCharacter === "WEREWOLF" &&
savedPlayer.die?.at === room.currentDay
) {
// 女巫只能救今天被狼人杀的人
if (
savedPlayer._id === player._id &&
room.currentDay !== 0
)
// 女巫只有第一夜才能自救
createError({
msg: "女巫只有第一夜才能自救",
status: 401,
});
// 设置成功救人
savedPlayer.die.saved = true;
savedPlayer.isAlive = true;
player.characterStatus.MEDICINE = {
usedAt: target,
usedDay: room.currentDay,
};
} else
createError({
msg: "女巫只能救今天被狼人杀的人",
status: 401,
});
}
return {
status: 200,
msg: "ok",
data: { target },
};
},
startOfState(room: Room) {
// 如果没有女巫就直接结束此阶段
if (!room.needingCharacters.includes("WITCH"))
return WitchActHandler.endOfState(room);
startCurrentState(this, room);
},
async endOfState(room: Room) {
GuardProtectHandler.startOfState(room);
},
};
================================================
FILE: werewolf-backend/src/handlers/http/gameActHandlers/WolfKill.ts
================================================
import { Context } from "koa";
import io from "../../..";
import { GameStatus, TIMEOUT } from "../../../../../werewolf-frontend/shared/GameDefs";
import { index } from "../../../../../werewolf-frontend/shared/ModelDefs";
import { Events } from "../../../../../werewolf-frontend/shared/WSEvents";
import { ChangeStatusMsg } from "../../../../../werewolf-frontend/shared/WSMsg/ChangeStatus";
import { ShowMsg } from "../../../../../werewolf-frontend/shared/WSMsg/ShowMsg";
import { Player } from "../../../models/PlayerModel";
import { Room } from "../../../models/RoomModel";
import { getVoteResult } from "../../../utils/getVoteResult";
import { GameActHandler, Response, startCurrentState, status2Handler } from "./";
import { WolfKillCheckHandler } from "./WolfKillCheck";
export const WolfKillHandler: GameActHandler = {
curStatus: GameStatus.WOLF_KILL,
async handleHttpInTheState(
room: Room,
player: Player,
target: index,
ctx: Context
) {
// 记录所作的操作
player.characterStatus.wantToKills =
player.characterStatus.wantToKills || [];
player.characterStatus.wantToKills[room.currentDay] = target;
return {
status: 200,
msg: "ok",
data: { target },
};
},
startOfState(room: Room, showCloseEye = true) {
room.currentDay++;
startCurrentState(this, room);
if (showCloseEye)
io.to(room.roomNumber).emit(Events.SHOW_MSG, {
innerHTML: "天黑请闭眼👁️",
} as ShowMsg);
},
async endOfState(room: Room) {
// 准备工作
const werewolfs = room.players.filter(
(p) => p.character === "WEREWOLF"
);
const today = room.currentDay;
const votes = werewolfs.map((p) => ({
from: p.index,
voteAt: p.characterStatus?.wantToKills?.[today],
}));
// console.log("# WolfKill", { votes });
// 找到死者
const voteRes = getVoteResult(votes);
// console.log("# WolfKill", { voteRes });
if (voteRes !== null) {
// 如果没有放弃杀人'
const toKillIndex = voteRes[0];
const toKillPlayer = room.getPlayerByIndex(toKillIndex);
if (toKillPlayer) {
// 设置死亡
toKillPlayer.die = {
at: today,
fromIndex: werewolfs.reduce<index[]>(
(prev, cur) =>
cur.characterStatus?.wantToKills?.[today] ===
toKillIndex
? [...prev, cur.index]
: prev,
[] as index[]
),
fromCharacter: "WEREWOLF",
};
}
// console.log("# WolfKill", { toKillPlayer });
}
// 进入下一状态, 狼人确认杀人结果
WolfKillCheckHandler.startOfState(room);
},
};
================================================
FILE: werewolf-backend/src/handlers/http/gameActHandlers/WolfKillCheck.ts
================================================
import { Context } from "koa";
import io from "../../..";
import { GameStatus, TIMEOUT } from "../../../../../werewolf-frontend/shared/GameDefs";
import { index } from "../../../../../werewolf-frontend/shared/ModelDefs";
import { Events } from "../../../../../werewolf-frontend/shared/WSEvents";
import { ChangeStatusMsg } from "../../../../../werewolf-frontend/shared/WSMsg/ChangeStatus";
import { Player } from "../../../models/PlayerModel";
import { Room } from "../../../models/RoomModel";
import { GameActHandler, Response, startCurrentState, status2Handler } from "./";
import { SeerCheckHandler } from "./SeerCheck";
export const WolfKillCheckHandler: GameActHandler = {
curStatus: GameStatus.WOLF_KILL_CHECK,
async handleHttpInTheState(
room: Room,
player: Player,
target: index,
ctx: Context
) {
return {
status: 200,
msg: "ok",
data: { target },
};
},
startOfState(room: Room) {
startCurrentState(this, room);
},
async endOfState(room: Room) {
SeerCheckHandler.startOfState(room);
},
};
================================================
FILE: werewolf-backend/src/handlers/http/gameActHandlers/index.ts
================================================
import { Context } from "koa";
import io from "../../..";
import { GameStatus, TIMEOUT } from "../../../../../werewolf-frontend/shared/GameDefs";
import { index } from "../../../../../werewolf-frontend/shared/ModelDefs";
import { Events } from "../../../../../werewolf-frontend/shared/WSEvents";
import { ChangeStatusMsg } from "../../../../../werewolf-frontend/shared/WSMsg/ChangeStatus";
import { Player } from "../../../models/PlayerModel";
import { Room } from "../../../models/RoomModel";
import { checkGameOver } from "../../../utils/checkGameOver";
import { BeforeDayDiscussHandler } from "./BeforeDayDiscuss";
import { DayDiscussHandler } from "./DayDiscuss";
import { ExileVoteHandler } from "./ExileVote";
import { ExileVoteCheckHandler } from "./ExileVoteCheck";
import { GuardProtectHandler } from "./GuardProtect";
import { HunterCheckHandler } from "./HunterCheck";
import { HunterShootHandler } from "./HunterShoot";
import { LeaveMsgHandler } from "./LeaveMsg";
import { SeerCheckHandler } from "./SeerCheck";
import { SheriffAssignHandler } from "./SheriffAssign";
import { SheriffAssignCheckHandler } from "./SheriffAssignCheck";
import { SheriffElectHandler } from "./SheriffElect";
import { SheriffSpeachHandler } from "./SheriffSpeach";
import { SheriffVoteHandler } from "./SheriffVote";
import { SheriffVoteCheckHandler } from "./SheriffVoteCheck";
import { WitchActHandler } from "./WitchAct";
import { WolfKillHandler } from "./WolfKill";
import { WolfKillCheckHandler } from "./WolfKillCheck";
export interface Response<T = {}> {
status: number;
msg: string;
data: T;
}
export interface GameActHandler {
/**
* 在状态中处理玩家发送到 http 请求(在此状态下进行的操作)
* 在 koa 中被调用, 是在某个状态中处理玩家操作的函数\
* 仅记录操作并返回操作结果, 多人操作则统一返回 ok
*/
handleHttpInTheState: (
room: Room,
player: Player,
target: index,
ctx: Context
) => Promise<Response>;
/**
* 链式调用\
* 在上一个定时器到点时调用下一个状态的结束函数
* 1. 对于结果
* - 单人操作: 直接返回操作结果
* - 多人操作: 用 socket 通知所有玩家主动拉取操作结果, 只给身份合法的人返回结果, 其他人不做处理
* 2. 对于下一状态
* - 下一状态入栈
* - 改变天数?
* - 改变玩家状态
* - 开启下一状态的定时器
*/
/**
* 在某个状态开始时调用
* 1. 设置此状态结束的回调
* 2. 通知玩家当前状态已经发生改变
* 3. 通知设置天数
*/
startOfState: (room: Room, ...rest: any) => void;
/**
* 在某个状态结束时调用
* 1. 向玩家发送此状态的结果
* 2. 根据局势判断要转移到什么状态
* 3. 调用下一状态的 start
*/
endOfState: (room: Room, ...rest: any) => void;
curStatus: GameStatus;
}
export const status2Handler: Record<GameStatus, GameActHandler> = {
[GameStatus.DAY_DISCUSS]: DayDiscussHandler,
[GameStatus.LEAVE_MSG]: LeaveMsgHandler,
[GameStatus.HUNTER_CHECK]: HunterCheckHandler,
[GameStatus.EXILE_VOTE]: ExileVoteHandler,
[GameStatus.GUARD_PROTECT]: GuardProtectHandler,
[GameStatus.HUNTER_SHOOT]: HunterShootHandler,
[GameStatus.SEER_CHECK]: SeerCheckHandler,
[GameStatus.SHERIFF_ASSIGN]: SheriffAssignHandler,
[GameStatus.SHERIFF_ELECT]: SheriffElectHandler,
[GameStatus.SHERIFF_SPEECH]: SheriffSpeachHandler,
[GameStatus.SHERIFF_VOTE]: SheriffVoteHandler,
[GameStatus.WITCH_ACT]: WitchActHandler,
[GameStatus.WOLF_KILL]: WolfKillHandler,
[GameStatus.EXILE_VOTE_CHECK]: ExileVoteCheckHandler,
[GameStatus.WOLF_KILL_CHECK]: WolfKillCheckHandler,
[GameStatus.SHERIFF_VOTE_CHECK]: SheriffVoteCheckHandler,
[GameStatus.BEFORE_DAY_DISCUSS]: BeforeDayDiscussHandler,
[GameStatus.SHERIFF_ASSIGN_CHECK]: SheriffAssignCheckHandler,
};
/**
* 设置当前没状态结束的定时器, 通知玩家修改状态
*/
export function startCurrentState(
handler: GameActHandler,
room: Room,
...extra: any
) {
// 更新当前房间状态
if (room.curStatus !== handler.curStatus) {
room.gameStatus.push(handler.curStatus);
}
const timeout = TIMEOUT[handler.curStatus];
// 设置此状态结束的回调
clearTimeout(room.timer);
room.timer = setTimeout(() => {
handler.endOfState(room, ...extra);
}, timeout * 1000);
// 通知玩家当前状态已经发生改变, 并通知设置天数
io.to(room.roomNumber).emit(Events.CHANGE_STATUS, {
setDay: room.currentDay,
setStatus: handler.curStatus,
timeout,
} as ChangeStatusMsg);
}
/**
* 当前死亡结算正式结束, 设置此人 isDying 为 false\
* 判断是否还有要进行死亡检查的人
* 1. 如果有就把他设置为 curDyingPlayer, 进行 LeaveMsg
* 2. 如果没有, 设置 curDyingPlayer 为 null, 进行 nextState, 并将他设为 null
*/
export function gotoNextStateAfterHandleDie(room: Room) {
if (checkGameOver(room)) return;
room.curDyingPlayer.isDying = false;
room.curDyingPlayer.isAlive = false;
const dyingPlayer = room.players.find((p) => p.isDying);
// console.log("# index", room.players);
// console.log("# index", { dyingPlayer });
if (dyingPlayer) {
room.curDyingPlayer = dyingPlayer;
return LeaveMsgHandler.startOfState(room);
} else {
room.curDyingPlayer = null;
// 单独处理, 从夜晚进入死亡结算再进入白天时
// 将未结束发言的人设为所有活着的人
// 同时设置能被投票的人为活着的
if (room.nextStateOfDieCheck === GameStatus.DAY_DISCUSS) {
room.toFinishPlayers = new Set(
room.players.filter((p) => p.isAlive).map((p) => p.index)
);
room.players.forEach((p) => (p.canBeVoted = p.isAlive));
}
status2Handler[room.nextStateOfDieCheck].startOfState(room);
room.nextStateOfDieCheck = null;
}
}
================================================
FILE: werewolf-backend/src/handlers/http/gameActHandlers/validateIdentity.ts
================================================
import { GameStatus, StatusWithAction } from "../../../../../werewolf-frontend/shared/GameDefs";
import { Player } from "../../../models/PlayerModel";
import { Room } from "../../../models/RoomModel";
/**
* 检验此玩家是否能在当前阶段发送请求
* @param room
* @param player
* @return {boolean} 是否合规
*/
export function validateIdentity(
room: Room,
player: Player
): true | string {
const gameStatus = room.curStatus;
switch (gameStatus) {
case GameStatus.HUNTER_SHOOT:
return (
(player.character === "HUNTER" &&
room.curDyingPlayer._id === player._id) ||
"你不是猎人"
);
case GameStatus.SHERIFF_ASSIGN:
return (
(player.isSheriff &&
room.curDyingPlayer._id === player._id) ||
"你不是警长"
);
case GameStatus.LEAVE_MSG:
return (
(player.isDying &&
room.curDyingPlayer._id === player._id) ||
"你不能发表遗言"
);
}
if (!player.isAlive) return "你已经是个死人了"; // 除了猎人和警长或者结束发言, 都必须明面上活着才能进行操作
switch (gameStatus as StatusWithAction) {
case GameStatus.WOLF_KILL:
return player.character === "WEREWOLF" || "你不是狼人";
case GameStatus.SEER_CHECK:
return player.character === "SEER" || "你不是预言家";
case GameStatus.WITCH_ACT:
return player.character === "WITCH" || "你不是女巫";
case GameStatus.GUARD_PROTECT:
return player.character === "GUARD" || "你不是守卫";
case GameStatus.DAY_DISCUSS:
return (
room.toFinishPlayers.has(player.index) || "你不能发言"
);
case GameStatus.SHERIFF_ELECT:
case GameStatus.EXILE_VOTE:
case GameStatus.SHERIFF_VOTE:
return true;
case GameStatus.SHERIFF_SPEECH:
return player.canBeVoted || "你不能发言"; // TODO 改成 room.toFinishPlayers.has(player.index) ?
}
// TODO 检查是否有遗漏的状态
return "操作不合法";
}
/*
SHERIFF_VOTE = "投票选警长",
SHERIFF_VOTE_CHECK = "警长投票结果",
SHERIFF_ASSIGN = "指派警长",
BEFORE_DAY_DISCUSS = "夜晚结算",
DAY_DISCUSS = "自由发言",
EXILE_VOTE = "票选狼人",
EXILE_VOTE_CHECK = "票选狼人结果",
HUNTER_SHOOT = "猎人开枪",
LEAVE_MSG = "留遗言",
*/
================================================
FILE: werewolf-backend/src/handlers/http/gameBegin.ts
================================================
import { Middleware } from "koa";
import io from "../../";
import { IDHeaderName, RoomNumberHeaderName } from "../../../../werewolf-frontend/shared/constants";
import { GameStatus } from "../../../../werewolf-frontend/shared/GameDefs";
import { HttpRes } from "../../../../werewolf-frontend/shared/httpMsg/_httpResTemplate";
import CharacterAct from "../../../../werewolf-frontend/shared/httpMsg/CharacterAct";
import { Events } from "../../../../werewolf-frontend/shared/WSEvents";
import { createError } from "../../middleware/handleError";
import { Player } from "../../models/PlayerModel";
import { Room } from "../../models/RoomModel";
import { status2Handler } from "./gameActHandlers";
import { validateIdentity } from "./gameActHandlers/validateIdentity";
/**
* handle game begin
*/
const gameBegin: Middleware = async (ctx) => {
const roomNumber = ctx.get(RoomNumberHeaderName);
const playerID = ctx.get(IDHeaderName);
const room = Room.getRoom(roomNumber);
if (room.creatorID !== playerID)
createError({
msg: "只有房主才能开始游戏",
status: 401,
});
if (room.players.length !== room.needingCharacters.length)
createError({
msg: "房间人数未满, 无法开始游戏",
status: 401,
});
// console.log("#game being");
// assign characters
const needingCharacters = [...room.needingCharacters];
for (let p of room.players) {
const index = Math.floor(
Math.random() * needingCharacters.length
);
const character = needingCharacters.splice(index, 1)[0];
p.character = character;
switch (character) {
case "GUARD":
p.characterStatus = {
protects: [],
};
break;
case "HUNTER":
p.characterStatus = {
shootAt: {
day: -1,
player: -1,
},
};
break;
case "SEER":
p.characterStatus = {
checks: [],
};
break;
case "WEREWOLF":
p.characterStatus = {
wantToKills: [],
};
break;
case "WITCH":
p.characterStatus = {
POISON: { usedDay: -1, usedAt: -1 },
MEDICINE: { usedDay: -1, usedAt: -1 },
};
break;
case "VILLAGER":
p.characterStatus = {};
default:
break;
}
}
io.to(roomNumber).emit(Events.GAME_BEGIN);
// console.log("# roomJoin", "start");
status2Handler[GameStatus.WOLF_KILL].startOfState(room, false);
ctx.body = {
data: {},
msg: "ok",
status: 200,
} as HttpRes;
};
export default gameBegin;
================================================
FILE: werewolf-backend/src/handlers/http/gameGetHint/getWolfs.ts
================================================
import { Middleware } from "koa";
import {
IDHeaderName, RoomNumberHeaderName
} from "../../../../../werewolf-frontend/shared/constants";
import { HttpRes } from "../../../../../werewolf-frontend/shared/httpMsg/_httpResTemplate";
import { createError } from "../../../middleware/handleError";
import { Room } from "../../../models/RoomModel";
import { getVoteResult } from "../../../utils/getVoteResult";
import { renderHintNPlayers } from "../../../utils/renderHintNPlayers";
export const getWolfs: Middleware = async (ctx) => {
const roomNumber = ctx.get(RoomNumberHeaderName);
const playerID = ctx.get(IDHeaderName);
const room = Room.getRoom(roomNumber);
const player = room.getPlayerById(playerID);
if (player.character !== "WEREWOLF")
createError({ status: 401, msg: "你的身份无法查看此消息" });
const wolfs = room.players
.filter(
(p) => p.character === "WEREWOLF" && p._id !== playerID
)
.map((p) => p.index);
const ret = {
status: 200,
msg: "ok",
data: "",
};
if (wolfs.length) {
ret.data = renderHintNPlayers("狼队友是:", wolfs);
} else {
ret.data = "你没有狼队友";
}
ctx.body = ret;
};
================================================
FILE: werewolf-backend/src/handlers/http/gameGetHint/index.ts
================================================
import * as Router from "koa-router";
import { getWolfs } from "./getWolfs";
import { witchGetDie } from "./witchGetDie";
import { getWolfKillResult } from "./wolfKill";
const hintRouter = new Router();
hintRouter.get(
"game hint wolfKill",
"/wolfKill",
getWolfKillResult
);
hintRouter.get(
"game hint witchGetDie",
"/witchGetDie",
witchGetDie
);
hintRouter.get("game hint getWolfs", "/getWolfs", getWolfs);
export default hintRouter;
================================================
FILE: werewolf-backend/src/handlers/http/gameGetHint/witchGetDie.ts
================================================
import { Middleware } from "koa";
import {
IDHeaderName, RoomNumberHeaderName
} from "../../../../../werewolf-frontend/shared/constants";
import { index } from "../../../../../werewolf-frontend/shared/ModelDefs";
import { createError } from "../../../middleware/handleError";
import { Room } from "../../../models/RoomModel";
import { renderHintNPlayers } from "../../../utils/renderHintNPlayers";
export const witchGetDie: Middleware = async (ctx) => {
const roomNumber = ctx.get(RoomNumberHeaderName);
const playerID = ctx.get(IDHeaderName);
const room = Room.getRoom(roomNumber);
const player = room.getPlayerById(playerID);
if (player.character !== "WITCH")
createError({ status: 401, msg: "你的身份无法查看此消息" });
if (player.characterStatus?.MEDICINE?.usedAt > 0)
createError({
status: 401,
msg: "你已经用过解药, 无法查看死者",
});
const killedByWolfToday = room.players.find(
(p) =>
p.die?.fromCharacter === "WEREWOLF" &&
p.die?.at === room.currentDay
);
const ret = {
status: 200,
msg: "ok",
data: "",
};
if (!killedByWolfToday) {
ret.data = "今晚无人被杀害";
} else {
ret.data = renderHintNPlayers("今晚被杀害的是:", [
killedByWolfToday.index,
]);
}
ctx.body = ret;
};
================================================
FILE: werewolf-backend/src/handlers/http/gameGetHint/wolfKill.ts
================================================
import { Middleware } from "koa";
import {
IDHeaderName, RoomNumberHeaderName
} from "../../../../../werewolf-frontend/shared/constants";
import { index } from "../../../../../werewolf-frontend/shared/ModelDefs";
import { createError } from "../../../middleware/handleError";
import { Room } from "../../../models/RoomModel";
import { renderHintNPlayers } from "../../../utils/renderHintNPlayers";
export const getWolfKillResult: Middleware = async (ctx) => {
const roomNumber = ctx.get(RoomNumberHeaderName);
const playerID = ctx.get(IDHeaderName);
const room = Room.getRoom(roomNumber);
const player = room.getPlayerById(playerID);
if (player.character !== "WEREWOLF")
createError({ status: 401, msg: "你的身份无法查看此消息" });
const finalTarget = room.players.find((player) => {
if (!player.die) return false;
const { at, fromCharacter } = player.die;
return at === room.currentDay && fromCharacter === "WEREWOLF"; // 今天被狼杀死的目标即为投票结果
});
let data: { hintText: string; result: index[] };
if (!finalTarget) {
data = {
hintText: "今晚是个平安夜",
result: null,
};
} else {
data = {
hintText: "今晚被杀的是",
result: [finalTarget.index],
};
}
const ret = {
status: 200,
msg: "ok",
data: renderHintNPlayers(data.hintText, data.result),
};
ctx.body = ret;
};
================================================
FILE: werewolf-backend/src/handlers/http/gameStatus.ts
================================================
import { Middleware } from "koa";
import { IDHeaderName, RoomNumberHeaderName } from "../../../../werewolf-frontend/shared/constants";
import { HttpRes } from "../../../../werewolf-frontend/shared/httpMsg/_httpResTemplate";
import { GameStatusResponse } from "../../../../werewolf-frontend/shared/httpMsg/GameStatusMsg";
import {
CharacterEvent, GameEvent, GuardStatus, HunterStatus, PlayerDef, SeerStatus, WerewolfStatus,
WitchStatus
} from "../../../../werewolf-frontend/shared/ModelDefs";
import { Player } from "../../models/PlayerModel";
import { Room } from "../../models/RoomModel";
/**
* fe refresh data
*/
const gameStatus: Middleware = async (ctx, next) => {
const playerID = ctx.get(IDHeaderName);
const roomNumber = ctx.get(RoomNumberHeaderName);
const room = Room.getRoom(roomNumber);
const curPlayer = room.getPlayerById(playerID);
// console.log("# gameStatus");
// TODO 不是所有时候都能看到谁死了的
const ret: HttpRes<GameStatusResponse> = {
status: 200,
msg: "ok",
data: {
self: curPlayer,
curDay: room.currentDay,
gameStatus: room.curStatus,
players: room.isFinished
? room.players
: room.choosePublicInfo(),
},
};
ctx.body = ret;
};
export default gameStatus;
================================================
FILE: werewolf-backend/src/handlers/http/roomCreate.ts
================================================
import { Middleware } from "koa";
import {
CreateRoomRequest, CreateRoomResponse
} from "../../../../werewolf-frontend/shared/httpMsg/CreateRoomMsg";
import { Player } from "../../models/PlayerModel";
import { Room } from "../../models/RoomModel";
const roomCreate: Middleware = async (ctx, next) => {
const req = ctx.request.body as CreateRoomRequest;
const { characters, name, password } = req;
const creator = new Player({
index: 1,
name,
});
const room = new Room({
creator: creator,
needingCharacters: characters,
password,
});
const ret: CreateRoomResponse = {
status: 200,
msg: "ok",
data: {
roomNumber: room.roomNumber,
ID: creator._id,
},
};
// console.log("# roomCreate", { room, creator });
ctx.body = ret;
};
export default roomCreate;
================================================
FILE: werewolf-backend/src/handlers/http/roomInit.ts
================================================
import { Middleware } from "koa";
import { RoomNumberHeaderName } from "../../../../werewolf-frontend/shared/constants";
import {
InitRoomRequest, InitRoomResponse
} from "../../../../werewolf-frontend/shared/httpMsg/InitRoomMsg";
import { Room } from "../../models/RoomModel";
/**
* enter room to get new data
*/
const roomInit: Middleware = async (ctx) => {
const roomNumber = ctx.get(RoomNumberHeaderName);
const room = Room.getRoom(roomNumber);
// console.log("# roomInit", { room, roomNumber });
const ret: InitRoomResponse = {
status: 200,
msg: "ok",
data: {
players: room.choosePublicInfo(),
needingCharacters: room.needingCharacters,
},
};
ctx.body = ret;
};
export default roomInit;
================================================
FILE: werewolf-backend/src/handlers/http/roomJoin.ts
================================================
import { Middleware } from "koa";
import { GameStatus, TIMEOUT } from "../../../../werewolf-frontend/shared/GameDefs";
import {
JoinRoomRequest, JoinRoomResponse
} from "../../../../werewolf-frontend/shared/httpMsg/JoinRoomMsg";
import { Events } from "../../../../werewolf-frontend/shared/WSEvents";
import { RoomJoinMsg } from "../../../../werewolf-frontend/shared/WSMsg/RoomJoin";
import io from "../../index";
import { Player } from "../../models/PlayerModel";
import { Room } from "../../models/RoomModel";
import { status2Handler } from "./gameActHandlers";
const roomJoin: Middleware = async (ctx) => {
const req = ctx.request.body as JoinRoomRequest;
const { name, password, roomNumber } = req;
// console.log("# roomJoin", { roomNumber });
const room = Room.getRoom(roomNumber);
const player = room.playerJoin(name, password);
const ret: JoinRoomResponse = {
status: 200,
msg: "ok",
data: {
ID: player._id,
index: player.index,
needingCharacters: room.needingCharacters,
},
};
const roomJoinMsg: RoomJoinMsg = room.choosePublicInfo();
io.to(roomNumber).emit(Events.ROOM_JOIN, roomJoinMsg);
ctx.body = ret;
};
export default roomJoin;
================================================
FILE: werewolf-backend/src/index.ts
================================================
import { createServer } from "http";
import * as Koa from "koa";
import * as KoaBody from "koa-body";
import * as logger from "koa-logger";
import { Server } from "socket.io";
import * as cors from "@koa/cors";
import { WS_PATH_CLIPED } from "../../werewolf-frontend/shared/constants";
import useHandleError from "./middleware/handleError";
import router from "./routes";
import { setup } from "./ws";
const app = new Koa<
{ isKnownError: Boolean },
{ error: (status: number, msg: string) => void }
>();
const httpServer = createServer(app.callback());
const io = new Server(httpServer, {
cors: {
origin: "http://localhost:3000",
methods: ["GET", "POST"],
},
path: WS_PATH_CLIPED,
});
// listen to some events
setup(io);
app
.use(logger())
.use(
cors({ credentials: true, origin: "http://localhost:3000" })
)
.use(useHandleError())
.use(KoaBody())
.use(router.routes())
.use(router.allowedMethods());
httpServer.listen(3011);
// console.log("listen on 3011");
export default io;
================================================
FILE: werewolf-backend/src/middleware/auth.ts
================================================
import { Middleware } from "koa";
import { IDHeaderName, RoomNumberHeaderName } from "../../../werewolf-frontend/shared/constants";
import { Room } from "../models/RoomModel";
const UseAuth = function (): Middleware {
return async (ctx, next) => {
if (ctx.method !== "OPTIONS") {
const playerID = ctx.get(IDHeaderName);
const roomNumber = ctx.get(RoomNumberHeaderName);
Room.getRoom(roomNumber)?.getPlayerById(playerID); // 调用的函数自带检查
}
await next();
};
};
export default UseAuth;
================================================
FILE: werewolf-backend/src/middleware/handleError.ts
================================================
import { Middleware } from "koa";
const useHandleError = function (): Middleware {
return async (ctx, next) => {
// TODO fix all ctx.throw
try {
await next();
} catch (err) {
try {
const msg = JSON.parse(err.message);
ctx.body = {
...msg,
data: {},
};
} catch {
ctx.body = {
status: 500,
msg: err.message,
data: {},
};
} finally {
console.error(err);
}
} finally {
ctx.status = 200;
}
};
};
export default useHandleError;
export function createError({
status,
msg,
}: {
status: number;
msg: string;
}): undefined {
// console.log("# handleError", { msg });
throw new Error(
JSON.stringify({
status,
msg,
})
);
}
================================================
FILE: werewolf-backend/src/models/PlayerModel.ts
================================================
import { Character } from "../../../werewolf-frontend/shared/GameDefs";
import {
CharacterStatus, day, ID, index, PlayerDef, PublicPlayerDef
} from "../../../werewolf-frontend/shared/ModelDefs";
import { Room } from "./RoomModel";
export class Player implements PlayerDef {
character: Character; // is set when game begins
hasVotedAt: index[] = [];
sheriffVotes: index[] = [];
isAlive = true;
isSheriff = false;
die?: {
at: day;
fromIndex: index[];
fromCharacter: Character;
saved?: boolean;
};
characterStatus?: CharacterStatus = {};
index: index;
name: string;
_id: ID;
constructor({ name, index }: { name: string; index: number }) {
this.name = name;
this.index = index;
this._id =
Math.random().toString(36).substring(2) + "." + Date.now(); // e.g. `5fs6yt6htlu.1621430145541`
}
isDying: boolean = false; // TODO isDying 的话角色闪烁?
canBeVoted: boolean = false;
/**
* 将 Player 信息转换成公开的信息
* @returns 可公开的信息
*/
getPublic(room: Room): PublicPlayerDef {
return {
index: this.index,
isAlive: this.isAlive,
isSheriff: this.isSheriff,
name: this.name,
isDying: this === room.curDyingPlayer,
hasVotedAt: this.hasVotedAt,
sheriffVotes: this.sheriffVotes,
};
}
}
================================================
FILE: werewolf-backend/src/models/RoomModel.ts
================================================
import {
Character,
GameStatus,
} from "../../../werewolf-frontend/shared/GameDefs";
import {
day,
ID,
index,
PlayerDef,
PublicPlayerDef,
RoomDef,
} from "../../../werewolf-frontend/shared/ModelDefs";
import { createError } from "../middleware/handleError";
import { Player } from "./PlayerModel";
export class Room implements RoomDef {
roomNumber: string;
creatorID: ID;
players: Player[];
password?: string;
currentDay: day = -1; // 狼人杀人时会 ++
needingCharacters: Character[];
remainingIndexes: index[];
isFinished = false;
gameStatus: GameStatus[] = [GameStatus.WOLF_KILL];
get curStatus(): GameStatus {
return this.gameStatus[this.gameStatus.length - 1];
}
toFinishPlayers = new Set<index>();
timer: NodeJS.Timeout;
clearSelfTimer: NodeJS.Timeout;
/** 死亡结算后的下一个状态 */
nextStateOfDieCheck: GameStatus;
/** 当前正在进行死亡结算的玩家序号 */
curDyingPlayer: Player;
createdAt = new Date();
private static roomMap: Record<string, Room> = {};
constructor({
creator,
needingCharacters,
password,
}: {
creator: Player;
needingCharacters: Character[];
password?: string;
}) {
if (!checkNeedingCharacters(needingCharacters))
createError({ msg: "人数配比不合法", status: 401 });
let tryTime = 20;
while (tryTime--) {
const roomNumber = Math.random().toString().slice(2, 8);
const prevRoom = Room.roomMap[roomNumber];
if (
prevRoom &&
Date.now() - prevRoom.createdAt.getTime() <
1000 * 3600 * 24
) {
continue;
} else {
this.roomNumber = roomNumber;
Room.roomMap[this.roomNumber] = this;
break;
}
}
if (tryTime <= 0) {
createError({ msg: "创建错误, 请重试!", status: 500 });
}
this.creatorID = creator._id;
this.players = [creator];
this.needingCharacters = needingCharacters; // default index=1
this.remainingIndexes = new Array(needingCharacters.length - 1)
.fill(0)
.map((_, i) => i + 2);
this.password = password;
this.clearSelfTimer = setTimeout(
() => Room.clearRoom(this.roomNumber),
3600 * 1000 * 12
); // 12h 后清除此房间
}
playerJoin(name: string, password?: string): Player {
const nameReg = /^.{1,15}$/;
if (!nameReg.test(name))
return createError({ status: 401, msg: "昵称不合法" });
if (this.password && this.password !== password) {
return createError({ status: 401, msg: "密码错误" });
}
if (this.remainingIndexes.length === 0) {
return createError({ status: 401, msg: "房间已满" });
}
const index = this.remainingIndexes.shift(); // assign smallest index
const player = new Player({ name, index });
this.players.push(player);
return player;
}
choosePublicInfo(): PublicPlayerDef[] {
return this.players
.map((p) => p.getPublic(this))
.sort((a, b) => a.index - b.index);
}
getPlayerById(id: string): Player {
const player = this.players.find((p) => p._id === id);
if (!player)
return createError({ status: 401, msg: "id 错误" });
return player;
}
getPlayerByIndex(index: index): Player {
const player = this.players.find((p) => p.index === index);
if (!player)
return createError({ status: 401, msg: "编号错误" });
return player;
}
static getRoom(number: string): Room {
const room = Room.roomMap[number];
// // console.log("# RoomModel", { room });
if (!room)
return createError({ status: 400, msg: "未找到房间号" });
return room;
}
static clearRoom(number: string): void {
delete this.roomMap[number];
}
}
function checkNeedingCharacters(
needingCharacters: Character[]
): boolean {
if (!needingCharacters.length) return false;
const charMap: Partial<Record<
Character,
number
>> = needingCharacters.reduce((map, character) => {
map[character] = map[character] || 0;
map[character]++;
return map;
}, {});
if (!charMap.WEREWOLF) return false;
if (charMap.WEREWOLF > needingCharacters.length / 2)
return false;
return true;
}
================================================
FILE: werewolf-backend/src/routes/gameRoutes.ts
================================================
import * as Router from "koa-router";
import gameAct from "../handlers/http/gameAct";
import gameBegin from "../handlers/http/gameBegin";
import hintResultRouter from "../handlers/http/gameGetHint";
import gameStatus from "../handlers/http/gameStatus";
const gameRouter = new Router();
gameRouter.post("game begin", "/begin", gameBegin); // 进行角色的操作相关 api
gameRouter.post("game status", "/status", gameStatus); // 查看游戏状态相关 api
gameRouter.post("game act", "/act", gameAct); // 进行角色的操作相关 api
gameRouter.use(
"/hint", // 获取提示信息并在前端显示弹窗的 api
hintResultRouter.routes(),
hintResultRouter.allowedMethods()
);
// TODO get vote result
// TODO 各个玩家在自己回合开始时检查自己能干嘛, 如女巫看看昨晚谁死了, 守卫看看自己昨晚保的谁
export default gameRouter;
================================================
FILE: werewolf-backend/src/routes/index.ts
================================================
import * as Router from "koa-router";
import UseAuth from "../middleware/auth";
import { test } from "../t";
import gameRouter from "./gameRoutes";
import roomRouter from "./roomRoutes";
const router = new Router();
router
.all("/test", test)
.use("/room", roomRouter.routes(), roomRouter.allowedMethods())
.use(
"/game",
UseAuth(),
gameRouter.routes(),
gameRouter.allowedMethods()
);
export default router;
================================================
FILE: werewolf-backend/src/routes/roomRoutes.ts
================================================
import * as Router from "koa-router";
import roomCreate from "../handlers/http/roomCreate";
import roomJoin from "../handlers/http/roomJoin";
import roomInit from "../handlers/http/roomInit";
const roomRouter = new Router();
roomRouter.post("room create", "/create", roomCreate);
roomRouter.post("room join", "/join", roomJoin);
roomRouter.post("room init", "/init", roomInit);
export default roomRouter;
================================================
FILE: werewolf-backend/src/utils/checkGameOver.ts
================================================
import io from "../";
import { Events } from "../../../werewolf-frontend/shared/WSEvents";
import { GameEndMsg } from "../../../werewolf-frontend/shared/WSMsg/GameEnd";
import { Room } from "../models/RoomModel";
const CLEAR_ROOM_TIME = 3600 * 1000;
/**
* @param room 当前房间
* @return {Promise<boolean>} 是否已经结束
*/
export function checkGameOver(room: Room): boolean {
// TODO 添加游戏结束的状态
const { werewolf, villager } = room.players.reduce(
(prev, p) => {
if (p.isAlive) {
if (p.character === "WEREWOLF") prev.werewolf++;
else prev.villager++;
}
return prev;
},
{ werewolf: 0, villager: 0 }
);
// console.log("# checkGameOver", { werewolf, villager }); // TODO
if (werewolf >= villager || werewolf === 0) {
// 通知游戏已结束
const winner = werewolf === 0 ? "VILLAGER" : "WEREWOLF";
io.to(room.roomNumber).emit(Events.GAME_END, {
winner,
} as GameEndMsg);
/* 设置房间状态 */
room.isFinished = true;
clearTimeout(room.timer);
clearTimeout(room.clearSelfTimer);
/* 关闭 sockets */
// make all Socket instances leave the room
io.socketsLeave(room.roomNumber);
// make all Socket instances in the room disconnect (and close the low-level connection)
io.in(room.roomNumber).disconnectSockets(true);
/* 删除此房间 */
setTimeout(() => {
Room.clearRoom(room.roomNumber);
}, CLEAR_ROOM_TIME);
return true;
} else {
return false;
}
}
================================================
FILE: werewolf-backend/src/utils/getVoteResult.ts
================================================
import { index } from "../../../werewolf-frontend/shared/ModelDefs";
/**
* @param votes 投票结果的数组, 每一项是某个玩家投票的玩家编号
* @returns 票数最多的几个玩家的编号, 全 undefined 返回 null
*/
export function getVoteResult(votes: Vote[]): index[] | null {
const voteSituation = getVoteSituation(votes);
/** 所有被投过的人 */
const allTargets = Object.keys(voteSituation);
if (
!allTargets ||
(allTargets.length === 1 && allTargets[0] === "0")
)
return null; // 全员弃票则返回 null
let maxVoteTargets: index[] = [];
let maxVoteCount = -Infinity;
Object.entries(voteSituation).forEach(([target, voters]) => {
if (target === "0") return; // 不考虑弃票的
if (voters.length < maxVoteCount) return;
// 如果这个人票数较少就不考虑了
else if (voters.length === maxVoteCount) {
// 如果平票, 加入 targets 中
maxVoteTargets.push(Number(target));
} else {
// 如果现在的是最高票, 设置相关数据
maxVoteCount = voters.length;
maxVoteTargets = [Number(target)];
}
});
return maxVoteTargets;
}
/**
* 返回票型, key 投票的*目标*, value 为投给这个玩家的人
* 选择弃票的玩家的*目标*为 0
* @param votes 所有人投票的结果
*/
export function getVoteSituation(
votes: Vote[]
): VoteSituationRecord {
const voteSituation: VoteSituationRecord = {};
votes.forEach((v) => {
if (!v.voteAt) v.voteAt = 0;
voteSituation[v.voteAt] = voteSituation[v.voteAt] || [];
voteSituation[v.voteAt].push(v.from);
});
return voteSituation;
}
export interface Vote {
from: index;
/** 弃票则为 falsy 值 */
voteAt: index;
}
/** key 投票的*目标*, value 为投给这个玩家的人 */
type VoteSituationRecord = Record<index, index[]>;
================================================
FILE: werewolf-backend/src/utils/renderHintNPlayers.ts
================================================
import { index } from "../../../werewolf-frontend/shared/ModelDefs";
/**
* 将提示信息和一串玩家渲染成好看的 html
* @param hint
* @param players
* @return 渲染后的 html 片段
*/
export function renderHintNPlayers(
hint: string,
players?: index[]
): string {
let playerHTML = "";
if (players) {
players.forEach(
(index) =>
(playerHTML += `
<div class="die-player">
<div class="player-index">${index}</div>号
</div>
`)
);
}
const innerHTML = `
<style>
.die-player-wrapper {
display: flex;
margin-top: 10px;
}
.die-player-wrapper .die-player {
display: flex;
align-items: flex-end;
margin: 5px;
}
.die-player-wrapper .die-player .player-index {
width: 40px;
height: 40px;
line-height: 40px;
text-align: center;
border-radius: 999px;
background-color: var(--on-bg);
color: var(--bg);
}
</style>
<div>${hint}</div>
<div class="die-player-wrapper">
${playerHTML}
</div>
`;
return innerHTML;
}
================================================
FILE: werewolf-backend/src/ws/index.ts
================================================
import { Server } from "socket.io";
import { Events } from "../../../werewolf-frontend/shared/WSEvents";
export function setup(io: Server) {
io.sockets.on("connection", (socket) => {
// console.log("ws connected");
socket.on(Events.ROOM_JOIN, (roomNumber) => {
// console.log("# join room: " + roomNumber, socket.id);
socket.join(roomNumber);
});
});
}
================================================
FILE: werewolf-backend/tsconfig.json
================================================
{
"compilerOptions": {
"module": "commonjs",
"declaration": false,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es6",
"sourceMap": false,
"outDir": "./dist"
},
"exclude": [
"node_modules"
]
// "include": ["ormconfig.json"]
}
================================================
FILE: werewolf-frontend/.eslintrc.js
================================================
module.exports = {
"env": {
"browser": true,
"es2021": true
},
"extends": [
"eslint:recommended",
"plugin:vue/essential",
"plugin:@typescript-eslint/recommended"
],
"parserOptions": {
"ecmaVersion": 12,
"parser": "@typescript-eslint/parser",
"sourceType": "module"
},
"plugins": [
"vue",
"@typescript-eslint"
],
"rules": {
"indent": [
"error",
4
],
"linebreak-style": [
"error",
"windows"
],
"quotes": [
"error",
"double"
],
"semi": [
"error",
"always"
]
}
};
================================================
FILE: werewolf-frontend/.gitignore
================================================
node_modules
.DS_Store
dist
dist-ssr
*.local
t.*
nohup.out
================================================
FILE: werewolf-frontend/.vscode/settings.json
================================================
{
"workbench.colorCustomizations": {
"activityBar.activeBackground": "#4c73fe",
"activityBar.activeBorder": "#9e0123",
"activityBar.background": "#4c73fe",
"activityBar.foreground": "#e7e7e7",
"activityBar.inactiveForeground": "#e7e7e799",
"activityBarBadge.background": "#9e0123",
"activityBarBadge.foreground": "#e7e7e7",
"editorGroup.border": "#4c73fe",
"panel.border": "#4c73fe",
"sideBar.border": "#4c73fe",
"statusBar.background": "#194cfe",
"statusBar.border": "#194cfe",
"statusBar.foreground": "#e7e7e7",
"statusBarItem.hoverBackground": "#4c73fe",
"tab.activeBorder": "#4c73fe",
"titleBar.activeBackground": "#194cfe",
"titleBar.activeForeground": "#e7e7e7",
"titleBar.border": "#194cfe",
"titleBar.inactiveBackground": "#194cfe99",
"titleBar.inactiveForeground": "#e7e7e799"
},
"peacock.color": "#194cfe"
}
================================================
FILE: werewolf-frontend/README.md
================================================
# 狼人杀前端代码
使用 vue3 + ts + vite 编写,
================================================
FILE: werewolf-frontend/index.html
================================================
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0;"
/>
<title>狼人杀 | xiong35</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
================================================
FILE: werewolf-frontend/package.json
================================================
{
"name": "werewolf-frontend",
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "vite build"
},
"dependencies": {
"axios": "^0.21.1",
"easyqrcodejs": "^4.3.3",
"sha256": "^0.2.0",
"socket.io-client": "^4.0.0",
"vue": "^3.0.4",
"vue-router": "^4.0.5"
},
"devDependencies": {
"@types/axios": "^0.14.0",
"@types/sha256": "^0.2.0",
"@types/socket.io-client": "^1.4.36",
"@types/vue": "^2.0.0",
"@typescript-eslint/eslint-plugin": "^4.18.0",
"@typescript-eslint/parser": "^4.18.0",
"@vue/compiler-sfc": "^3.0.4",
"eslint": "^7.22.0",
"eslint-plugin-vue": "^7.7.0",
"node-sass": "^5.0.0",
"sass": "^1.32.8",
"sass-loader": "^11.0.1",
"vite": "^1.0.0-rc.13"
}
}
================================================
FILE: werewolf-frontend/shared/.vscode/WS Msg Model.code-snippets
================================================
{
"web socket message define": {
"scope": "typescript",
"prefix": "ws#",
"body": [
"import { } from \"../ModelDefs\"",
"",
"export interface ${TM_FILENAME/(.*)\\..+$/$1/}Msg {",
" $1",
"}"
],
"description": "web socket message define"
}
}
================================================
FILE: werewolf-frontend/shared/.vscode/http Msg Model.code-snippets
================================================
{
"http message define": {
"scope": "typescript",
"prefix": "http#",
"body": [
"import { ID, index } from \"../ModelDefs\";",
"import {} from \"../GameDefs\";",
"",
"export interface ${TM_FILENAME/(.*)Msg\\..+$/$1/}Request {",
" ID: ID; // 鉴权用的 ID",
" roomNumber: string; // 当前房间号",
" $1",
"}",
"",
"export interface ${TM_FILENAME/(.*)Msg\\..+$/$1/}Response {",
" status: number;",
" msg: string;",
" data: {}",
"}"
],
"description": "http message define"
}
}
================================================
FILE: werewolf-frontend/shared/GameDefs.ts
================================================
import { RoomDef } from "./ModelDefs";
export type SetableCharacters =
| "HUNTER"
| "WITCH"
| "SEER"
| "GUARD"
| "VILLAGER"
| "WEREWOLF";
export type Character =
| SetableCharacters
| "SHERIFF"
// | "HOST"
| "";
export type Potion = "POISON" | "MEDICINE";
export const ChineseNames: Record<Character, string> = {
HUNTER: "猎人",
GUARD: "守卫",
// HOST: "主持人",
SEER: "预言家",
SHERIFF: "警长",
VILLAGER: "村民",
WEREWOLF: "狼人",
WITCH: "女巫",
"": "",
};
export const CharacterIntro: Record<Character, string> = {
HUNTER:
"你在死亡后可以选择开枪杀死任意一名玩家,但若是被女巫毒死则无法使用此技能。每晚你会醒来查看自己的开枪状态。",
GUARD:
"你每晚可以保护一名角色(包括自己)不被狼人伤害,但不能连续两天守护同一个人。若你守护的人同时被女巫施用了灵药,他还是会死亡。",
// HOST: "主持人",
SEER: "每晚可以查验一名角色是否为狼人。",
SHERIFF:
"在白天的放逐投票中,你选择的人将获得 1.5 票。在你死后,可以选择指派任意玩家继任警长,也可以销毁警徽(如果这么做,村庄将再也不会有警长了)。",
VILLAGER:
"你是一名普通村民,没有特殊能力,但可以发挥你的推理,找出狼人!",
WEREWOLF:
"你是一个狼人,每晚你将和同伴一起苏醒,投票选择一名玩家将其杀害。你的目标是杀死所有非狼人角色!",
WITCH:
"你有两瓶药。第一瓶是灵药,当你未使用过它时,每晚你都将察觉到谁被狼人杀害了,你可以使用灵药来救活他;第二瓶是毒药,你可以使用它杀死任意一名玩家。游戏中,灵药毒药各只能使用一次。每晚你最多只能使用一瓶药。只有第一晚你可以使用灵药救自己。",
"": "",
};
/**
* 当前是什么游戏阶段
* // TODO 每个状态需要一个 http 请求来结束
*/
export enum GameStatus {
// TODO 添加游戏开始前和游戏已经结束的状态
WOLF_KILL = "狼人杀人",
WOLF_KILL_CHECK = "狼人查看投票结果",
SEER_CHECK = "预言家验人",
WITCH_ACT = "女巫用药",
GUARD_PROTECT = "守卫保人",
SHERIFF_ELECT = "上警",
SHERIFF_VOTE = "投票选警长",
SHERIFF_SPEECH = "警长竞选发言",
SHERIFF_VOTE_CHECK = "查看警长投票结果",
/**
* 指当前警长去世了, 指定新的警长
*/
SHERIFF_ASSIGN = "指派警长",
SHERIFF_ASSIGN_CHECK = "检查指派警长的结果",
BEFORE_DAY_DISCUSS = "夜晚结算",
DAY_DISCUSS = "自由发言",
EXILE_VOTE = "票选狼人",
EXILE_VOTE_CHECK = "票选狼人结果",
HUNTER_SHOOT = "若你是猎人, 请选择是否开枪",
HUNTER_CHECK = "查看猎人开枪结果",
LEAVE_MSG = "留遗言",
}
/** 可以允许玩家进行操作的状态 */
export type StatusWithAction =
| GameStatus.WOLF_KILL
| GameStatus.SEER_CHECK
| GameStatus.WITCH_ACT
| GameStatus.GUARD_PROTECT
| GameStatus.SHERIFF_ELECT
| GameStatus.SHERIFF_VOTE
| GameStatus.SHERIFF_ASSIGN
| GameStatus.DAY_DISCUSS
| GameStatus.EXILE_VOTE
| GameStatus.HUNTER_SHOOT
| GameStatus.SHERIFF_SPEECH
| GameStatus.LEAVE_MSG;
/** 预设的每个阶段的时间限制(s) */
export const TIMEOUT: Record<GameStatus, number> = {
[GameStatus.WOLF_KILL]: 25,
[GameStatus.WOLF_KILL_CHECK]: 5,
[GameStatus.SEER_CHECK]: 20,
[GameStatus.WITCH_ACT]: 25,
[GameStatus.GUARD_PROTECT]: 20,
[GameStatus.HUNTER_CHECK]: 5,
[GameStatus.SHERIFF_ELECT]: 15,
[GameStatus.SHERIFF_VOTE]: 25,
[GameStatus.SHERIFF_VOTE_CHECK]: 5,
[GameStatus.SHERIFF_ASSIGN]: 20,
[GameStatus.DAY_DISCUSS]: 996,
[GameStatus.EXILE_VOTE]: 20,
[GameStatus.EXILE_VOTE_CHECK]: 5,
[GameStatus.HUNTER_SHOOT]: 20,
[GameStatus.LEAVE_MSG]: 996,
[GameStatus.BEFORE_DAY_DISCUSS]: 5,
[GameStatus.SHERIFF_SPEECH]: 996,
[GameStatus.SHERIFF_ASSIGN_CHECK]: 5,
};
================================================
FILE: werewolf-frontend/shared/ModelDefs.ts
================================================
import { Character, GameStatus, Potion } from "./GameDefs";
export type ID = string; // 玩家 id
export type index = number; // 玩家编号, 从1开始
export type day = number; // 第0夜: 0, 第 n 天白天: 2n-1, 第 n 天晚上: 2n
export interface RoomDef {
roomNumber: string; // 房间号码, 6 位数字
creatorID: ID; // 创建者 ID
players: PlayerDef[]; // 参与者
password?: string; // 是否设置密码, 存放哈希过的密码
currentDay: day; // 当前天数 -> 游戏结束重置
needingCharacters: Character[]; // 设置的角色
remainingIndexes: index[]; // 空缺的玩家号码
isFinished: boolean; // 是否已结束 -> 游戏结束重置
gameStatus: GameStatus[]; // 所有的游戏状态的栈 -> 游戏结束重置
toFinishPlayers: Set<index>; // 选择结束当前阶段的玩家(每次改变状态需重置)
timer: NodeJS.Timeout; // 事件定时器 id, undefined 则为结束
}
export interface PublicPlayerDef {
index: index; // 玩家编号 -> 游戏结束重置
name: string; // 昵称
isAlive: boolean; // 是否存活 -> 游戏结束重置
// 此状态不代表实际存活状态, 仅代表公开的存活信息
// 如, 晚上有角色被杀了, 但是只有
isSheriff: boolean; // 是否为警长 -> 游戏结束重置
isDying: boolean; // 是否正在进行死亡结算
hasVotedAt: index[]; // 下标是天数, value 是投给了谁
// 包括 狼人杀人 / 白天投票
sheriffVotes: index[]; // 下标是天数, 包括上警(index=0)和白天传警徽 -> 游戏结束重置
}
export interface PlayerDef extends PublicPlayerDef {
character: Character; // 游戏角色 -> 游戏结束重置
characterStatus?: CharacterStatus; // 允许自定义 -> 游戏结束重置
die?: {
// 具体死亡信息 -> 游戏结束重置
at: day; // 第几天死的
fromIndex: index[]; // 被哪些人杀死的(名字)
fromCharacter: Character; // 被哪个角色杀死的
};
_id: ID; // string + 时间戳 的 token
canBeVoted: boolean; // 是否能在当前阶段被投票
}
export interface TokenDef {
ID: ID;
datetime: number;
roomNumber: string;
}
export interface HunterStatus {
shootAt: {
day: day;
player: index;
};
}
export interface GuardStatus {
protects: index[];
}
export interface SeerStatus {
checks: {
index: index;
isWerewolf: boolean;
}[];
}
export interface WerewolfStatus {
wantToKills: index[];
}
interface PotionStatus {
usedDay: day;
usedAt: index;
}
export type WitchStatus = Record<Potion, PotionStatus>;
export type CharacterStatus = Partial<
HunterStatus &
GuardStatus &
SeerStatus &
WerewolfStatus &
WitchStatus
>;
export interface CharacterEvent {
character: Character;
events: {
at: day;
deed: string;
}[];
}
export type GameEvent = {
character: Character;
at: day;
deed: string;
};
// TODO add vote event
================================================
FILE: werewolf-frontend/shared/WSEvents.ts
================================================
export enum Events {
/** 房间相关 */
ROOM_EXILE = "ROOM_EXILE", // 踢出房间
ROOM_JOIN = "ROOM_JOIN", // 有人加入房间
GAME_BEGIN = "GAME_BEGIN", // 开始游戏
GAME_END = "GAME_END", // 结束游戏
/** 游戏相关 */
CHANGE_STATUS = "CHANGE_STATUS", // 设置游戏当前状态
NOTICE_GET_STATUS = "NOTICE_GET_STATUS", // 自己请求角色信息 // TODO
SHOW_MSG = "SHOW_MSG", // 后端推送给前端的消息
}
================================================
FILE: werewolf-frontend/shared/WSMsg/ChangeStatus.ts
================================================
import { GameStatus } from "../GameDefs";
import { day } from "../ModelDefs";
export interface ChangeStatusMsg {
setDay: day; // 设置当前天数
setStatus: GameStatus;
timeout: number; // 有多少秒可以确认
}
================================================
FILE: werewolf-frontend/shared/WSMsg/GameEnd.ts
================================================
import { Character } from "../GameDefs";
import {} from "../ModelDefs";
/**
* Server to Client
*/
export interface GameEndMsg {
winner: "WEREWOLF" | "VILLAGER"; // TODO 限制为 Character 类型
}
================================================
FILE: werewolf-frontend/shared/WSMsg/RoomExile.ts
================================================
import { PublicPlayerDef } from "../ModelDefs";
/**
* Server to Client
*/
export type RoomExileMsg = PublicPlayerDef[];
================================================
FILE: werewolf-frontend/shared/WSMsg/RoomJoin.ts
================================================
import { PublicPlayerDef } from "../ModelDefs";
export type RoomJoinMsg = PublicPlayerDef[];
================================================
FILE: werewolf-frontend/shared/WSMsg/ShowMsg.ts
================================================
export interface ShowMsg {
innerHTML: string; // 展示在弹窗中的信息
showTime?: number; // 展示的时间(s)
}
================================================
FILE: werewolf-frontend/shared/constants.ts
================================================
export const CLIENT_BASE_URL =
// "http://localhost:3000";
"http://werewolf.xiong35.cn";
export const SERVER_DOMAIN =
// "http://localhost:3011";
"http://werewolf.xiong35.cn";
export const SERVER_BASE_URL =SERVER_DOMAIN + "/api"
export const WS_PATH_CLIPED = "/werewolf-ws";
export const WS_PATH = "/api" + WS_PATH_CLIPED;
export const IDHeaderName = "player-id";
export const RoomNumberHeaderName = "room-number";
================================================
FILE: werewolf-frontend/shared/httpMsg/CharacterAct.ts
================================================
import { index } from "../ModelDefs";
export default interface CharacterAct {
/**
* 执行操作的目标玩家编号\
* 若为 女巫, 则正编号代表救人, 负编号代表杀人
*/
target: index;
}
================================================
FILE: werewolf-frontend/shared/httpMsg/CreateRoomMsg.ts
================================================
import { Character } from "../GameDefs";
import { ID, index } from "../ModelDefs";
import { HttpRes } from "./_httpResTemplate";
export interface CreateRoomRequest {
characters: Character[];
password?: string;
name: string;
}
export type CreateRoomResponse = HttpRes<{
roomNumber: string;
ID: ID;
}>;
================================================
FILE: werewolf-frontend/shared/httpMsg/GameStatusMsg.ts
================================================
import { Vote } from "../../src/utils/votes";
import { Character, GameStatus } from "../GameDefs";
import { day, GameEvent, index, PlayerDef, PublicPlayerDef } from "../ModelDefs";
import { HttpRes } from "./_httpResTemplate";
export interface GameStatusRequest {}
export type GameStatusResponse = {
players: PublicPlayerDef[];
self: PlayerDef;
curDay: day;
gameStatus: GameStatus;
};
================================================
FILE: werewolf-frontend/shared/httpMsg/InitRoomMsg.ts
================================================
import { Character } from "../GameDefs";
import { PublicPlayerDef } from "../ModelDefs";
import { HttpRes } from "./_httpResTemplate";
export interface InitRoomRequest {}
export type InitRoomResponse = HttpRes<{
players: PublicPlayerDef[]; // 已有的角色
needingCharacters: Character[];
}>;
================================================
FILE: werewolf-frontend/shared/httpMsg/JoinRoomMsg.ts
================================================
import { Character } from "../GameDefs";
import { ID, index } from "../ModelDefs";
import { HttpRes } from "./_httpResTemplate";
export interface JoinRoomRequest {
name: string; // 昵称
password?: string; // 哈希过的密码
roomNumber: string; // 六位房间号
}
export type JoinRoomResponse = HttpRes<{
ID: ID; // token
index: index;
needingCharacters: Character[]; // 设置的人物
}>;
================================================
FILE: werewolf-frontend/shared/httpMsg/SeerCheckMsg.ts
================================================
import { index } from "../ModelDefs";
import { HttpRes } from "./_httpResTemplate";
import CharacterAct from "./CharacterAct";
export interface SeerCheckRequest extends CharacterAct {}
export type SeerCheckData = {
isWolf: boolean;
};
================================================
FILE: werewolf-frontend/shared/httpMsg/_httpResTemplate.ts
================================================
export interface HttpRes<T = {}> {
status: number;
msg: string;
data: T;
}
================================================
FILE: werewolf-frontend/src/App.vue
================================================
<template>
<div class="wrapper" :class="{ dark: theme === DARK }">
<div class="main">
<router-view></router-view>
<Dialog></Dialog>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { RouterView } from "vue-router";
import { theme, DARK } from "./reactivity/theme";
import Dialog from "./components/Dialog.vue";
const Component = defineComponent({
name: "App",
components: {
RouterView,
Dialog,
},
setup(props) {
return { theme, DARK };
},
});
export default Component;
</script>
<style lang="scss" scoped>
.wrapper {
.main {
max-width: 30rem;
margin: auto;
min-height: 100vh;
padding-bottom: 30px;
box-sizing: border-box;
}
}
</style>
================================================
FILE: werewolf-frontend/src/components/Avatar.vue
================================================
<template>
<div class="avatar">
<img
class="icon"
:src="`/assets/${character.toLowerCase()}${theme}.svg`"
:alt="name"
/>
<div class="info">{{ name }}</div>
</div>
</template>
<script lang="ts">
import { computed, defineComponent } from "vue";
import {
ChineseNames,
SetableCharacters,
} from "../../shared/GameDefs";
import { theme } from "../reactivity/theme";
const Avatar = defineComponent({
name: "Avatar",
props: {
character: { type: String, required: true },
},
setup(props) {
const name = computed(
() => ChineseNames[props.character as SetableCharacters]
);
return { theme, name };
},
});
export default Avatar;
</script>
<style lang="scss" scoped>
.avatar {
width: 2rem;
display: inline-block;
position: relative;
.icon {
width: 100%;
border-radius: 15%;
}
.icon:hover + .info {
opacity: 0.7;
}
.info {
opacity: 0;
transition: opacity 0.2s;
font-size: 0.6rem;
position: absolute;
top: -1.7rem;
left: -5rem;
right: -5rem;
margin: auto;
background-color: var(--on-bg);
color: var(--bg);
padding: 0.3rem;
width: min-content;
border-radius: 5px;
word-break: keep-all;
&::before {
content: "";
position: absolute;
width: 0.5rem;
height: 0.5rem;
background-color: var(--on-bg);
transform-origin: 50% 50%;
left: 0;
right: 0;
margin: auto;
transform: rotate(45deg);
bottom: -12%;
}
}
}
</style>
================================================
FILE: werewolf-frontend/src/components/Btn.vue
================================================
<template>
<div
class="btn"
:class="{ disabled }"
@click="(e) => (disabled ? null : onClick(e))"
>
<UseBorder>
<span class="content">{{ content }}</span>
</UseBorder>
</div>
</template>
<script lang="ts">
// TODO 水波纹效果?
import { ComputedRef, defineComponent } from "vue";
import UseBorder from "./UseBorder.vue";
const Btn = defineComponent({
name: "Btn",
components: { UseBorder },
props: {
content: String,
disabled: {
type: Boolean,
default: false,
},
onClick: { type: Function, default: () => {} },
},
});
export default Btn;
</script>
<style lang="scss" scoped>
.btn {
cursor: pointer;
display: inline-block;
.content {
padding: 0.5rem;
display: inline-block;
}
&.disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
</style>
================================================
FILE: werewolf-frontend/src/components/Dialog.vue
================================================
<template>
<UseMenu
v-show="dialogTimeLeft > 0"
:onCancel="() => (dialogTimeLeft = 0)"
>
<div class="dialog-content">
<span
class="content"
v-html="content && content.content"
></span>
<div @click="dialogTimeLeft = 0" class="confirm">
确认({{ dialogTimeLeft }}s)
</div>
</div>
</UseMenu>
</template>
<script lang="ts">
import { defineComponent, watch, watchEffect } from "vue";
import {
content,
dialogTimeLeft,
showDialog,
toShowContents,
} from "../reactivity/dialog";
import UseMenu from "./UseMenu.vue";
const Dialog = defineComponent({
name: "Dialog",
components: { UseMenu },
setup(props) {
var timer: number;
watch(content, () => {
if (content.value === null) {
clearInterval(timer);
dialogTimeLeft.value = -1;
} else {
dialogTimeLeft.value = content.value.timeout;
timer = window.setInterval(() => {
dialogTimeLeft.value--;
if (dialogTimeLeft.value <= 0) {
clearInterval(timer);
dialogTimeLeft.value = -1;
toShowContents.value.shift();
}
}, 1000);
}
});
return { dialogTimeLeft, content };
},
});
export default Dialog;
</script>
<style lang="scss" scoped>
.dialog-content {
min-height: 8rem;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
word-break: break-word;
padding: 1.5rem 0 0rem;
.confirm {
margin-top: 1rem;
padding: 0.5rem;
cursor: pointer;
}
}
</style>
================================================
FILE: werewolf-frontend/src/components/PlayActions/ActionBtn.vue
================================================
<template>
<Btn
:disabled="disabled"
:onClick="
() => {
onClick ? onClick() : void 0;
disabled ? null : commonAction(noTarget);
}
"
></Btn>
<!-- TODO 死人不能行动 -->
</template>
<script lang="ts">
import { ComputedRef, defineComponent } from "vue";
import Btn from "../../components/Btn.vue";
import { commonAction } from "./commonAction";
const ActionBtn = defineComponent({
name: "ActionBtn",
components: { Btn },
props: {
disabled: {
type: Boolean,
default: false,
},
/** 当前操作是否是有目标的操作, 如结束发言就没有目标 */
noTarget: {
type: Boolean,
default: false,
},
onClick: {
type: Function,
required: false,
},
},
setup(props) {
return { commonAction };
},
});
export default ActionBtn;
</script>
<style lang="scss" scoped></style>
================================================
FILE: werewolf-frontend/src/components/PlayActions/commonAction.ts
================================================
import { isActing, noTarget, target } from "../../reactivity/playAction";
import { showActions } from "../../reactivity/playPage";
/** 每个操作都需要做的事, 如关闭操作面板等 */
export function commonAction(no_target: boolean) {
showActions.value = false;
isActing.value = true;
target.value = -1;
noTarget.value = no_target;
}
================================================
FILE: werewolf-frontend/src/components/PlayActions/index.vue
================================================
<script lang="ts">
import { defineComponent, h, withDirectives, vShow } from "vue";
import { showActions } from "../../reactivity/playPage";
import { renderActionList } from "./renderActionList";
import UseMenu from "../UseMenu.vue";
import { character } from "../../reactivity/game";
const PlayActions = defineComponent({
name: "PlayActions",
render() {
const dialogVNode = h(
UseMenu,
{
onCancel: () => (showActions.value = false),
},
() =>
h(
"div",
{ class: "play__action-list" },
renderActionList()
)
);
return withDirectives(dialogVNode, [
[vShow, showActions.value],
]);
},
});
export default PlayActions;
</script>
<style lang="scss">
.play__action-list {
display: flex;
flex-direction: column;
.btn {
margin: 8px;
}
}
</style>
================================================
FILE: werewolf-frontend/src/components/PlayActions/renderActionList.ts
================================================
import {
ComponentOptions,
ComputedRef,
h,
vShow,
withDirectives,
} from "vue";
import { Character, GameStatus } from "../../../shared/GameDefs";
import { gameStatus, players, self } from "../../reactivity/game";
import { potion } from "../../reactivity/playAction";
import ActionBtn from "./ActionBtn.vue";
const actionInfoList: {
content: string;
isShown: () => boolean;
disabled: () => boolean;
noTarget?: boolean;
onClick?: Function;
}[] = [
{
content: "票选狼人",
isShown: () => true,
disabled: () => gameStatus.value !== GameStatus.EXILE_VOTE,
},
{
content: "票选警长",
isShown: () => true,
disabled: () => gameStatus.value !== GameStatus.SHERIFF_VOTE,
},
{
content: "参与警长竞选",
isShown: () => true,
disabled: () => gameStatus.value !== GameStatus.SHERIFF_ELECT,
noTarget: true,
},
{
content: "狼人杀人",
isShown: () => self.value.character === "WEREWOLF",
disabled: () => gameStatus.value !== GameStatus.WOLF_KILL,
},
{
content: "查验身份",
isShown: () => self.value.character === "SEER",
disabled: () => gameStatus.value !== GameStatus.SEER_CHECK,
},
{
content: "使用毒药",
isShown: () => self.value.character === "WITCH",
disabled: () => gameStatus.value !== GameStatus.WITCH_ACT,
onClick: () => (potion.value = "POISON"),
},
{
content: "使用灵药",
isShown: () => self.value.character === "WITCH",
disabled: () => gameStatus.value !== GameStatus.WITCH_ACT,
onClick: () => (potion.value = "MEDICINE"),
},
{
content: "保护一名玩家",
isShown: () => self.value.character === "GUARD",
disabled: () => gameStatus.value !== GameStatus.GUARD_PROTECT,
},
{
content: "传递警徽",
isShown: () => self.value.isSheriff,
disabled: () => gameStatus.value !== GameStatus.SHERIFF_ASSIGN,
},
{
content: "猎人开枪",
isShown: () => self.value.character === "HUNTER",
disabled: () => gameStatus.value !== GameStatus.HUNTER_SHOOT,
},
{
content: "结束发言",
isShown: () => true,
disabled: () => {
if (gameStatus.value === GameStatus.DAY_DISCUSS)
return !self.value.isAlive;
if (
gameStatus.value === GameStatus.SHERIFF_SPEECH &&
self.value.canBeVoted
)
return false;
if (gameStatus.value === GameStatus.LEAVE_MSG) {
const dyingPlayer = players.value.find((p) => p.isDying);
if (dyingPlayer && dyingPlayer.index === self.value.index)
return false;
}
return true;
},
noTarget: true,
},
];
export const renderActionList = () =>
actionInfoList.map((obj) => {
if (!obj.isShown()) return null;
if (
~["传递警徽", "猎人开枪", "结束发言"].indexOf(obj.content)
) {
return h(ActionBtn, {
disabled: obj.disabled(),
content: obj.content,
noTarget: obj.noTarget,
onClick: obj.onClick,
});
}
return h(ActionBtn, {
disabled: obj.disabled() || !self.value.isAlive,
content: obj.content,
noTarget: obj.noTarget,
onClick: obj.onClick,
});
});
================================================
FILE: werewolf-frontend/src/components/PlayBottomActions.vue
================================================
<template>
<div class="play-bottom-actions" v-show="isActing">
<img
@click="isActing = false"
:src="`/assets/close${theme}.svg`"
alt="close"
/>
<div>
<div>
<span>{{ noTarget ? "是否确认" : "选择目标" }}</span>
</div>
<div>
<small :style="{ opacity: 0.6 }">不选即为放弃</small>
</div>
</div>
<img
@click="act"
:src="`/assets/checked${theme}.svg`"
alt="checked"
/>
</div>
<div class="play-bottom-action-holder"></div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import {
isActing,
act,
noTarget,
} from "../reactivity/playAction";
import { theme } from "../reactivity/theme";
const PlayBottomActions = defineComponent({
name: "PlayBottomActions",
components: {},
setup(props) {
return {
isActing,
theme,
noTarget,
act,
};
},
});
export default PlayBottomActions;
</script>
<style lang="scss" scoped>
$height: 3rem;
.play-bottom-action-holder {
height: $height;
}
.play-bottom-actions {
position: fixed;
background-color: var(--bg);
right: 0;
left: 0;
bottom: 0;
height: $height;
display: flex;
justify-content: space-between;
align-items: center;
padding: 2vh 10%;
max-width: 30rem;
margin: auto;
img {
background-color: transparent;
padding: 0.5rem;
width: 2rem;
height: 2rem;
cursor: pointer;
}
&::before {
content: "";
position: absolute;
top: 0;
bottom: 0;
right: 0;
left: 0;
z-index: -1;
opacity: 0.7;
}
}
</style>
================================================
FILE: werewolf-frontend/src/components/PlayCharacter.vue
================================================
<template>
<UseMenu v-show="showCharacter" :onCancel="() => (showCharacter = false)">
<Avatar :character="character"></Avatar>
<div class="character">
你的身份是:<span class="character-name">{{ name }}</span>
</div>
<p class="intro">{{ intro }}</p>
</UseMenu>
</template>
<script lang="ts">
import { defineComponent, computed } from "vue";
import { character } from "../reactivity/game";
import { showCharacter } from "../reactivity/playPage";
import { ChineseNames, CharacterIntro } from "../../shared/GameDefs";
import UseMenu from "./UseMenu.vue";
import Avatar from "./Avatar.vue";
const PlayCharacter = defineComponent({
name: "PlayCharacter",
components: {
UseMenu,
Avatar,
},
setup(props) {
const name = computed(() => ChineseNames[character.value]);
const intro = computed(() => CharacterIntro[character.value]);
return {
character,
name,
intro,
showCharacter,
};
},
});
export default PlayCharacter;
</script>
<style lang="scss" scoped>
.use-menu {
text-align: center;
.avatar {
width: 40%;
}
.character {
font-size: 1.2rem;
margin: 1rem;
.character-name {
font-weight: bold;
}
}
.intro {
text-align: left;
}
}
</style>
================================================
FILE: werewolf-frontend/src/components/PlayEventList.vue
================================================
<template>
<div v-if="events !== undefined" class="event-day">
Day {{ day }}
</div>
<Tile
v-for="item in events"
:key="item.at + item.deed"
:character="item.character"
:deed="item.deed"
:at="item.at"
></Tile>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import type { GameEvent } from "../../shared/ModelDefs";
import Tile from "../components/PlayEventTile.vue";
const PlayEventList = defineComponent({
name: "PlayEventList",
components: { Tile },
props: {
events: Object as () => GameEvent[], // GameEvent 的数组
day: Number,
},
setup(props) {
return {};
},
});
export default PlayEventList;
</script>
<style lang="scss" scoped>
.event-day {
text-align: left;
padding: 0.4rem 0.2rem 0.3rem;
font-weight: bold;
font-size: 1.1rem;
}
</style>
================================================
FILE: werewolf-frontend/src/components/PlayEventTile.vue
================================================
<template>
<div class="play-event-tile" :class="'.level' + level">
<div class="left-info">
<Avatar :character="character"></Avatar>
<img
:src="`/assets/${
at % 2 === 1 ? 'sun' : 'moon'
}${theme}.svg`"
class="isDay"
/>
</div>
<pre class="deed">{{ deed }}</pre>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { theme } from "../reactivity/theme";
import Avatar from "./Avatar.vue";
const PlayEventTile = defineComponent({
name: "PlayEventTile",
components: { Avatar },
props: {
character: { type: String, isRequired: true },
level: { type: Number, default: 1 },
deed: String,
at: Number,
},
setup(props) {
return { theme };
},
});
export default PlayEventTile;
</script>
<style lang="scss" scoped>
.play-event-tile {
display: flex;
align-items: center;
margin: 0.4rem;
position: relative;
&::before {
content: "";
position: absolute;
background-color: currentColor;
top: -0.3rem;
right: 0;
bottom: -0.3rem;
left: 1.12rem;
border-radius: 999px;
width: 2px;
}
.left-info {
position: relative;
padding: 0.3rem;
border: 2px solid;
border-radius: 50%;
width: 1.5rem;
height: 1.5rem;
margin-right: 0.5rem;
.isDay {
position: absolute;
width: 30%;
left: -10%;
top: -10%;
border-radius: 50%;
border: 2px solid transparent;
box-shadow: 0 0 0 1px currentColor;
}
.avatar {
width: 100%;
background-color: transparent;
}
}
.deed {
flex: 1 0 0;
text-align: left;
font: inherit;
word-break: break-all;
white-space: pre-wrap;
}
}
</style>
================================================
FILE: werewolf-frontend/src/components/PlayEvents.vue
================================================
<template>
<UseMenu
class="game-event"
v-show="showEvents"
:onCancel="() => (showEvents = false)"
>
<div class="title">事件一览</div>
<div v-if="groupedGameEvents.length > 0">
<EventList
v-for="(events, day) in groupedGameEvents"
:key="day"
:day="day"
:events="events"
></EventList>
</div>
<div class="placeholder" v-else>暂无事件</div>
</UseMenu>
</template>
<script lang="ts">
import { defineComponent, computed } from "vue";
import { showEvents } from "../reactivity/playPage";
import { GameEvent } from "../../shared/ModelDefs";
import UseMenu from "./UseMenu.vue";
import EventList from "./PlayEventList.vue";
import { groupedGameEvents } from "../reactivity/computeGameEvents";
const Events = defineComponent({
name: "Events",
components: { UseMenu, EventList },
props: {},
setup(props) {
return { showEvents, groupedGameEvents };
},
});
export default Events;
</script>
<style lang="scss">
.use-menu.game-event {
text-align: center;
.use-border {
overflow-y: scroll;
overflow-x: hidden;
max-height: 80vh;
}
.title {
font-size: 1.3rem;
font-weight: bold;
margin-bottom: 1rem;
}
textarea {
resize: none;
width: 85%;
margin: 1rem;
}
.placeholder {
text-align: center;
opacity: 0.5;
padding: 2rem 0;
}
}
</style>
================================================
FILE: werewolf-frontend/src/components/PlayMemo.vue
================================================
<template>
<UseMenu v-show="showMemo" :onCancel="() => (showMemo = false)">
<span class="title">备忘录</span>
<textarea v-model="memoContent" rows="20"></textarea>
</UseMenu>
</template>
<script lang="ts">
import { defineComponent, onMounted } from "vue";
import { showMemo, memoContent } from "../reactivity/playPage";
import UseMenu from "./UseMenu.vue";
const Memo = defineComponent({
name: "Memo",
components: { UseMenu },
props: {},
setup(props) {
onMounted(() => {
memoContent.value = localStorage.getItem("memo") || "";
});
return { showMemo, memoContent };
},
});
export default Memo;
</script>
<style lang="scss" scoped>
.use-menu {
text-align: center;
.title {
font-size: 1.3rem;
font-weight: bold;
}
textarea {
resize: none;
width: 85%;
margin: 1rem;
}
}
</style>
================================================
FILE: werewolf-frontend/src/components/RoomCharacterTile.vue
================================================
<template>
<div class="room-character-tile">
<character-avatar :character="character"></character-avatar>
<div class="controll">
<div @click="setCharacter(character, -1)" class="down"></div>
<div class="number">{{ num }}</div>
<div @click="setCharacter(character, 1)" class="up"></div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, toRef } from "vue";
import {
setCharacter,
characters,
} from "../reactivity/createRoom";
import {
ChineseNames,
SetableCharacters,
} from "../../shared/GameDefs";
import CharacterAvatar from "./Avatar.vue";
const RoomCharacter = defineComponent({
name: "RoomCharacter",
components: { CharacterAvatar },
props: {
character: { type: String, required: true },
},
setup(props) {
const num = toRef(
characters,
props.character as SetableCharacters
);
const name =
ChineseNames[props.character as SetableCharacters];
return { setCharacter, name, num };
},
});
export default RoomCharacter;
</script>
<style lang="scss" scoped>
.room-character-tile {
$size: 0.6rem;
position: relative;
text-align: center;
.avatar {
width: 40%;
}
.controll {
display: flex;
justify-content: space-around;
.up,
.down {
box-sizing: border-box;
color: var(--on-bg);
cursor: pointer;
border: $size solid transparent;
border-bottom-color: currentColor;
width: $size;
height: $size;
position: relative;
&::before {
content: "";
position: absolute;
background-color: var(--bg);
width: $size * 0.3;
height: $size * 0.3;
border-radius: 50%;
top: $size * 0.3;
left: $size * 0.2;
}
}
.up {
transform: rotate(90deg);
}
.down {
transform: rotate(-90deg);
}
.number {
line-height: $size * 2;
font-weight: bold;
}
}
}
</style>
================================================
FILE: werewolf-frontend/src/components/RoomPlayerList.vue
================================================
<template>
<div class="room-player-list">
<div
v-for="item in playerList"
:key="item.index"
class="player"
>
<div
v-if="item.name !== undefined"
class="box"
:style="{ cursor: isActing ? 'pointer' : 'inherit' }"
:class="{
isDead: !item.isAlive,
isChosen: item.index === target && isActing,
}"
@click="
() => setTarget(target === item.index ? 0 : item.index)
"
>
{{
item.name.slice(0, 3) +
(item.name.length > 3 ? "..." : "")
}}
<div class="index">
<span class="index-content">{{ item.index }}</span>
</div>
<img
v-show="item.isSheriff"
alt="警长"
:src="`/assets/sheriff${theme}.svg`"
class="sherrif"
/>
<img
class="dead"
:class="{ isDying: item.isDying }"
v-show="!item.isAlive"
alt="骷髅"
:src="`/assets/dead${theme}.svg`"
/>
</div>
<div v-else class="box empty">
<span class="index">{{ item.index }}</span>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, computed, PropType } from "vue";
import { PublicPlayerDef } from "../../shared/ModelDefs";
import {
setTarget,
isActing,
target,
} from "../reactivity/playAction";
import { theme } from "../reactivity/theme";
const RoomPlayerList = defineComponent({
name: "RoomPlayerList",
props: {
playerList: {
type: Object as PropType<PublicPlayerDef[]>,
required: true,
},
},
components: {},
setup(props) {
return { theme, setTarget, target, isActing };
},
});
export default RoomPlayerList;
</script>
<style lang="scss">
.room-player-list {
display: flex;
flex-wrap: wrap;
.player {
display: flex;
margin: 5% 0;
flex: 1 1 33%;
justify-content: center;
.box {
$size: 6rem;
width: $size;
height: $size;
display: flex;
align-items: center;
justify-content: center;
border-radius: 5px;
border: 2px solid;
background-color: var(--secondary);
position: relative;
$icon-size: 0.25 * $size;
font-size: 1.5rem;
.index,
.sherrif,
.dead {
position: absolute;
width: $icon-size;
height: $icon-size;
text-align: center;
box-sizing: border-box;
&.isDying {
@keyframes shine {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
animation: shine 0.7s linear infinite alternate;
}
}
$offset: -0.5 * $icon-size;
.index {
top: $offset;
left: $offset;
border: 1px solid;
font-size: 0.5em;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
.index-content {
background-color: transparent;
line-height: 1em;
}
}
.dead {
bottom: $offset;
right: $offset;
background-color: transparent;
}
.sherrif {
top: $offset;
right: $offset;
background-color: transparent;
}
}
.box.empty {
background-color: rgb(131, 131, 131);
opacity: 30%;
}
.box.isDead {
opacity: 50%;
}
.box.isChosen {
filter: brightness(1.7);
box-shadow: var(--on-bg) 2px 2px 0px 0px;
}
}
}
</style>
================================================
FILE: werewolf-frontend/src/components/UseBorder.vue
================================================
<template>
<span class="use-border">
<slot></slot>
<div class="use-border-mask"></div>
</span>
</template>
<script lang="ts">
import { defineComponent } from "vue";
const UseBorder = defineComponent({
name: "UseBorder",
components: {},
setup(props) {},
});
export default UseBorder;
</script>
<style lang="scss" scoped>
$border-width: 3px;
.use-border {
border: $border-width solid;
border-radius: 5px;
position: relative;
display: inline-block;
.use-border-mask {
position: absolute;
right: 6%;
top: -2 * $border-width;
height: 3 * $border-width;
width: 8px;
background-color: var(--bg);
&::before,
&::after {
content: "";
width: $border-width;
height: $border-width;
background-color: currentColor;
position: absolute;
border-radius: 50%;
top: $border-width;
}
&::before {
left: -$border-width / 2;
}
&::after {
right: -$border-width / 2;
}
}
}
</style>
================================================
FILE: werewolf-frontend/src/components/UseMenu.vue
================================================
<template>
<div class="use-menu">
<UseBorder>
<slot></slot>
<img
@click="onCancel"
class="cancel"
:src="`/assets/close${theme}.svg`"
/>
</UseBorder>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { theme } from "../reactivity/theme";
import UseBorder from "./UseBorder.vue";
const UseMenu = defineComponent({
name: "UseMenu",
components: {
UseBorder,
},
props: {
onCancel: { type: Function, required: true },
},
setup(props) {
return { theme };
},
});
export default UseMenu;
</script>
<style lang="scss" scoped>
.use-menu {
position: fixed;
top: 0;
bottom: 0;
right: 0;
left: 0;
.use-border {
position: absolute;
top: 46%;
left: 50%;
transform: translate(-50%, -50%);
box-sizing: border-box;
padding: 1rem;
width: 80%;
max-width: 20rem;
.cancel {
$size: 1.5rem;
position: absolute;
top: -$size * 0.5;
right: -$size * 0.5;
width: $size;
height: $size;
padding: $size;
cursor: pointer;
background-color: transparent;
}
}
}
</style>
================================================
FILE: werewolf-frontend/src/http/_request.ts
================================================
import axios, { AxiosRequestConfig } from "axios";
import { IDHeaderName, RoomNumberHeaderName, SERVER_BASE_URL } from "../../shared/constants";
import { HttpRes } from "../../shared/httpMsg/_httpResTemplate";
import { showDialog } from "../reactivity/dialog";
import { getToken } from "../utils/token";
export default function request<T = {}>(
config: AxiosRequestConfig
) {
const instance = axios.create({
baseURL: SERVER_BASE_URL,
timeout: 60000,
withCredentials: true,
});
instance.interceptors.request.use(
(config) => {
const token = getToken();
config.headers[IDHeaderName] = token && token.ID;
config.headers[RoomNumberHeaderName] =
token && token.roomNumber;
return config;
},
(err) => {
console.error(err);
}
);
instance.interceptors.response.use(
(response) => {
const data = response.data || {};
if (data.status === 200) {
return data;
} else {
if (data.msg) {
showDialog(data.msg);
} else {
console.error("# e", { response });
showDialog("不知道发生了什么呢QwQ");
}
return null;
}
},
(err) => {
console.error(err);
}
);
return new Promise<HttpRes<T>>(async (resolve) => {
const res = await instance(config);
resolve((res as unknown) as HttpRes<T>);
});
}
================================================
FILE: werewolf-frontend/src/http/action.ts
================================================
import { HttpRes } from "../../shared/httpMsg/_httpResTemplate";
import CharacterAct from "../../shared/httpMsg/CharacterAct";
import { SeerCheckData } from "../../shared/httpMsg/SeerCheckMsg";
import request from "./_request";
export async function characterAct(
data: CharacterAct
): Promise<HttpRes<Partial<SeerCheckData>>> {
const res = await request({
url: "/game/act",
method: "POST",
data,
});
return res;
}
================================================
FILE: werewolf-frontend/src/http/gameGetHint.ts
================================================
import { showDialog } from "../reactivity/dialog";
import request from "./_request";
/**
* 获得狼人杀人结果并显示弹窗
* @returns 是否成功
*/
export async function getWolfKillResNShow(): Promise<boolean> {
const res = await request<string>({
url: "/game/hint/wolfKill",
method: "GET",
});
if (!res || res.status !== 200) return false;
showDialog(res.data);
return true;
}
/**
* 获得狼人队友并显示弹窗
* @returns 是否成功
*/
export async function getWolfsNShow(): Promise<boolean> {
const res = await request<string>({
url: "/game/hint/getWolfs",
method: "GET",
});
if (!res || res.status !== 200) return false;
showDialog(res.data);
return true;
}
/**
* 女巫获得狼人杀人结果并显示弹窗
* @returns 是否成功
*/
export async function witchGetDieNShow(): Promise<boolean> {
const res = await request<string>({
url: "/game/hint/witchGetDie",
method: "GET",
});
if (!res || res.status !== 200) return false;
showDialog(res.data);
return true;
}
================================================
FILE: werewolf-frontend/src/http/gameStatus.ts
================================================
import { GameStatusRequest, GameStatusResponse } from "../../shared/httpMsg/GameStatusMsg";
import request from "./_request";
export async function getGameStatus(
data: GameStatusRequest
): Promise<GameStatusResponse | null> {
const res = await request<GameStatusResponse>({
url: "/game/status",
method: "POST",
data,
});
if (!res || res.status !== 200) {
return null;
}
return res.data;
}
================================================
FILE: werewolf-frontend/src/http/room.ts
================================================
import { CreateRoomRequest, CreateRoomResponse } from "../../shared/httpMsg/CreateRoomMsg";
import { InitRoomRequest, InitRoomResponse } from "../../shared/httpMsg/InitRoomMsg";
import { JoinRoomRequest, JoinRoomResponse } from "../../shared/httpMsg/JoinRoomMsg";
import request from "./_request";
export async function createRoom(
data: CreateRoomRequest
): Promise<CreateRoomResponse> {
const res = (await request({
url: "/room/create",
method: "POST",
data,
})) as unknown;
return res as CreateRoomResponse;
}
export async function joinRoom(
data: JoinRoomRequest
): Promise<JoinRoomResponse | null> {
const res = (await request({
url: "/room/join",
method: "POST",
data,
})) as unknown;
return res as JoinRoomResponse;
}
export async function initRoom(
data: InitRoomRequest
): Promise<InitRoomResponse | null> {
const res = (await request({
url: "/room/init",
method: "POST",
data,
})) as unknown;
return res as InitRoomResponse;
}
export async function gameBegin(): Promise<boolean> {
const res = await request({
url: "/game/begin",
method: "POST",
});
return res.status === 200;
}
================================================
FILE: werewolf-frontend/src/index.css
================================================
.wrapper.dark {
--bg: #111;
--on-bg: #f8f8f8;
--secondary: #222;
}
.wrapper {
--bg: #f8f8f8;
--on-bg: #111;
--secondary: #eee;
}
* {
color: var(--on-bg);
background-color: var(--bg);
transition: color 0.2s, background-color 0.2s;
user-select: none;
}
.main {
min-height: 100vh;
background-color: var(--bg);
}
input {
border: none;
}
input:focus {
outline: none;
}
*::-webkit-scrollbar {
width: 0px;
height: 0px;
}
*::-webkit-scrollbar-track {
background: var(--bg);
}
*::-webkit-scrollbar-thumb {
background-color: var(--bg);
border-radius: 20px;
border: 0px solid var(--on-bg);
}
* {
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
.spacer {
flex: 1;
}
================================================
FILE: werewolf-frontend/src/main.js
================================================
import { createApp } from "vue";
// import App from "./t.vue";
import App from "./App.vue";
import router from "./router";
import "./normalize.css";
import "./index.css";
createApp(App).use(router).mount("#app");
================================================
FILE: werewolf-frontend/src/normalize.css
================================================
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
/* Document
========================================================================== */
/**
* 1. Correct the line height in all browsers.
* 2. Prevent adjustments of font size after orientation changes in iOS.
*/
html {
line-height: 1.15; /* 1 */
-webkit-text-size-adjust: 100%; /* 2 */
}
/* Sections
========================================================================== */
/**
* Remove the margin in all browsers.
*/
body {
margin: 0;
}
/**
* Render the `main` element consistently in IE.
*/
main {
display: block;
}
/**
* Correct the font size and margin on `h1` elements within `section` and
* `article` contexts in Chrome, Firefox, and Safari.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/* Grouping content
========================================================================== */
/**
* 1. Add the correct box sizing in Firefox.
* 2. Show the overflow in Edge and IE.
*/
hr {
box-sizing: content-box; /* 1 */
height: 0; /* 1 */
overflow: visible; /* 2 */
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
pre {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/* Text-level semantics
========================================================================== */
/**
* Remove the gray background on active links in IE 10.
*/
a {
background-color: transparent;
}
/**
* 1. Remove the bottom border in Chrome 57-
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
*/
abbr[title] {
border-bottom: none; /* 1 */
text-decoration: underline; /* 2 */
text-decoration: underline dotted; /* 2 */
}
/**
* Add the correct font weight in Chrome, Edge, and Safari.
*/
b,
strong {
font-weight: bolder;
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/**
* Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` elements from affecting the line height in
* all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/* Embedded content
========================================================================== */
/**
* Remove the border on images inside links in IE 10.
*/
img {
border-style: none;
}
/* Forms
========================================================================== */
/**
* 1. Change the font styles in all browsers.
* 2. Remove the margin in Firefox and Safari.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit; /* 1 */
font-size: 100%; /* 1 */
line-height: 1.15; /* 1 */
margin: 0; /* 2 */
}
/**
* Show the overflow in IE.
* 1. Show the overflow in Edge.
*/
button,
input {
/* 1 */
overflow: visible;
}
/**
* Remove the inheritance of text transform in Edge, Firefox, and IE.
* 1. Remove the inheritance of text transform in Firefox.
*/
button,
select {
/* 1 */
text-transform: none;
}
/**
* Correct the inability to style clickable types in iOS and Safari.
*/
button,
[type="button"],
[type="reset"],
[type="submit"] {
-webkit-appearance: button;
}
/**
* Remove the inner border and padding in Firefox.
*/
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
border-style: none;
padding: 0;
}
/**
* Restore the focus styles unset by the previous rule.
*/
button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
outline: 1px dotted ButtonText;
}
/**
* Correct the padding in Firefox.
*/
fieldset {
padding: 0.35em 0.75em 0.625em;
}
/**
* 1. Correct the text wrapping in Edge and IE.
* 2. Correct the color inheritance from `fieldset` elements in IE.
* 3. Remove the padding so developers are not caught out when they zero out
* `fieldset` elements in all browsers.
*/
legend {
box-sizing: border-box; /* 1 */
color: inherit; /* 2 */
display: table; /* 1 */
max-width: 100%; /* 1 */
padding: 0; /* 3 */
white-space: normal; /* 1 */
}
/**
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
*/
progress {
vertical-align: baseline;
}
/**
* Remove the default vertical scrollbar in IE 10+.
*/
textarea {
overflow: auto;
}
/**
* 1. Add the correct box sizing in IE 10.
* 2. Remove the padding in IE 10.
*/
[type="checkbox"],
[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
* Correct the cursor style of increment and decrement buttons in Chrome.
*/
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Correct the odd appearance in Chrome and Safari.
* 2. Correct the outline style in Safari.
*/
[type="search"] {
-webkit-appearance: textfield; /* 1 */
outline-offset: -2px; /* 2 */
}
/**
* Remove the inner padding in Chrome and Safari on macOS.
*/
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* 1. Correct the inability to style clickable types in iOS and Safari.
* 2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button; /* 1 */
font: inherit; /* 2 */
}
/* Interactive
========================================================================== */
/*
* Add the correct display in Edge, IE 10+, and Firefox.
*/
details {
display: block;
}
/*
* Add the correct display in all browsers.
*/
summary {
display: list-item;
}
/* Misc
========================================================================== */
/**
* Add the correct display in IE 10+.
*/
template {
display: none;
}
/**
* Add the correct display in IE 10.
*/
[hidden] {
display: none;
}
================================================
FILE: werewolf-frontend/src/pages/CreateRoom.vue
================================================
<template>
<div class="createroom">
<span class="title">角色设置</span>
<!-- TODO 警长, 屠边屠城 -->
<div class="tile-wrapper">
<room-character-tile
:key="value"
v-for="(_, value) in characters"
:character="value"
></room-character-tile>
</div>
<div class="name">
<span class="hint">你的昵称:</span>
<use-border>
<input
:maxlength="10"
type="text"
placeholder="请输入昵称"
v-model="nickname"
/>
</use-border>
</div>
<div class="password">
<span class="hint">房间密码:</span>
<use-border>
<input
type="text"
:maxlength="20"
placeholder="(可选)"
v-model="password"
/>
</use-border>
</div>
<outlined-btn
@click="create()"
content="确认创建"
></outlined-btn>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import RoomCharacterTile from "../components/RoomCharacterTile.vue";
import OutlinedBtn from "../components/Btn.vue";
import UseBorder from "../components/UseBorder.vue";
import {
characters,
nickname,
password,
create,
} from "../reactivity/createRoom";
import { showDialog } from "../reactivity/dialog";
const CreateRoom = defineComponent({
name: "CreateRoom",
components: { RoomCharacterTile, OutlinedBtn, UseBorder },
setup(props) {
return {
characters,
nickname,
password,
create,
showDialog,
};
},
});
export default CreateRoom;
</script>
<style lang="scss" scoped>
.createroom {
padding: 1rem 1rem 0;
text-align: center;
.title {
font-size: 1.5rem;
font-weight: bold;
}
.tile-wrapper {
display: flex;
flex-wrap: wrap;
.room-character-tile {
flex: 1 1 33%;
padding: 1rem 0;
}
}
.name,
.password {
.hint {
position: relative;
bottom: 0.04em;
word-break: keep-all;
margin-right: 0.5rem;
font-weight: bold;
}
display: flex;
align-items: center;
justify-content: center;
margin: 2rem 0;
input {
max-width: calc(100% - 1rem);
padding: 0 0.5rem;
line-height: 2.4rem;
overflow: visible;
}
.useborder {
max-width: 50%;
}
}
.btn {
margin: auto;
}
}
</style>
================================================
FILE: werewolf-frontend/src/pages/Home.vue
================================================
<template>
<div class="main-page">
<img
:src="`/assets/werewolf${theme}.svg`"
alt="logo"
class="logo"
/>
<div class="title">狼人杀</div>
<Btn
@click="$router.push('joinRoom')"
content="加入房间"
></Btn>
<Btn
@click="$router.push('createRoom')"
content="创建房间"
></Btn>
<Btn @click="$router.push('review')" content="游戏记录"></Btn>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { theme } from "../reactivity/theme";
import Btn from "../components/Btn.vue";
const Home = defineComponent({
name: "Home",
components: { Btn },
setup(props) {
return { theme };
},
});
export default Home;
</script>
<style lang="scss" scoped>
@font-face {
font-family: gete;
src: url("../../public/assets/gete.ttf");
}
.main-page {
.logo {
padding-top: 10vh;
width: 18vh;
margin: auto;
display: block;
}
.title {
margin: 7vh;
text-align: center;
font-size: 3rem;
font-weight: bolder;
font-family: gete;
}
.btn {
display: block;
margin: 5vh auto;
text-align: center;
}
}
</style>
================================================
FILE: werewolf-frontend/src/pages/JoinRoom.vue
================================================
<template>
<div class="joinroom">
<div class="title">加入房间</div>
<div class="number">
<span class="hint">房号:</span>
<UseBorder>
<input maxlength="6" type="text" v-model="roomNumber" />
</UseBorder>
</div>
<div class="pw">
<span class="hint">密码:</span>
<UseBorder>
<input
maxlength="20"
type="text"
placeholder="(可选)"
v-model="password"
/>
</UseBorder>
</div>
<div class="name">
<span class="hint">昵称:</span>
<UseBorder>
<input
:maxlength="8"
type="text"
placeholder=""
v-model="nickname"
/>
</UseBorder>
</div>
<div class="spacer"></div>
<Btn @click="join" content="确认加入"></Btn>
</div>
</template>
<script lang="ts">
import { defineComponent, toRefs } from "vue";
import { useRouter } from "vue-router";
import UseBorder from "../components/UseBorder.vue";
import Btn from "../components/Btn.vue";
import {
password,
roomNumber,
nickname,
join,
} from "../reactivity/joinRoom";
const JoinRoom = defineComponent({
name: "JoinRoom",
components: { UseBorder, Btn },
props: {
pw: String,
number: String,
},
setup(props) {
const { pw, number } = toRefs(props);
if (pw && pw.value) password.value = pw.value;
if (number && number.value)
roomNumber.value = number.value.slice(0, 6);
return { password, roomNumber, nickname, join };
},
});
export default JoinRoom;
</script>
<style lang="scss" scoped>
.joinroom {
display: flex;
flex-direction: column;
align-items: center;
min-height: 90vh;
.title {
font-weight: bold;
font-size: 2rem;
padding: 2rem;
}
.pw,
.name,
.number {
.hint {
position: relative;
bottom: 0.08em;
word-break: keep-all;
margin-right: 0.5rem;
font-weight: bold;
}
display: flex;
align-items: center;
justify-content: center;
margin: 1rem 0;
input {
max-width: calc(100% - 1rem);
padding: 0 0.5rem;
line-height: 2.4rem;
overflow: visible;
}
.useborder {
max-width: 50%;
}
}
.spacer {
flex: 1;
}
.btn {
}
}
</style>
================================================
FILE: werewolf-frontend/src/pages/Play.vue
================================================
<template>
<div class="play">
<PlayerList :playerList="players"></PlayerList>
<div class="date">
Day {{ Math.ceil(date / 2) }}
<img
class="date-icon"
:src="`/assets/${
date % 2 === 0 ? 'moon' : 'sun'
}${theme}.svg`"
/>
</div>
<div class="game-status">{{ gameStatus }}</div>
<div class="game-status">
剩余时间:
{{
gameStatusTimeLeft < 0 ? "---" : gameStatusTimeLeft + "S"
}}
</div>
<div class="actions">
<Btn
:disabled="isActing"
@click="showCharacter = true"
content="查看角色"
></Btn>
<Btn
:disabled="isActing"
@click="showActions = true"
:class="{ active: canAct }"
content="显示操作"
></Btn>
<Btn
:disabled="isActing"
@click="showMemo = true"
content="备忘速记"
></Btn>
<Btn
:disabled="isActing"
@click="showEvents = true"
content="事件记录"
></Btn>
<Character></Character>
<Actions></Actions>
<Memo></Memo>
<Events></Events>
</div>
<BottomActions></BottomActions>
</div>
</template>
<script lang="ts">
import {
defineComponent,
onActivated,
onMounted,
onUnmounted,
} from "vue";
import PlayerList from "../components/RoomPlayerList.vue";
import Btn from "../components/Btn.vue";
import Events from "../components/PlayEvents.vue";
import Memo from "../components/PlayMemo.vue";
import Character from "../components/PlayCharacter.vue";
import Actions from "../components/PlayActions/index.vue";
import BottomActions from "../components/PlayBottomActions.vue";
import {
self,
character,
refresh,
players,
gameStatus,
date,
gameStatusTimeLeft,
} from "../reactivity/game";
import {
showMemo,
showActions,
showCharacter,
showEvents,
canAct,
} from "../reactivity/playPage";
import { theme } from "../reactivity/theme";
import { isActing } from "../reactivity/playAction";
import { joinRoom } from "../socket";
import { getToken } from "../utils/token";
import { showDialog } from "../reactivity/dialog";
import router from "../router";
import { roomNumber } from "../reactivity/joinRoom";
const Play = defineComponent({
name: "Play",
components: {
Btn,
PlayerList,
Memo,
Character,
Actions,
Events,
BottomActions,
},
setup(props) {
onMounted(() => {
const token = getToken();
if (token === null) {
showDialog("未加入房间或房间已过期!");
router.replace({ name: "home" });
} else {
roomNumber.value = token.roomNumber;
joinRoom(token.roomNumber);
refresh();
}
});
onActivated(refresh);
// 设定剩余时间每秒减一
let timer: NodeJS.Timeout;
onMounted(() => {
timer = setInterval(
() => (gameStatusTimeLeft.value -= 1),
1000
);
});
onUnmounted(() => clearInterval(timer));
return {
players,
self,
character,
showMemo,
showActions,
canAct,
showCharacter,
showEvents,
isActing,
gameStatus,
date,
theme,
gameStatusTimeLeft,
};
},
});
export default Play;
</script>
<style lang="scss" scoped>
.play {
text-align: center;
.actions {
display: flex;
justify-content: space-around;
flex-wrap: wrap;
.btn {
margin: 0.5rem;
}
}
.date,
.game-status {
font-weight: bold;
font-size: 1.5rem;
padding-bottom: 1.3rem;
}
.date {
display: flex;
align-items: center;
justify-content: center;
.date-icon {
width: 2.6rem;
margin: 0 1rem;
}
}
}
</style>
<style lang="scss">
.play {
@keyframes blink {
from {
background-color: var(--bg);
}
to {
background-color: var(--on-bg);
}
}
.btn {
position: relative;
&.active::after {
opacity: 1;
}
&::after {
transition: all 0.2s;
opacity: 0;
$size: 0.6rem;
content: "";
position: absolute;
right: -0.3 * $size;
top: -0.3 * $size;
width: $size;
height: $size;
background-color: var(--on-bg);
border: 2px solid var(--bg);
border-radius: 50%;
animation: blink 1s linear infinite alternate;
}
}
}
</style>
================================================
FILE: werewolf-frontend/src/pages/Review.vue
================================================
<template>
<div class="review">
<div class="title">游戏记录</div>
<div class="events" v-if="records.length">
<div
class="record-item"
v-for="recordBrief in records"
:key="recordBrief.time"
@click="
$router.push({
name: 'review-detail',
query: {
roomNumber: recordBrief.roomNumber,
time: recordBrief.time,
},
})
"
>
<div class="room-number info">
<span class="key">房间: </span>
{{ recordBrief.roomNumber }}
</div>
<div class="time info">
<span class="key">时间: </span>
{{ new Date(recordBrief.time).toLocaleString() }}
</div>
</div>
</div>
<div class="placeholder" v-else>
什么都没有呢
</div>
<Btn
:onClick="() => $router.push({ name: 'home' })"
content="返回主页"
></Btn>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, onMounted, ref } from "vue";
import Btn from "../components/Btn.vue";
import PlayEventList from "../components/PlayEventList.vue";
import { showDialog } from "../reactivity/dialog";
import { refresh } from "../reactivity/game";
import { useAllRecords } from "../reactivity/record";
const ReviewPage = defineComponent({
name: "ReviewPage",
components: { PlayEventList, Btn },
setup(props) {
const records = useAllRecords();
return {
showDialog,
records,
};
},
});
export default ReviewPage;
</script>
<style lang="scss" scoped>
.review {
text-align: center;
.title {
font-size: 2rem;
font-weight: bold;
padding: 1.5rem;
}
.events {
margin: 0 auto 3rem;
max-width: 95vw;
.record-item {
text-align: left;
padding: 0.4rem;
cursor: pointer;
.info {
margin: 0.2rem;
}
.key {
font-weight: bold;
}
}
}
.placeholder {
margin: 2rem 0 4rem;
opacity: 0.5;
}
}
</style>
================================================
FILE: werewolf-frontend/src/pages/ReviewDetail.vue
================================================
<template>
<div class="review-detail">
<h2 class="title">
对局记录
</h2>
<div class="review" v-if="record">
<div class="room-number info">
<span class="key">房间: </span>
{{ record.roomNumber }}
</div>
<div class="time info">
<span class="key">时间: </span>
{{ new Date(record.time).toLocaleString() }}
</div>
<div class="players">
<div
class="player-info"
v-for="p in record.playerBriefs"
:key="p.index"
>
<div class="info">
<span class="key">昵称: </span>
{{ p.name }}
</div>
<div class="info">
<span class="key">编号: </span>
{{ p.index }}
</div>
<div class="info">
<span class="key">角色: </span>
{{ ChineseNames[p.character] }}
</div>
</div>
</div>
<h3>详细记录</h3>
<PlayEventList
v-for="(events, day) in record.groupedGameEvents"
:key="day"
:day="day"
:events="events"
></PlayEventList>
</div>
<div v-else class="placeholder">
未找到对局记录
</div>
<Btn
:onClick="() => $router.push({ name: 'home' })"
content="返回主页"
></Btn>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { ChineseNames } from "../../shared/GameDefs";
import Btn from "../components/Btn.vue";
import PlayEventList from "../components/PlayEventList.vue";
import { useRecord } from "../reactivity/record";
const ReviewDetail = defineComponent({
name: "ReviewDetail",
components: { PlayEventList, Btn },
props: {
roomNumber: { type: String, required: true },
time: { type: Number, required: true },
},
setup(props) {
const record = useRecord(props.roomNumber, props.time);
return { record, ChineseNames };
},
});
export default ReviewDetail;
</script>
<style lang="scss" scoped>
.review-detail {
padding: 1rem;
.info {
.key {
font-weight: bold;
}
}
.room-number,
.time {
margin: 0.5rem 0;
font-size: 1.2rem;
}
.player-info {
margin: 0.7rem 0;
}
.btn {
margin: 2rem auto;
display: block;
text-align: center;
}
.placeholder {
margin: 2rem 0 4rem;
opacity: 0.5;
text-align: center;
}
}
</style>
================================================
FILE: werewolf-frontend/src/pages/WaitRoom.vue
================================================
<template>
<div class="waitroom">
<RoomPlayerList :playerList="playerList"></RoomPlayerList>
<div class="room-number">房间号:{{ number }}</div>
<div id="qr-code"></div>
<Btn
@click="gameBegin"
v-if="self.index === 1"
content="开始游戏"
class="wait-btn"
:disabled="!canBegin"
></Btn>
<Btn
class="wait-btn"
@click="showDialog('暂未实现')"
content="查看规则"
></Btn>
</div>
</template>
<script lang="ts">
import {
defineComponent,
toRefs,
onMounted,
computed,
} from "vue";
import QRCode from "easyqrcodejs";
import { CLIENT_BASE_URL } from "../../shared/constants";
import RoomPlayerList from "../components/RoomPlayerList.vue";
import Btn from "../components/Btn.vue";
import { showDialog } from "../reactivity/dialog";
import {
needingCharacters,
refresh,
self,
players,
} from "../reactivity/game";
import { gameBegin, initRoom } from "../http/room";
const WaitRoom = defineComponent({
name: "WaitRoom",
components: { RoomPlayerList, Btn },
props: {
pw: { type: String, required: false },
number: { type: String, required: true },
},
setup(props) {
const { pw, number } = toRefs(props);
onMounted(async () => {
new QRCode(document.getElementById("qr-code"), {
text: `${CLIENT_BASE_URL}/joinRoom?pw=${
pw && pw.value
}&number=${number && number.value}`,
logo: "/wolf.png",
logoWidth: 20,
logoHeight: 20,
width: 100,
height: 100,
});
const res = await initRoom({ roomNumber: number.value });
if (res && res.status === 200) {
players.value = res.data.players;
needingCharacters.value = res.data.needingCharacters;
}
refresh();
});
const playerList = computed(() => {
return new Array(needingCharacters.value.length)
.fill(0)
.map(
(_, ind) =>
players.value.find(
(player) => player.index === ind + 1
) || {
index: ind + 1,
}
);
});
const canBegin = computed(
() =>
needingCharacters.value.length === players.value.length
);
return { showDialog, playerList, self, gameBegin, canBegin };
},
});
export default WaitRoom;
</script>
<style lang="scss" scoped>
.waitroom {
#qr-code {
margin: 5vh auto;
width: min-content;
}
.room-number {
font-weight: bold;
font-size: 1.6rem;
text-align: center;
}
.btn {
display: block;
text-align: center;
margin: 1rem;
}
}
</style>
================================================
FILE: werewolf-frontend/src/reactivity/computeGameEvents.ts
================================================
import { computed } from "vue";
import {
CharacterEvent, GameEvent, GuardStatus, HunterStatus, PlayerDef, SeerStatus, WerewolfStatus,
WitchStatus
} from "../../shared/ModelDefs";
import { getVoteSituation, Vote } from "../utils/votes";
import { date, players, self } from "./game";
export const gameEvents = computed(() => {
let _gameEvents: GameEvent[] = [];
let _characterEvents: CharacterEvent[] = [];
/** 警长竞选投票 */
const sheriffVotes: Vote[] = [];
/** 下标为天数, value 为 Vote[] */
const exileVotes: Vote[][] = [];
players.value.forEach((p) => {
// 警长竞选投票
if (date.value !== 0)
sheriffVotes.push({
from: p.index,
voteAt: p.sheriffVotes[0],
});
// 传递警徽
p.sheriffVotes.slice(1).forEach((vote, at) => {
_gameEvents.push({
at,
character: "SHERIFF",
deed: `${p.index} 号将警徽传给了 ${vote} 号`,
});
});
// 放逐投票
for (let at = 1; at < date.value; at += 2) {
// 检查从第一天起每个白天的放逐投票
const voteAt = p.hasVotedAt[at];
exileVotes[at] = exileVotes[at] || [];
exileVotes[at].push({
from: p.index,
voteAt,
});
}
});
// 处理警长竞选投票结果
const sheriffVoteEvent: GameEvent = {
at: 0,
character: "SHERIFF",
deed: "",
};
Object.entries(getVoteSituation(sheriffVotes)).map(
([target, voters]) => {
const votersStr = voters.join(",");
if (target === "0") {
sheriffVoteEvent.deed += `警长投票中, ${votersStr} 弃票\n`;
} else {
sheriffVoteEvent.deed += `警长投票中, ${votersStr} 投给了 ${target}\n`;
}
}
);
if (sheriffVoteEvent.deed.length)
_gameEvents.push(sheriffVoteEvent);
// 处理白天投票结果
const exileVoteEvents = exileVotes.map<GameEvent>(
(votes, at) => {
const exileVoteEvent: GameEvent = {
at,
character: "VILLAGER",
deed: "",
};
Object.entries(getVoteSituation(votes)).map(
([target, voters]) => {
const votersStr = voters.join(",");
if (target === "0") {
exileVoteEvent.deed += `放逐投票中, ${votersStr} 弃票\n`;
} else {
exileVoteEvent.deed += `放逐投票中, ${votersStr} 投给了 ${target}\n`;
}
}
);
return exileVoteEvent;
}
);
_gameEvents = _gameEvents.concat(exileVoteEvents);
// 1. 游戏中, 渲染自己的角色行动
// 2. 游戏结束后可获得所有角色信息, 将他们都渲染出来
const playerDetails = players.value as PlayerDef[];
playerDetails.forEach((p) => {
if (self.value.index === p.index) {
_characterEvents.push(getEvents(self.value));
} else if (p.characterStatus) {
_characterEvents.push(getEvents(p));
}
});
return mergeEvents(_gameEvents, _characterEvents);
});
function mergeEvents(
gameEvents: GameEvent[],
characterEvents: CharacterEvent[]
): GameEvent[] {
return characterEvents
.reduce<GameEvent[]>(
(outPrev, cEvent) =>
cEvent.events.reduce<GameEvent[]>(
(innerPrev, eValue) => [
...innerPrev,
{
at: eValue.at,
character: cEvent.character,
deed: eValue.deed,
},
],
outPrev
),
[]
)
.concat(gameEvents)
.sort((e1, e2) => e1.at - e2.at);
}
export const groupedGameEvents = computed(() => {
const list: GameEvent[][] = [];
gameEvents.value.forEach((e) => {
const at = e.at;
const day = Math.ceil(at / 2);
list[day] ? void 0 : (list[day] = []);
list[day].push(e);
});
return list;
});
/**
* @param player 某个角色
* @returns 这个角色对应的 event对象列表
*/
function getEvents(player: PlayerDef): CharacterEvent {
const { character, characterStatus, index } = player;
const ret: CharacterEvent = {
character,
events: [],
};
switch (character) {
case "GUARD":
(characterStatus as GuardStatus).protects.forEach(
(index, at) => {
if (at % 2 === 0)
ret.events.push({
at,
deed:
index === undefined || index === null
? `${index} 号空守`
: `${index} 号保护了 ${index} 号玩家`,
});
}
);
break;
case "HUNTER":
const {
player,
day,
} = (characterStatus as HunterStatus).shootAt;
if (day !== -1)
ret.events.push({
at: day,
deed:
player === undefined || player === null
? `${index} 号没有开枪`
: `${index} 号射死了 ${player} 号玩家`,
});
break;
case "SEER":
(characterStatus as SeerStatus).checks.forEach(
(check, at) => {
if (at % 2 === 0)
ret.events.push({
at,
deed:
check === undefined || check === null
? `${index} 号没有查人`
: `${index} 号查验了 ${
check.index
} 号玩家,他是${
check.isWerewolf ? "狼人" : "良民"
}`,
});
}
);
break;
case "WEREWOLF":
(characterStatus as WerewolfStatus).wantToKills.forEach(
(kill, at) => {
if (at % 2 === 0)
ret.events.push({
at,
deed:
kill === undefined || kill === null
? `${index} 号放弃选择`
: `${index} 号投票想杀 ${kill} 号玩家`,
});
}
);
break;
case "WITCH":
const { MEDICINE, POISON } = characterStatus as WitchStatus;
if (POISON.usedDay !== -1)
ret.events.push({
at: POISON.usedDay,
deed: `${index} 号用毒药杀害了 ${POISON.usedAt} 号玩家`,
});
if (MEDICINE.usedDay !== -1)
ret.events.push({
at: MEDICINE.usedDay,
deed: `${index} 号用灵药复活了 ${MEDICINE.usedAt} 号玩家`,
});
break;
}
return ret;
}
================================================
FILE: werewolf-frontend/src/reactivity/createRoom.ts
================================================
import sha256 from "sha256";
import { reactive, ref } from "vue";
import { SetableCharacters } from "../../shared/GameDefs";
import { createRoom } from "../http/room";
import router from "../router";
import { joinRoom } from "../socket";
import { setToken } from "../utils/token";
import { showDialog } from "./dialog";
import { needingCharacters, players } from "./game";
/**
* 游戏人数配置(reactive)
*/
export const characters = reactive<
Record<SetableCharacters, number>
>({
GUARD: 1,
HUNTER: 1,
SEER: 1,
VILLAGER: 2,
WEREWOLF: 3,
WITCH: 1,
});
/**
* 设置游戏人数配置
* @param character 设置的对象
* @param type 设置增大还是减小
* @returns {boolean} 是否设置成功
*/
export function setCharacter(
character: SetableCharacters,
type: 1 | -1
): boolean {
if (characters[character] + type < 0) return false;
if (["SEER", "HUNTER", "GUARD", "WITCH"].includes(character)) {
if (type === 1 && characters[character] === 1) return false;
}
characters[character] += type;
return true;
}
/* 玩家信息 */
export const nickname = ref<string>("");
export const password = ref<string>("");
export async function create() {
if (!nickname.value) return showDialog("请填写昵称");
/* 设置人数配置 */
let characterNames: SetableCharacters[] = [];
Object.keys(characters).map((_name) => {
const name = _name as SetableCharacters;
characterNames = characterNames.concat(
new Array(characters[name]).fill(name)
);
});
needingCharacters.value = characterNames;
const res = await createRoom({
characters: characterNames,
name: nickname.value,
password: password.value ? sha256(password.value) : undefined,
});
if (res && res.status === 200) {
const data = res.data;
/* 通知后端, 在 io 中加入该房间 */
joinRoom(data.roomNumber);
showDialog("创建成功, 进入等待房间");
router.push({
name: "waitRoom",
query: {
pw: password.value,
number: data.roomNumber,
},
});
setToken(data.ID, data.roomNumber);
players.value = [
{
index: 1,
isAlive: true,
name: nickname.value,
isSheriff: false,
isDying: false,
hasVotedAt: [],
sheriffVotes: [],
},
];
}
}
================================================
FILE: werewolf-frontend/src/reactivity/dialog.ts
================================================
import { computed, ref } from "vue";
export const dialogTimeLeft = ref(0);
export const toShowContents = ref<
{ content: string; timeout: number }[]
>([]);
export const content = computed(() =>
toShowContents.value.length ? toShowContents.value[0] : null
);
/**
* 展示一个出现 showTime 秒数(默认5s) 的弹窗
* @param toShowContent 显示的文字(支持 html)
* @param showTime 显示的秒数
*/
export function showDialog(
toShowContent: string,
showTime?: number
) {
toShowContents.value.push({
content: toShowContent,
timeout: showTime || 5,
});
}
================================================
FILE: werewolf-frontend/src/reactivity/game.ts
================================================
import { computed, ref, Ref, watchEffect } from "vue";
import { Character, GameStatus, TIMEOUT } from "../../shared/GameDefs";
import {
CharacterStatus, day, GameEvent, PlayerDef, PublicPlayerDef
} from "../../shared/ModelDefs";
import { getGameStatus } from "../http/gameStatus";
/** 玩家的公开信息 */
export const players: Ref<PublicPlayerDef[]> = ref([]);
/** 角色配置 */
export const needingCharacters = ref<Character[]>([]);
/** 自己的详细状态 */
export const self = ref<PlayerDef>({
_id: "",
character: "",
hasVotedAt: [],
index: 0,
isAlive: false,
isSheriff: false,
name: "---",
sheriffVotes: [],
canBeVoted: false,
isDying: false,
});
/** 自己的角色 */
export const character = computed(() =>
self.value ? self.value.character : ""
);
/** 天数 */
export const date = ref<day>(-1);
/** 当前游戏进程 */
export const gameStatus = ref<GameStatus>(GameStatus.WOLF_KILL);
/** 当前状态还有多结束 */
export const gameStatusTimeLeft = ref(
TIMEOUT[GameStatus.WOLF_KILL]
);
/**
* gameStatus 被修改时调用, 改变 ui 状态, 弹出提示等
*/
/**
* 获得最新的游戏信息
*/
export async function refresh() {
const data = await getGameStatus({});
if (!data) return;
date.value = data.curDay;
gameStatus.value = data.gameStatus;
players.value = data.players;
self.value = data.self;
}
================================================
FILE: werewolf-frontend/src/reactivity/joinRoom.ts
================================================
import sha256 from "sha256";
import { ref } from "vue";
import { joinRoom } from "../http/room";
import router from "../router";
import { Events, joinRoom as joinRoomSocket } from "../socket";
import { getToken, setToken } from "../utils/token";
import { showDialog } from "./dialog";
import { needingCharacters } from "./game";
export const password = ref("");
export const roomNumber = ref("");
export const nickname = ref("");
export async function join() {
if (!roomNumber.value) return showDialog("请填写房间号");
if (!nickname.value) return showDialog("请填写昵称");
const res = await joinRoom({
roomNumber: roomNumber.value,
name: nickname.value,
password: password.value ? sha256(password.value) : undefined,
});
if (res && res.status === 200) {
/* 向后端 socket 注册加入房间 */
joinRoomSocket(roomNumber.value);
showDialog("成功加入房间!");
needingCharacters.value = res.data.needingCharacters;
router.push({
name: "waitRoom",
query: {
pw: password.value,
number: roomNumber.value,
},
});
setToken(res.data.ID, roomNumber.value);
}
}
export function gameBegin() {
/* 清空以前的备忘录 */
localStorage.removeItem("memo");
showDialog("游戏开始, 天黑请闭眼👁️");
setTimeout(() => {
router.push({
name: "play",
});
}, 500);
}
================================================
FILE: werewolf-frontend/src/reactivity/playAction.ts
================================================
import { ref } from "vue";
import { GameStatus, Potion } from "../../shared/GameDefs";
import { index } from "../../shared/ModelDefs";
import { characterAct } from "../http/action";
import { showDialog } from "./dialog";
import { gameStatus } from "./game";
export async function act() {
if (
potion.value === "POISON" &&
gameStatus.value === GameStatus.WITCH_ACT
)
target.value *= -1;
const res = await characterAct({
target: target.value,
});
// TODO deal with res
/* hide dialog */
isActing.value = false;
if (res && res.status === 200) {
if (res.data.isWolf !== undefined) {
showDialog(
`该玩家为${res.data.isWolf ? "狼人" : "人类"}`,
3
);
} else {
showDialog("操作成功!", 3);
}
}
/* reset */
potion.value = undefined;
target.value = 0;
noTarget.value = false;
}
export const isActing = ref(false);
export const noTarget = ref(false);
export const target = ref<index>(0);
export const potion = ref<Potion>();
export function setTarget(index: index) {
if (!isActing.value) return;
target.value = index;
}
================================================
FILE: werewolf-frontend/src/reactivity/playPage.ts
================================================
import { ref, watch } from "vue";
export const showMemo = ref(false);
export const memoContent = ref("");
watch(memoContent, () => {
localStorage.setItem("memo", memoContent.value);
});
export const showActions = ref(false);
export const showEvents = ref(false);
export const showCharacter = ref(true);
export const canAct = ref(false);
================================================
FILE: werewolf-frontend/src/reactivity/record.ts
================================================
/* 将游戏记录存在 localStorage 的相关操作 */
import { onMounted, ref, Ref } from "vue";
import { Character } from "../../shared/GameDefs";
import { GameEvent, index, PlayerDef, PublicPlayerDef } from "../../shared/ModelDefs";
const ROOM_NUMBER_PREFIX = "WERE_WOLF_ROOM";
interface RoomRecord extends RoomRecordBrief {
groupedGameEvents: GameEvent[][]; // 按天数归类的对局信息
playerBriefs: {
name: string;
index: index;
character: Character;
}[];
selfIndex: index;
}
const ROOM_LIST_KEY = "WERE_WOLF_ROOMS";
interface RoomRecordBrief {
time: number; // 游戏结束时的时间戳
roomNumber: string;
}
function getKeyByNumberNTime(
roomNumber: string,
time: number
): string {
return `${ROOM_NUMBER_PREFIX}-${roomNumber}-${time}`;
}
export function saveRecord(
groupedGameEvents: GameEvent[][],
roomNumber: string,
self: PlayerDef,
players: PlayerDef[],
time: number
) {
const recordBrief: RoomRecordBrief = {
roomNumber,
time,
};
const record: RoomRecord = {
groupedGameEvents,
playerBriefs: players.map((p) => ({
name: p.name,
index: p.index,
character: p.character,
})),
selfIndex: self.index,
...recordBrief,
};
localStorage.setItem(
getKeyByNumberNTime(roomNumber, time),
JSON.stringify(record)
);
const prevRoomListStr =
localStorage.getItem(ROOM_LIST_KEY) || "[]";
const roomList = JSON.parse(
prevRoomListStr
) as RoomRecordBrief[];
roomList.push(recordBrief);
localStorage.setItem(ROOM_LIST_KEY, JSON.stringify(roomList));
}
function getAllRecords(): RoomRecordBrief[] {
const prevRoomListStr =
localStorage.getItem(ROOM_LIST_KEY) || "[]";
return JSON.parse(prevRoomListStr) as RoomRecordBrief[];
}
export function useAllRecords(): Ref<RoomRecordBrief[]> {
const records = ref([]) as Ref<RoomRecordBrief[]>;
onMounted(() => {
records.value = getAllRecords();
});
return records;
}
function getRecordByNumberNTime(
roomNumber: string,
time: number
): RoomRecord | null {
const key = getKeyByNumberNTime(roomNumber, time);
const recordStr = localStorage.getItem(key);
if (!recordStr) return null;
return JSON.parse(recordStr) as RoomRecord;
}
export function useRecord(roomNumber: string, time: number) {
const record = ref(null) as Ref<RoomRecord | null>;
onMounted(() => {
record.value = getRecordByNumberNTime(roomNumber, time);
});
return record;
}
// TODO 加入摘要防止篡改localstorage?
// TODO try JSON.parse
================================================
FILE: werewolf-frontend/src/reactivity/theme.ts
================================================
import { computed } from "vue";
import { date } from "./game";
export const DARK = "-dark";
export const LIGHT = "";
export const theme = computed(() =>
date.value % 2 === 0 ? DARK : LIGHT
);
================================================
FILE: werewolf-frontend/src/router.ts
================================================
import { createRouter, createWebHistory } from "vue-router";
import CreateRoom from "./pages/CreateRoom.vue";
import Home from "./pages/Home.vue";
import JoinRoom from "./pages/JoinRoom.vue";
import Play from "./pages/Play.vue";
import Review from "./pages/Review.vue";
import ReviewDetail from "./pages/ReviewDetail.vue";
import WaitRoom from "./pages/WaitRoom.vue";
const routes = [
{ path: "/", name: "home", component: Home },
{
path: "/createRoom",
name: "createRoom",
component: CreateRoom,
},
{
path: "/joinRoom",
name: "joinRoom",
component: JoinRoom,
props: (route: any) => ({
pw: route.query.pw,
number: route.query.number,
}),
},
{ path: "/play", name: "play", component: Play },
{
path: "/review-detail",
name: "review-detail",
component: ReviewDetail,
props: (route: any) => ({
roomNumber: route.query.roomNumber,
time: route.query.time,
}),
},
{
path: "/review",
name: "review",
component: Review,
},
{
path: "/waitRoom",
name: "waitRoom",
component: WaitRoom,
props: (route: any) => ({
pw: route.query.pw,
number: route.query.number,
}),
},
];
const router = createRouter({
history: createWebHistory(),
routes,
});
export default router;
================================================
FILE: werewolf-frontend/src/socket/changeStatus.ts
================================================
import { Character, GameStatus, TIMEOUT } from "../../shared/GameDefs";
import { ChangeStatusMsg } from "../../shared/WSMsg/ChangeStatus";
import { getWolfKillResNShow, getWolfsNShow, witchGetDieNShow } from "../http/gameGetHint";
import { getGameStatus } from "../http/gameStatus";
import { date, gameStatus, gameStatusTimeLeft, refresh, self } from "../reactivity/game";
/* */
export default async function changeStatus(msg: ChangeStatusMsg) {
// console.log("# changeStatus", { msg });
date.value = msg.setDay;
gameStatus.value = msg.setStatus;
gameStatusTimeLeft.value = msg.timeout || TIMEOUT[msg.setStatus];
await refresh();
if (
msg.setStatus === GameStatus.WOLF_KILL_CHECK &&
self.value.character === "WEREWOLF"
) {
getWolfKillResNShow();
} else if (
msg.setStatus === GameStatus.WOLF_KILL &&
self.value.character === "WEREWOLF"
) {
getWolfsNShow();
} else if (
msg.setStatus === GameStatus.WITCH_ACT &&
self.value.character === "WITCH"
) {
witchGetDieNShow();
}
}
================================================
FILE: werewolf-frontend/src/socket/gameBegin.ts
================================================
import { gameBegin as begin } from "../reactivity/joinRoom";
export default function gameBegin() {
// console.log("#ws on game begin");
begin();
}
================================================
FILE: werewolf-frontend/src/socket/gameEnd.ts
================================================
import { PlayerDef } from "../../shared/ModelDefs";
import { GameEndMsg } from "../../shared/WSMsg/GameEnd";
import { groupedGameEvents } from "../reactivity/computeGameEvents";
import { showDialog } from "../reactivity/dialog";
import { players, refresh, self } from "../reactivity/game";
import { roomNumber } from "../reactivity/joinRoom";
import { saveRecord } from "../reactivity/record";
import router from "../router";
import { socket } from "./";
export default async function gameEnd(msg: GameEndMsg) {
socket.removeAllListeners();
socket.disconnect();
// console.log("# gameEnd", "end");
// TODO game over
await refresh();
const time = Date.now();
saveRecord(
groupedGameEvents.value,
roomNumber.value,
self.value,
players.value as PlayerDef[],
time
);
showDialog(
`<b>游戏结束</b> </br> 获胜者为${
msg.winner === "WEREWOLF" ? "狼人" : "村民"
}`
);
router.replace({
name: "review-detail",
query: {
roomNumber: roomNumber.value,
time,
},
});
}
================================================
FILE: werewolf-frontend/src/socket/index.ts
================================================
import io from "socket.io-client";
// const io = require("socket.io-client");
import { SERVER_DOMAIN, WS_PATH } from "../../shared/constants";
import { Events } from "../../shared/WSEvents";
// handlers
import changeStatus from "./changeStatus";
import gameBegin from "./gameBegin";
import gameEnd from "./gameEnd";
import roomJoin from "./roomJoin";
import showWSMsg from "./showWSMsg";
let socket: SocketIOClient.Socket;
function joinRoom(roomNumber: string) {
if (socket) {
socket.removeAllListeners();
socket.disconnect();
}
socket = io(SERVER_DOMAIN, {
path: WS_PATH,
});
socket.on("connection", () => {
// console.log("#ws connected");
});
socket.on(Events.CHANGE_STATUS, changeStatus);
socket.on(Events.GAME_BEGIN, gameBegin);
socket.on(Events.GAME_END, gameEnd);
socket.on(Events.ROOM_JOIN, roomJoin);
socket.on(Events.SHOW_MSG, showWSMsg);
socket.emit(Events.ROOM_JOIN, roomNumber);
}
export { joinRoom, Events, socket };
================================================
FILE: werewolf-frontend/src/socket/roomJoin.ts
================================================
import { RoomJoinMsg } from "../../shared/WSMsg/RoomJoin";
import { players } from "../reactivity/game";
export default function roomJoin(msg: RoomJoinMsg) {
// console.log("#ws on room join");
players.value = msg;
}
================================================
FILE: werewolf-frontend/src/socket/showWSMsg.ts
================================================
import { ShowMsg } from "../../shared/WSMsg/ShowMsg";
import { showDialog } from "../reactivity/dialog";
export default function showWSMsg(msg: ShowMsg) {
showDialog(msg.innerHTML, msg.showTime);
}
================================================
FILE: werewolf-frontend/src/utils/setObj.ts
================================================
/*
before:
editingItem._id = item._id;
editingItem.name = item.name;
editingItem.price = item.price;
editingItem.summary = item.summary;
editingItem.uid = item.uid;
editingItem.picture = item.picture;
after:
setObj(editingItem, item)
*/
export function setObj<T>(oldObj: T, newObj: T) {
const newObj_ = (newObj as unknown) as Record<string, any>;
const oldObj_ = (oldObj as unknown) as Record<string, any>;
const keys = Object.keys(newObj_);
keys.forEach((k) => {
oldObj_[k] = newObj_[k];
});
}
================================================
FILE: werewolf-frontend/src/utils/token.ts
================================================
import { TokenDef } from "../../shared/ModelDefs";
const KEY = "_werewolf_token_";
export function setToken(ID: string, roomNumber: string) {
const token: TokenDef = {
ID,
datetime: Date.now(),
roomNumber,
};
window.localStorage.setItem(KEY, JSON.stringify(token));
}
export function getToken(): TokenDef | null {
try {
const str = window.localStorage.getItem(KEY) || "@";
const token = JSON.parse(str) as TokenDef;
if (
typeof token.ID === "string" &&
typeof token.roomNumber === "string" &&
typeof token.datetime === "number"
) {
const dtDiff = Date.now() - token.datetime;
if (dtDiff / 1000 / 3600 / 24 < 1) {
return token;
} else {
// window.localStorage.removeItem(KEY);
// TODO remove token
}
}
} catch (error) {}
return null;
}
================================================
FILE: werewolf-frontend/src/utils/votes.ts
================================================
import { index } from "../../shared/ModelDefs";
/**
* 返回票型, key 投票的*目标*, value 为投给这个玩家的人
* 选择弃票的玩家的*目标*为 0
* @param votes 所有人投票的结果
*/
export function getVoteSituation(
votes: Vote[]
): VoteSituationRecord {
const voteSituation: VoteSituationRecord = {};
votes.forEach((v) => {
if (!v.voteAt) v.voteAt = 0;
voteSituation[v.voteAt] = voteSituation[v.voteAt] || [];
voteSituation[v.voteAt].push(v.from);
});
return voteSituation;
}
export interface Vote {
from: index;
/** 弃票则为 falsy 值 */
voteAt: index;
}
/** key 投票的*目标*, value 为投给这个玩家的人 */
type VoteSituationRecord = Record<index, index[]>;
================================================
FILE: werewolf-frontend/src/werewolf.d.ts
================================================
declare module "*.vue" {
import { ComponentOptions } from "vue";
const componentOptions: ComponentOptions;
export default componentOptions;
}
================================================
FILE: werewolf-frontend/tsconfig.json
================================================
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
// 这样就可以对 `this` 上的数据属性进行更严格的推断`
"strict": true,
"jsx": "preserve",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true
}
}
================================================
FILE: werewolf-frontend/vite.config.js
================================================
/**
* @type {import('vite').UserConfig}
*/
const config = {
// base: "/werewolf/game",
};
export default config;
gitextract_v_63r64i/
├── .vscode/
│ └── settings.json
├── LICENSE
├── README.md
├── docs/
│ ├── 笔记.md
│ └── 需求文档.md
├── reload.sh
├── werewolf-backend/
│ ├── .eslintrc
│ ├── .gitignore
│ ├── .vscode/
│ │ └── be.code-snippets
│ ├── ecosystem.config.js
│ ├── nodemon.json
│ ├── package.json
│ ├── src/
│ │ ├── handlers/
│ │ │ └── http/
│ │ │ ├── gameAct.ts
│ │ │ ├── gameActHandlers/
│ │ │ │ ├── BeforeDayDiscuss.ts
│ │ │ │ ├── DayDiscuss.ts
│ │ │ │ ├── ExileVote.ts
│ │ │ │ ├── ExileVoteCheck.ts
│ │ │ │ ├── GuardProtect.ts
│ │ │ │ ├── HunterCheck.ts
│ │ │ │ ├── HunterShoot.ts
│ │ │ │ ├── LeaveMsg.ts
│ │ │ │ ├── SeerCheck.ts
│ │ │ │ ├── SheriffAssign.ts
│ │ │ │ ├── SheriffAssignCheck.ts
│ │ │ │ ├── SheriffElect.ts
│ │ │ │ ├── SheriffSpeach.ts
│ │ │ │ ├── SheriffVote.ts
│ │ │ │ ├── SheriffVoteCheck.ts
│ │ │ │ ├── WitchAct.ts
│ │ │ │ ├── WolfKill.ts
│ │ │ │ ├── WolfKillCheck.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── validateIdentity.ts
│ │ │ ├── gameBegin.ts
│ │ │ ├── gameGetHint/
│ │ │ │ ├── getWolfs.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── witchGetDie.ts
│ │ │ │ └── wolfKill.ts
│ │ │ ├── gameStatus.ts
│ │ │ ├── roomCreate.ts
│ │ │ ├── roomInit.ts
│ │ │ └── roomJoin.ts
│ │ ├── index.ts
│ │ ├── middleware/
│ │ │ ├── auth.ts
│ │ │ └── handleError.ts
│ │ ├── models/
│ │ │ ├── PlayerModel.ts
│ │ │ └── RoomModel.ts
│ │ ├── routes/
│ │ │ ├── gameRoutes.ts
│ │ │ ├── index.ts
│ │ │ └── roomRoutes.ts
│ │ ├── utils/
│ │ │ ├── checkGameOver.ts
│ │ │ ├── getVoteResult.ts
│ │ │ └── renderHintNPlayers.ts
│ │ └── ws/
│ │ └── index.ts
│ └── tsconfig.json
└── werewolf-frontend/
├── .eslintrc.js
├── .gitignore
├── .vscode/
│ └── settings.json
├── README.md
├── index.html
├── package.json
├── shared/
│ ├── .vscode/
│ │ ├── WS Msg Model.code-snippets
│ │ └── http Msg Model.code-snippets
│ ├── GameDefs.ts
│ ├── ModelDefs.ts
│ ├── WSEvents.ts
│ ├── WSMsg/
│ │ ├── ChangeStatus.ts
│ │ ├── GameEnd.ts
│ │ ├── RoomExile.ts
│ │ ├── RoomJoin.ts
│ │ └── ShowMsg.ts
│ ├── constants.ts
│ └── httpMsg/
│ ├── CharacterAct.ts
│ ├── CreateRoomMsg.ts
│ ├── GameStatusMsg.ts
│ ├── InitRoomMsg.ts
│ ├── JoinRoomMsg.ts
│ ├── SeerCheckMsg.ts
│ └── _httpResTemplate.ts
├── src/
│ ├── App.vue
│ ├── components/
│ │ ├── Avatar.vue
│ │ ├── Btn.vue
│ │ ├── Dialog.vue
│ │ ├── PlayActions/
│ │ │ ├── ActionBtn.vue
│ │ │ ├── commonAction.ts
│ │ │ ├── index.vue
│ │ │ └── renderActionList.ts
│ │ ├── PlayBottomActions.vue
│ │ ├── PlayCharacter.vue
│ │ ├── PlayEventList.vue
│ │ ├── PlayEventTile.vue
│ │ ├── PlayEvents.vue
│ │ ├── PlayMemo.vue
│ │ ├── RoomCharacterTile.vue
│ │ ├── RoomPlayerList.vue
│ │ ├── UseBorder.vue
│ │ └── UseMenu.vue
│ ├── http/
│ │ ├── _request.ts
│ │ ├── action.ts
│ │ ├── gameGetHint.ts
│ │ ├── gameStatus.ts
│ │ └── room.ts
│ ├── index.css
│ ├── main.js
│ ├── normalize.css
│ ├── pages/
│ │ ├── CreateRoom.vue
│ │ ├── Home.vue
│ │ ├── JoinRoom.vue
│ │ ├── Play.vue
│ │ ├── Review.vue
│ │ ├── ReviewDetail.vue
│ │ └── WaitRoom.vue
│ ├── reactivity/
│ │ ├── computeGameEvents.ts
│ │ ├── createRoom.ts
│ │ ├── dialog.ts
│ │ ├── game.ts
│ │ ├── joinRoom.ts
│ │ ├── playAction.ts
│ │ ├── playPage.ts
│ │ ├── record.ts
│ │ └── theme.ts
│ ├── router.ts
│ ├── socket/
│ │ ├── changeStatus.ts
│ │ ├── gameBegin.ts
│ │ ├── gameEnd.ts
│ │ ├── index.ts
│ │ ├── roomJoin.ts
│ │ └── showWSMsg.ts
│ ├── utils/
│ │ ├── setObj.ts
│ │ ├── token.ts
│ │ └── votes.ts
│ └── werewolf.d.ts
├── tsconfig.json
└── vite.config.js
SYMBOL INDEX (172 symbols across 66 files)
FILE: werewolf-backend/src/handlers/http/gameActHandlers/BeforeDayDiscuss.ts
method handleHttpInTheState (line 18) | async handleHttpInTheState(
method startOfState (line 32) | startOfState(room: Room) {
method endOfState (line 88) | async endOfState(room: Room, dyingPlayers: Player[]) {
FILE: werewolf-backend/src/handlers/http/gameActHandlers/DayDiscuss.ts
method handleHttpInTheState (line 15) | async handleHttpInTheState(
method startOfState (line 34) | startOfState(room: Room) {
method endOfState (line 38) | async endOfState(room: Room) {
FILE: werewolf-backend/src/handlers/http/gameActHandlers/ExileVote.ts
method handleHttpInTheState (line 20) | async handleHttpInTheState(
method startOfState (line 41) | startOfState(room: Room) {
method endOfState (line 45) | async endOfState(room: Room) {
FILE: werewolf-backend/src/handlers/http/gameActHandlers/ExileVoteCheck.ts
method handleHttpInTheState (line 14) | async handleHttpInTheState(
method endOfState (line 35) | async endOfState(room: Room, nextState: GameStatus) {
FILE: werewolf-backend/src/handlers/http/gameActHandlers/GuardProtect.ts
method handleHttpInTheState (line 20) | async handleHttpInTheState(
method startOfState (line 73) | startOfState(room: Room) {
method endOfState (line 81) | async endOfState(room: Room) {
FILE: werewolf-backend/src/handlers/http/gameActHandlers/HunterCheck.ts
method handleHttpInTheState (line 21) | async handleHttpInTheState(
method startOfState (line 34) | startOfState(room: Room) {
method endOfState (line 38) | async endOfState(room: Room) {
FILE: werewolf-backend/src/handlers/http/gameActHandlers/HunterShoot.ts
method handleHttpInTheState (line 21) | async handleHttpInTheState(
method startOfState (line 59) | startOfState(room) {
method endOfState (line 71) | async endOfState(room, showHunter: boolean) {
function showHunter (line 103) | function showHunter(room: Room): boolean {
FILE: werewolf-backend/src/handlers/http/gameActHandlers/LeaveMsg.ts
method handleHttpInTheState (line 17) | async handleHttpInTheState(
method startOfState (line 33) | startOfState(room) {
method endOfState (line 52) | async endOfState(room) {
FILE: werewolf-backend/src/handlers/http/gameActHandlers/SeerCheck.ts
method handleHttpInTheState (line 20) | async handleHttpInTheState(
method startOfState (line 52) | startOfState(room: Room) {
method endOfState (line 60) | async endOfState(room: Room) {
FILE: werewolf-backend/src/handlers/http/gameActHandlers/SheriffAssign.ts
method handleHttpInTheState (line 22) | async handleHttpInTheState(
method startOfState (line 40) | startOfState(room) {
method endOfState (line 54) | async endOfState(room, showSheriff: boolean = true) {
FILE: werewolf-backend/src/handlers/http/gameActHandlers/SheriffAssignCheck.ts
method handleHttpInTheState (line 16) | async handleHttpInTheState(
method startOfState (line 29) | startOfState(room: Room) {
method endOfState (line 33) | async endOfState(room: Room) {
FILE: werewolf-backend/src/handlers/http/gameActHandlers/SheriffElect.ts
method handleHttpInTheState (line 19) | async handleHttpInTheState(
method startOfState (line 35) | startOfState(room: Room) {
method endOfState (line 40) | async endOfState(room: Room) {
FILE: werewolf-backend/src/handlers/http/gameActHandlers/SheriffSpeach.ts
method handleHttpInTheState (line 18) | async handleHttpInTheState(
method startOfState (line 39) | startOfState(room: Room) {
method endOfState (line 43) | async endOfState(room: Room) {
FILE: werewolf-backend/src/handlers/http/gameActHandlers/SheriffVote.ts
method handleHttpInTheState (line 20) | async handleHttpInTheState(
method startOfState (line 41) | startOfState(room: Room) {
method endOfState (line 45) | async endOfState(room: Room) {
FILE: werewolf-backend/src/handlers/http/gameActHandlers/SheriffVoteCheck.ts
method handleHttpInTheState (line 17) | async handleHttpInTheState(
method startOfState (line 30) | startOfState(room: Room) {
method endOfState (line 34) | async endOfState(room: Room) {
FILE: werewolf-backend/src/handlers/http/gameActHandlers/WitchAct.ts
method handleHttpInTheState (line 18) | async handleHttpInTheState(
method startOfState (line 86) | startOfState(room: Room) {
method endOfState (line 94) | async endOfState(room: Room) {
FILE: werewolf-backend/src/handlers/http/gameActHandlers/WolfKill.ts
method handleHttpInTheState (line 18) | async handleHttpInTheState(
method startOfState (line 36) | startOfState(room: Room, showCloseEye = true) {
method endOfState (line 45) | async endOfState(room: Room) {
FILE: werewolf-backend/src/handlers/http/gameActHandlers/WolfKillCheck.ts
method handleHttpInTheState (line 16) | async handleHttpInTheState(
method startOfState (line 29) | startOfState(room: Room) {
method endOfState (line 33) | async endOfState(room: Room) {
FILE: werewolf-backend/src/handlers/http/gameActHandlers/index.ts
type Response (line 30) | interface Response<T = {}> {
type GameActHandler (line 36) | interface GameActHandler {
function startCurrentState (line 103) | function startCurrentState(
function gotoNextStateAfterHandleDie (line 133) | function gotoNextStateAfterHandleDie(room: Room) {
FILE: werewolf-backend/src/handlers/http/gameActHandlers/validateIdentity.ts
function validateIdentity (line 11) | function validateIdentity(
FILE: werewolf-backend/src/middleware/handleError.ts
function createError (line 32) | function createError({
FILE: werewolf-backend/src/models/PlayerModel.ts
class Player (line 7) | class Player implements PlayerDef {
method constructor (line 26) | constructor({ name, index }: { name: string; index: number }) {
method getPublic (line 40) | getPublic(room: Room): PublicPlayerDef {
FILE: werewolf-backend/src/models/RoomModel.ts
class Room (line 16) | class Room implements RoomDef {
method curStatus (line 26) | get curStatus(): GameStatus {
method constructor (line 41) | constructor({
method playerJoin (line 86) | playerJoin(name: string, password?: string): Player {
method choosePublicInfo (line 105) | choosePublicInfo(): PublicPlayerDef[] {
method getPlayerById (line 111) | getPlayerById(id: string): Player {
method getPlayerByIndex (line 117) | getPlayerByIndex(index: index): Player {
method getRoom (line 124) | static getRoom(number: string): Room {
method clearRoom (line 132) | static clearRoom(number: string): void {
function checkNeedingCharacters (line 137) | function checkNeedingCharacters(
FILE: werewolf-backend/src/utils/checkGameOver.ts
constant CLEAR_ROOM_TIME (line 6) | const CLEAR_ROOM_TIME = 3600 * 1000;
function checkGameOver (line 12) | function checkGameOver(room: Room): boolean {
FILE: werewolf-backend/src/utils/getVoteResult.ts
function getVoteResult (line 7) | function getVoteResult(votes: Vote[]): index[] | null {
function getVoteSituation (line 42) | function getVoteSituation(
type Vote (line 57) | interface Vote {
type VoteSituationRecord (line 64) | type VoteSituationRecord = Record<index, index[]>;
FILE: werewolf-backend/src/utils/renderHintNPlayers.ts
function renderHintNPlayers (line 9) | function renderHintNPlayers(
FILE: werewolf-backend/src/ws/index.ts
function setup (line 5) | function setup(io: Server) {
FILE: werewolf-frontend/shared/GameDefs.ts
type SetableCharacters (line 3) | type SetableCharacters =
type Character (line 11) | type Character =
type Potion (line 17) | type Potion = "POISON" | "MEDICINE";
type GameStatus (line 53) | enum GameStatus {
type StatusWithAction (line 90) | type StatusWithAction =
constant TIMEOUT (line 105) | const TIMEOUT: Record<GameStatus, number> = {
FILE: werewolf-frontend/shared/ModelDefs.ts
type ID (line 3) | type ID = string;
type index (line 4) | type index = number;
type day (line 6) | type day = number;
type RoomDef (line 8) | interface RoomDef {
type PublicPlayerDef (line 22) | interface PublicPlayerDef {
type PlayerDef (line 35) | interface PlayerDef extends PublicPlayerDef {
type TokenDef (line 48) | interface TokenDef {
type HunterStatus (line 54) | interface HunterStatus {
type GuardStatus (line 61) | interface GuardStatus {
type SeerStatus (line 65) | interface SeerStatus {
type WerewolfStatus (line 72) | interface WerewolfStatus {
type PotionStatus (line 76) | interface PotionStatus {
type WitchStatus (line 81) | type WitchStatus = Record<Potion, PotionStatus>;
type CharacterStatus (line 83) | type CharacterStatus = Partial<
type CharacterEvent (line 91) | interface CharacterEvent {
type GameEvent (line 99) | type GameEvent = {
FILE: werewolf-frontend/shared/WSEvents.ts
type Events (line 1) | enum Events {
FILE: werewolf-frontend/shared/WSMsg/ChangeStatus.ts
type ChangeStatusMsg (line 4) | interface ChangeStatusMsg {
FILE: werewolf-frontend/shared/WSMsg/GameEnd.ts
type GameEndMsg (line 7) | interface GameEndMsg {
FILE: werewolf-frontend/shared/WSMsg/RoomExile.ts
type RoomExileMsg (line 6) | type RoomExileMsg = PublicPlayerDef[];
FILE: werewolf-frontend/shared/WSMsg/RoomJoin.ts
type RoomJoinMsg (line 3) | type RoomJoinMsg = PublicPlayerDef[];
FILE: werewolf-frontend/shared/WSMsg/ShowMsg.ts
type ShowMsg (line 1) | interface ShowMsg {
FILE: werewolf-frontend/shared/constants.ts
constant CLIENT_BASE_URL (line 1) | const CLIENT_BASE_URL =
constant SERVER_DOMAIN (line 4) | const SERVER_DOMAIN =
constant SERVER_BASE_URL (line 8) | const SERVER_BASE_URL =SERVER_DOMAIN + "/api"
constant WS_PATH_CLIPED (line 10) | const WS_PATH_CLIPED = "/werewolf-ws";
constant WS_PATH (line 11) | const WS_PATH = "/api" + WS_PATH_CLIPED;
FILE: werewolf-frontend/shared/httpMsg/CharacterAct.ts
type CharacterAct (line 3) | interface CharacterAct {
FILE: werewolf-frontend/shared/httpMsg/CreateRoomMsg.ts
type CreateRoomRequest (line 5) | interface CreateRoomRequest {
type CreateRoomResponse (line 11) | type CreateRoomResponse = HttpRes<{
FILE: werewolf-frontend/shared/httpMsg/GameStatusMsg.ts
type GameStatusRequest (line 6) | interface GameStatusRequest {}
type GameStatusResponse (line 8) | type GameStatusResponse = {
FILE: werewolf-frontend/shared/httpMsg/InitRoomMsg.ts
type InitRoomRequest (line 5) | interface InitRoomRequest {}
type InitRoomResponse (line 7) | type InitRoomResponse = HttpRes<{
FILE: werewolf-frontend/shared/httpMsg/JoinRoomMsg.ts
type JoinRoomRequest (line 5) | interface JoinRoomRequest {
type JoinRoomResponse (line 11) | type JoinRoomResponse = HttpRes<{
FILE: werewolf-frontend/shared/httpMsg/SeerCheckMsg.ts
type SeerCheckRequest (line 5) | interface SeerCheckRequest extends CharacterAct {}
type SeerCheckData (line 7) | type SeerCheckData = {
FILE: werewolf-frontend/shared/httpMsg/_httpResTemplate.ts
type HttpRes (line 1) | interface HttpRes<T = {}> {
FILE: werewolf-frontend/src/components/PlayActions/commonAction.ts
function commonAction (line 5) | function commonAction(no_target: boolean) {
FILE: werewolf-frontend/src/http/_request.ts
function request (line 8) | function request<T = {}>(
FILE: werewolf-frontend/src/http/action.ts
function characterAct (line 6) | async function characterAct(
FILE: werewolf-frontend/src/http/gameGetHint.ts
function getWolfKillResNShow (line 8) | async function getWolfKillResNShow(): Promise<boolean> {
function getWolfsNShow (line 25) | async function getWolfsNShow(): Promise<boolean> {
function witchGetDieNShow (line 42) | async function witchGetDieNShow(): Promise<boolean> {
FILE: werewolf-frontend/src/http/gameStatus.ts
function getGameStatus (line 4) | async function getGameStatus(
FILE: werewolf-frontend/src/http/room.ts
function createRoom (line 6) | async function createRoom(
function joinRoom (line 18) | async function joinRoom(
function initRoom (line 30) | async function initRoom(
function gameBegin (line 42) | async function gameBegin(): Promise<boolean> {
FILE: werewolf-frontend/src/reactivity/computeGameEvents.ts
function mergeEvents (line 105) | function mergeEvents(
function getEvents (line 144) | function getEvents(player: PlayerDef): CharacterEvent {
FILE: werewolf-frontend/src/reactivity/createRoom.ts
function setCharacter (line 32) | function setCharacter(
function create (line 48) | async function create() {
FILE: werewolf-frontend/src/reactivity/dialog.ts
function showDialog (line 16) | function showDialog(
FILE: werewolf-frontend/src/reactivity/game.ts
function refresh (line 45) | async function refresh() {
FILE: werewolf-frontend/src/reactivity/joinRoom.ts
function join (line 15) | async function join() {
function gameBegin (line 43) | function gameBegin() {
FILE: werewolf-frontend/src/reactivity/playAction.ts
function act (line 9) | async function act() {
function setTarget (line 46) | function setTarget(index: index) {
FILE: werewolf-frontend/src/reactivity/record.ts
constant ROOM_NUMBER_PREFIX (line 8) | const ROOM_NUMBER_PREFIX = "WERE_WOLF_ROOM";
type RoomRecord (line 9) | interface RoomRecord extends RoomRecordBrief {
constant ROOM_LIST_KEY (line 19) | const ROOM_LIST_KEY = "WERE_WOLF_ROOMS";
type RoomRecordBrief (line 20) | interface RoomRecordBrief {
function getKeyByNumberNTime (line 25) | function getKeyByNumberNTime(
function saveRecord (line 32) | function saveRecord(
function getAllRecords (line 68) | function getAllRecords(): RoomRecordBrief[] {
function useAllRecords (line 74) | function useAllRecords(): Ref<RoomRecordBrief[]> {
function getRecordByNumberNTime (line 83) | function getRecordByNumberNTime(
function useRecord (line 96) | function useRecord(roomNumber: string, time: number) {
FILE: werewolf-frontend/src/reactivity/theme.ts
constant DARK (line 4) | const DARK = "-dark";
constant LIGHT (line 5) | const LIGHT = "";
FILE: werewolf-frontend/src/socket/changeStatus.ts
function changeStatus (line 9) | async function changeStatus(msg: ChangeStatusMsg) {
FILE: werewolf-frontend/src/socket/gameBegin.ts
function gameBegin (line 3) | function gameBegin() {
FILE: werewolf-frontend/src/socket/gameEnd.ts
function gameEnd (line 11) | async function gameEnd(msg: GameEndMsg) {
FILE: werewolf-frontend/src/socket/index.ts
function joinRoom (line 15) | function joinRoom(roomNumber: string) {
FILE: werewolf-frontend/src/socket/roomJoin.ts
function roomJoin (line 5) | function roomJoin(msg: RoomJoinMsg) {
FILE: werewolf-frontend/src/socket/showWSMsg.ts
function showWSMsg (line 4) | function showWSMsg(msg: ShowMsg) {
FILE: werewolf-frontend/src/utils/setObj.ts
function setObj (line 17) | function setObj<T>(oldObj: T, newObj: T) {
FILE: werewolf-frontend/src/utils/token.ts
constant KEY (line 3) | const KEY = "_werewolf_token_";
function setToken (line 5) | function setToken(ID: string, roomNumber: string) {
function getToken (line 14) | function getToken(): TokenDef | null {
FILE: werewolf-frontend/src/utils/votes.ts
function getVoteSituation (line 8) | function getVoteSituation(
type Vote (line 23) | interface Vote {
type VoteSituationRecord (line 30) | type VoteSituationRecord = Record<index, index[]>;
Condensed preview — 134 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (206K chars).
[
{
"path": ".vscode/settings.json",
"chars": 1021,
"preview": "{\n \"workbench.colorCustomizations\": {\n \"activityBar.activeBackground\": \"#73dd26\",\n \"activityBar.activeB"
},
{
"path": "LICENSE",
"chars": 1064,
"preview": "MIT License\n\nCopyright (c) 2021 xiong35\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof"
},
{
"path": "README.md",
"chars": 3928,
"preview": "<div align=\"center\" id=\"top\">\n <img width=\"100\" src=\"werewolf-frontend/public/wolf.png\" alt=\"Werewolf Logo\" />\n\n <!-- "
},
{
"path": "docs/笔记.md",
"chars": 437,
"preview": "\n当后端某个状态的定时器到点了\n\n1. 通知所有人下一个状态是什么, 前端设置状态\n2. 同时告知所有人存活情况(在进入白天时还会**通知今晚谁死了**)\n3. 若是玩家参与的多人共同操作状态结束了\n 1. 进入查看结果的状态\n 2"
},
{
"path": "docs/需求文档.md",
"chars": 1340,
"preview": "\n# 需求文档\n\n- [ ] 编写需求文档\n\n## 狼人杀流程\n\n> 每人都只有 15 s 操作, 可提前结束, 死人操作时间随机\n\n每次死人检查是不是警长\n\n1. 设置神职, 设置人数, 设置屠边屠城\n2. 发身份, 看身份\n3. 第一天"
},
{
"path": "reload.sh",
"chars": 136,
"preview": "#!/bin/bash\n\ngit pull\n\ncd werewolf-frontend\n\nnpm i\nnpm run build\n\ncd ../werewolf-backend\n\nnpm i\nnpm run build\nnpm run st"
},
{
"path": "werewolf-backend/.eslintrc",
"chars": 24,
"preview": "{\n \"extends\": \"koa\"\n}"
},
{
"path": "werewolf-backend/.gitignore",
"chars": 27,
"preview": "node_modules\nt.*\ndist/\nlog/"
},
{
"path": "werewolf-backend/.vscode/be.code-snippets",
"chars": 848,
"preview": "{\n\t\"create a http handler\": {\n\t\t\"scope\": \"typescript\",\n\t\t\"prefix\": \"h#\",\n\t\t\"body\": [\n\t\t\t\"import { Middleware } from \\\"ko"
},
{
"path": "werewolf-backend/ecosystem.config.js",
"chars": 370,
"preview": "const { name } = require(\"./package.json\");\nconst path = require(\"path\");\n\nmodule.exports = {\n apps: [\n {\n name"
},
{
"path": "werewolf-backend/nodemon.json",
"chars": 118,
"preview": "{\n \"watch\": [\n \"src\"\n ],\n \"ext\": \"ts\",\n \"exec\": \"ts-node -r tsconfig-paths/register src/index.ts\"\n}"
},
{
"path": "werewolf-backend/package.json",
"chars": 1249,
"preview": "{\n \"name\": \"werewolf-backend\",\n \"version\": \"1.0.0\",\n \"description\": \"\",\n \"main\": \"index.js\",\n \"scripts\": {\n \"dev"
},
{
"path": "werewolf-backend/src/handlers/http/gameAct.ts",
"chars": 1254,
"preview": "import { Middleware } from \"koa\";\n\nimport { IDHeaderName, RoomNumberHeaderName } from \"../../../../werewolf-frontend/sha"
},
{
"path": "werewolf-backend/src/handlers/http/gameActHandlers/BeforeDayDiscuss.ts",
"chars": 3612,
"preview": "import { Context } from \"koa\";\n\nimport io from \"../../../\";\nimport { GameStatus, TIMEOUT } from \"../../../../../werewolf"
},
{
"path": "werewolf-backend/src/handlers/http/gameActHandlers/DayDiscuss.ts",
"chars": 1242,
"preview": "import { Context } from \"koa\";\n\nimport { GameStatus, TIMEOUT } from \"../../../../../werewolf-frontend/shared/GameDefs\";\n"
},
{
"path": "werewolf-backend/src/handlers/http/gameActHandlers/ExileVote.ts",
"chars": 3874,
"preview": "import { Context } from \"koa\";\n\nimport io from \"../../..\";\nimport { GameStatus, TIMEOUT } from \"../../../../../werewolf-"
},
{
"path": "werewolf-backend/src/handlers/http/gameActHandlers/ExileVoteCheck.ts",
"chars": 1166,
"preview": "import { Context } from \"koa\";\n\nimport { GameStatus, TIMEOUT } from \"../../../../../werewolf-frontend/shared/GameDefs\";\n"
},
{
"path": "werewolf-backend/src/handlers/http/gameActHandlers/GuardProtect.ts",
"chars": 2731,
"preview": "import { Context } from \"koa\";\n\nimport io from \"../../..\";\nimport { GameStatus, TIMEOUT } from \"../../../../../werewolf-"
},
{
"path": "werewolf-backend/src/handlers/http/gameActHandlers/HunterCheck.ts",
"chars": 1352,
"preview": "import { Context } from \"koa\";\n\nimport io from \"../../..\";\nimport { GameStatus, TIMEOUT } from \"../../../../../werewolf-"
},
{
"path": "werewolf-backend/src/handlers/http/gameActHandlers/HunterShoot.ts",
"chars": 3513,
"preview": "import { Context } from \"koa\";\n\nimport io from \"../../..\";\nimport { GameStatus, TIMEOUT } from \"../../../../../werewolf-"
},
{
"path": "werewolf-backend/src/handlers/http/gameActHandlers/LeaveMsg.ts",
"chars": 1561,
"preview": "import { Context } from \"koa\";\n\nimport io from \"../../..\";\nimport { GameStatus, TIMEOUT } from \"../../../../../werewolf-"
},
{
"path": "werewolf-backend/src/handlers/http/gameActHandlers/SeerCheck.ts",
"chars": 2041,
"preview": "import { Context } from \"koa\";\n\nimport io from \"../../..\";\nimport { GameStatus, TIMEOUT } from \"../../../../../werewolf-"
},
{
"path": "werewolf-backend/src/handlers/http/gameActHandlers/SheriffAssign.ts",
"chars": 2441,
"preview": "import { Context } from \"koa\";\n\nimport io from \"../../..\";\nimport { GameStatus, TIMEOUT } from \"../../../../../werewolf-"
},
{
"path": "werewolf-backend/src/handlers/http/gameActHandlers/SheriffAssignCheck.ts",
"chars": 1103,
"preview": "import { Context } from \"koa\";\n\nimport io from \"../../..\";\nimport { GameStatus, TIMEOUT } from \"../../../../../werewolf-"
},
{
"path": "werewolf-backend/src/handlers/http/gameActHandlers/SheriffElect.ts",
"chars": 2283,
"preview": "import { Context } from \"koa\";\n\nimport io from \"../../..\";\nimport { GameStatus, TIMEOUT } from \"../../../../../werewolf-"
},
{
"path": "werewolf-backend/src/handlers/http/gameActHandlers/SheriffSpeach.ts",
"chars": 1387,
"preview": "import { Context } from \"koa\";\n\nimport io from \"../../..\";\nimport { GameStatus, TIMEOUT } from \"../../../../../werewolf-"
},
{
"path": "werewolf-backend/src/handlers/http/gameActHandlers/SheriffVote.ts",
"chars": 3050,
"preview": "import { Context } from \"koa\";\n\nimport io from \"../../..\";\nimport { GameStatus, TIMEOUT } from \"../../../../../werewolf-"
},
{
"path": "werewolf-backend/src/handlers/http/gameActHandlers/SheriffVoteCheck.ts",
"chars": 1141,
"preview": "import { Context } from \"koa\";\n\nimport io from \"../../..\";\nimport { GameStatus, TIMEOUT } from \"../../../../../werewolf-"
},
{
"path": "werewolf-backend/src/handlers/http/gameActHandlers/WitchAct.ts",
"chars": 2629,
"preview": "import { Context } from \"koa\";\n\nimport io from \"../../..\";\nimport { GameStatus, TIMEOUT } from \"../../../../../werewolf-"
},
{
"path": "werewolf-backend/src/handlers/http/gameActHandlers/WolfKill.ts",
"chars": 2608,
"preview": "import { Context } from \"koa\";\n\nimport io from \"../../..\";\nimport { GameStatus, TIMEOUT } from \"../../../../../werewolf-"
},
{
"path": "werewolf-backend/src/handlers/http/gameActHandlers/WolfKillCheck.ts",
"chars": 1068,
"preview": "import { Context } from \"koa\";\n\nimport io from \"../../..\";\nimport { GameStatus, TIMEOUT } from \"../../../../../werewolf-"
},
{
"path": "werewolf-backend/src/handlers/http/gameActHandlers/index.ts",
"chars": 5099,
"preview": "import { Context } from \"koa\";\n\nimport io from \"../../..\";\nimport { GameStatus, TIMEOUT } from \"../../../../../werewolf-"
},
{
"path": "werewolf-backend/src/handlers/http/gameActHandlers/validateIdentity.ts",
"chars": 2052,
"preview": "import { GameStatus, StatusWithAction } from \"../../../../../werewolf-frontend/shared/GameDefs\";\nimport { Player } from "
},
{
"path": "werewolf-backend/src/handlers/http/gameBegin.ts",
"chars": 2553,
"preview": "import { Middleware } from \"koa\";\n\nimport io from \"../../\";\nimport { IDHeaderName, RoomNumberHeaderName } from \"../../.."
},
{
"path": "werewolf-backend/src/handlers/http/gameGetHint/getWolfs.ts",
"chars": 1154,
"preview": "import { Middleware } from \"koa\";\n\nimport {\n IDHeaderName, RoomNumberHeaderName\n} from \"../../../../../werewolf-front"
},
{
"path": "werewolf-backend/src/handlers/http/gameGetHint/index.ts",
"chars": 453,
"preview": "import * as Router from \"koa-router\";\n\nimport { getWolfs } from \"./getWolfs\";\nimport { witchGetDie } from \"./witchGetDie"
},
{
"path": "werewolf-backend/src/handlers/http/gameGetHint/witchGetDie.ts",
"chars": 1251,
"preview": "import { Middleware } from \"koa\";\n\nimport {\n IDHeaderName, RoomNumberHeaderName\n} from \"../../../../../werewolf-front"
},
{
"path": "werewolf-backend/src/handlers/http/gameGetHint/wolfKill.ts",
"chars": 1341,
"preview": "import { Middleware } from \"koa\";\n\nimport {\n IDHeaderName, RoomNumberHeaderName\n} from \"../../../../../werewolf-front"
},
{
"path": "werewolf-backend/src/handlers/http/gameStatus.ts",
"chars": 1254,
"preview": "import { Middleware } from \"koa\";\n\nimport { IDHeaderName, RoomNumberHeaderName } from \"../../../../werewolf-frontend/sha"
},
{
"path": "werewolf-backend/src/handlers/http/roomCreate.ts",
"chars": 828,
"preview": "import { Middleware } from \"koa\";\n\nimport {\n CreateRoomRequest, CreateRoomResponse\n} from \"../../../../werewolf-front"
},
{
"path": "werewolf-backend/src/handlers/http/roomInit.ts",
"chars": 744,
"preview": "import { Middleware } from \"koa\";\n\nimport { RoomNumberHeaderName } from \"../../../../werewolf-frontend/shared/constants\""
},
{
"path": "werewolf-backend/src/handlers/http/roomJoin.ts",
"chars": 1211,
"preview": "import { Middleware } from \"koa\";\n\nimport { GameStatus, TIMEOUT } from \"../../../../werewolf-frontend/shared/GameDefs\";\n"
},
{
"path": "werewolf-backend/src/index.ts",
"chars": 1028,
"preview": "import { createServer } from \"http\";\nimport * as Koa from \"koa\";\nimport * as KoaBody from \"koa-body\";\nimport * as logger"
},
{
"path": "werewolf-backend/src/middleware/auth.ts",
"chars": 518,
"preview": "import { Middleware } from \"koa\";\n\nimport { IDHeaderName, RoomNumberHeaderName } from \"../../../werewolf-frontend/shared"
},
{
"path": "werewolf-backend/src/middleware/handleError.ts",
"chars": 807,
"preview": "import { Middleware } from \"koa\";\n\nconst useHandleError = function (): Middleware {\n return async (ctx, next) => {\n "
},
{
"path": "werewolf-backend/src/models/PlayerModel.ts",
"chars": 1292,
"preview": "import { Character } from \"../../../werewolf-frontend/shared/GameDefs\";\nimport {\n CharacterStatus, day, ID, index, Pl"
},
{
"path": "werewolf-backend/src/models/RoomModel.ts",
"chars": 4063,
"preview": "import {\n Character,\n GameStatus,\n} from \"../../../werewolf-frontend/shared/GameDefs\";\nimport {\n day,\n ID,\n index,\n"
},
{
"path": "werewolf-backend/src/routes/gameRoutes.ts",
"chars": 715,
"preview": "import * as Router from \"koa-router\";\n\nimport gameAct from \"../handlers/http/gameAct\";\nimport gameBegin from \"../handler"
},
{
"path": "werewolf-backend/src/routes/index.ts",
"chars": 436,
"preview": "import * as Router from \"koa-router\";\n\nimport UseAuth from \"../middleware/auth\";\nimport { test } from \"../t\";\nimport gam"
},
{
"path": "werewolf-backend/src/routes/roomRoutes.ts",
"chars": 409,
"preview": "import * as Router from \"koa-router\";\n\nimport roomCreate from \"../handlers/http/roomCreate\";\nimport roomJoin from \"../ha"
},
{
"path": "werewolf-backend/src/utils/checkGameOver.ts",
"chars": 1448,
"preview": "import io from \"../\";\nimport { Events } from \"../../../werewolf-frontend/shared/WSEvents\";\nimport { GameEndMsg } from \"."
},
{
"path": "werewolf-backend/src/utils/getVoteResult.ts",
"chars": 1560,
"preview": "import { index } from \"../../../werewolf-frontend/shared/ModelDefs\";\n\n/**\n * @param votes 投票结果的数组, 每一项是某个玩家投票的玩家编号\n * @r"
},
{
"path": "werewolf-backend/src/utils/renderHintNPlayers.ts",
"chars": 1164,
"preview": "import { index } from \"../../../werewolf-frontend/shared/ModelDefs\";\n\n/**\n * 将提示信息和一串玩家渲染成好看的 html\n * @param hint\n * @pa"
},
{
"path": "werewolf-backend/src/ws/index.ts",
"chars": 384,
"preview": "import { Server } from \"socket.io\";\n\nimport { Events } from \"../../../werewolf-frontend/shared/WSEvents\";\n\nexport functi"
},
{
"path": "werewolf-backend/tsconfig.json",
"chars": 370,
"preview": "{\n \"compilerOptions\": {\n \"module\": \"commonjs\",\n \"declaration\": false,\n \"removeComments\": true,\n "
},
{
"path": "werewolf-frontend/.eslintrc.js",
"chars": 740,
"preview": "module.exports = {\n \"env\": {\n \"browser\": true,\n \"es2021\": true\n },\n \"extends\": [\n \"eslint:"
},
{
"path": "werewolf-frontend/.gitignore",
"chars": 59,
"preview": "node_modules\n.DS_Store\ndist\ndist-ssr\n*.local\nt.*\n\nnohup.out"
},
{
"path": "werewolf-frontend/.vscode/settings.json",
"chars": 903,
"preview": "{\n \"workbench.colorCustomizations\": {\n \"activityBar.activeBackground\": \"#4c73fe\",\n \"activityBar.activeBorder\": \"#"
},
{
"path": "werewolf-frontend/README.md",
"chars": 36,
"preview": "\n# 狼人杀前端代码\n\n使用 vue3 + ts + vite 编写, "
},
{
"path": "werewolf-frontend/index.html",
"chars": 399,
"preview": "<!DOCTYPE html>\n<html lang=\"zh-CN\">\n <head>\n <meta charset=\"UTF-8\" />\n <link rel=\"icon\" href=\"/favicon.ico\" />\n "
},
{
"path": "werewolf-frontend/package.json",
"chars": 763,
"preview": "{\n \"name\": \"werewolf-frontend\",\n \"version\": \"0.0.0\",\n \"scripts\": {\n \"dev\": \"vite\",\n \"build\": \"vite build\"\n },\n"
},
{
"path": "werewolf-frontend/shared/.vscode/WS Msg Model.code-snippets",
"chars": 266,
"preview": "{\n\t\"web socket message define\": {\n\t\t\"scope\": \"typescript\",\n\t\t\"prefix\": \"ws#\",\n\t\t\"body\": [\n\t\t\t\"import { } from \\\"../Model"
},
{
"path": "werewolf-frontend/shared/.vscode/http Msg Model.code-snippets",
"chars": 519,
"preview": "{\n\t\"http message define\": {\n\t\t\"scope\": \"typescript\",\n\t\t\"prefix\": \"http#\",\n\t\t\"body\": [\n\t\t\t\"import { ID, index } from \\\".."
},
{
"path": "werewolf-frontend/shared/GameDefs.ts",
"chars": 2818,
"preview": "import { RoomDef } from \"./ModelDefs\";\n\nexport type SetableCharacters =\n | \"HUNTER\"\n | \"WITCH\"\n | \"SEER\"\n | \"GUARD\"\n"
},
{
"path": "werewolf-frontend/shared/ModelDefs.ts",
"chars": 2296,
"preview": "import { Character, GameStatus, Potion } from \"./GameDefs\";\n\nexport type ID = string; // 玩家 id\nexport type index = numbe"
},
{
"path": "werewolf-frontend/shared/WSEvents.ts",
"chars": 345,
"preview": "export enum Events {\n /** 房间相关 */\n ROOM_EXILE = \"ROOM_EXILE\", // 踢出房间\n ROOM_JOIN = \"ROOM_JOIN\", // 有人加入房间\n GAME_BEGI"
},
{
"path": "werewolf-frontend/shared/WSMsg/ChangeStatus.ts",
"chars": 197,
"preview": "import { GameStatus } from \"../GameDefs\";\nimport { day } from \"../ModelDefs\";\n\nexport interface ChangeStatusMsg {\n setD"
},
{
"path": "werewolf-frontend/shared/WSMsg/GameEnd.ts",
"chars": 193,
"preview": "import { Character } from \"../GameDefs\";\nimport {} from \"../ModelDefs\";\n\n/**\n * Server to Client\n */\nexport interface Ga"
},
{
"path": "werewolf-frontend/shared/WSMsg/RoomExile.ts",
"chars": 123,
"preview": "import { PublicPlayerDef } from \"../ModelDefs\";\n\n/**\n * Server to Client\n */\nexport type RoomExileMsg = PublicPlayerDef["
},
{
"path": "werewolf-frontend/shared/WSMsg/RoomJoin.ts",
"chars": 94,
"preview": "import { PublicPlayerDef } from \"../ModelDefs\";\n\nexport type RoomJoinMsg = PublicPlayerDef[];\n"
},
{
"path": "werewolf-frontend/shared/WSMsg/ShowMsg.ts",
"chars": 96,
"preview": "export interface ShowMsg {\n innerHTML: string; // 展示在弹窗中的信息\n showTime?: number; // 展示的时间(s)\n}\n"
},
{
"path": "werewolf-frontend/shared/constants.ts",
"chars": 428,
"preview": "export const CLIENT_BASE_URL =\n // \"http://localhost:3000\";\n \"http://werewolf.xiong35.cn\";\nexport const SERVER_DOMAIN "
},
{
"path": "werewolf-frontend/shared/httpMsg/CharacterAct.ts",
"chars": 158,
"preview": "import { index } from \"../ModelDefs\";\n\nexport default interface CharacterAct {\n /**\n * 执行操作的目标玩家编号\\\n * 若为 女巫, 则正编号代"
},
{
"path": "werewolf-frontend/shared/httpMsg/CreateRoomMsg.ts",
"chars": 313,
"preview": "import { Character } from \"../GameDefs\";\nimport { ID, index } from \"../ModelDefs\";\nimport { HttpRes } from \"./_httpResTe"
},
{
"path": "werewolf-frontend/shared/httpMsg/GameStatusMsg.ts",
"chars": 395,
"preview": "import { Vote } from \"../../src/utils/votes\";\nimport { Character, GameStatus } from \"../GameDefs\";\nimport { day, GameEve"
},
{
"path": "werewolf-frontend/shared/httpMsg/InitRoomMsg.ts",
"chars": 291,
"preview": "import { Character } from \"../GameDefs\";\nimport { PublicPlayerDef } from \"../ModelDefs\";\nimport { HttpRes } from \"./_htt"
},
{
"path": "werewolf-frontend/shared/httpMsg/JoinRoomMsg.ts",
"chars": 375,
"preview": "import { Character } from \"../GameDefs\";\nimport { ID, index } from \"../ModelDefs\";\nimport { HttpRes } from \"./_httpResTe"
},
{
"path": "werewolf-frontend/shared/httpMsg/SeerCheckMsg.ts",
"chars": 239,
"preview": "import { index } from \"../ModelDefs\";\nimport { HttpRes } from \"./_httpResTemplate\";\nimport CharacterAct from \"./Characte"
},
{
"path": "werewolf-frontend/shared/httpMsg/_httpResTemplate.ts",
"chars": 81,
"preview": "export interface HttpRes<T = {}> {\n status: number;\n msg: string;\n data: T;\n}\n"
},
{
"path": "werewolf-frontend/src/App.vue",
"chars": 800,
"preview": "<template>\n <div class=\"wrapper\" :class=\"{ dark: theme === DARK }\">\n <div class=\"main\">\n <router-view></router-"
},
{
"path": "werewolf-frontend/src/components/Avatar.vue",
"chars": 1653,
"preview": "<template>\n <div class=\"avatar\">\n <img\n class=\"icon\"\n :src=\"`/assets/${character.toLowerCase()}${theme}.sv"
},
{
"path": "werewolf-frontend/src/components/Btn.vue",
"chars": 887,
"preview": "<template>\n <div\n class=\"btn\"\n :class=\"{ disabled }\"\n @click=\"(e) => (disabled ? null : onClick(e))\"\n >\n <"
},
{
"path": "werewolf-frontend/src/components/Dialog.vue",
"chars": 1673,
"preview": "<template>\n <UseMenu\n v-show=\"dialogTimeLeft > 0\"\n :onCancel=\"() => (dialogTimeLeft = 0)\"\n >\n <div class=\"dia"
},
{
"path": "werewolf-frontend/src/components/PlayActions/ActionBtn.vue",
"chars": 896,
"preview": "<template>\n <Btn\n :disabled=\"disabled\"\n :onClick=\"\n () => {\n onClick ? onClick() : void 0;\n di"
},
{
"path": "werewolf-frontend/src/components/PlayActions/commonAction.ts",
"chars": 318,
"preview": "import { isActing, noTarget, target } from \"../../reactivity/playAction\";\nimport { showActions } from \"../../reactivity/"
},
{
"path": "werewolf-frontend/src/components/PlayActions/index.vue",
"chars": 926,
"preview": "<script lang=\"ts\">\n import { defineComponent, h, withDirectives, vShow } from \"vue\";\n\n import { showActions } from \".."
},
{
"path": "werewolf-frontend/src/components/PlayActions/renderActionList.ts",
"chars": 3073,
"preview": "import {\n ComponentOptions,\n ComputedRef,\n h,\n vShow,\n withDirectives,\n} from \"vue\";\n\nimport { Character, GameStatu"
},
{
"path": "werewolf-frontend/src/components/PlayBottomActions.vue",
"chars": 1682,
"preview": "<template>\n <div class=\"play-bottom-actions\" v-show=\"isActing\">\n <img\n @click=\"isActing = false\"\n :src=\"`/"
},
{
"path": "werewolf-frontend/src/components/PlayCharacter.vue",
"chars": 1339,
"preview": "<template>\n <UseMenu v-show=\"showCharacter\" :onCancel=\"() => (showCharacter = false)\">\n <Avatar :character=\"characte"
},
{
"path": "werewolf-frontend/src/components/PlayEventList.vue",
"chars": 874,
"preview": "<template>\n <div v-if=\"events !== undefined\" class=\"event-day\">\n Day {{ day }}\n </div>\n <Tile\n v-for=\"item in e"
},
{
"path": "werewolf-frontend/src/components/PlayEventTile.vue",
"chars": 1865,
"preview": "<template>\n <div class=\"play-event-tile\" :class=\"'.level' + level\">\n <div class=\"left-info\">\n <Avatar :characte"
},
{
"path": "werewolf-frontend/src/components/PlayEvents.vue",
"chars": 1450,
"preview": "<template>\n <UseMenu\n class=\"game-event\"\n v-show=\"showEvents\"\n :onCancel=\"() => (showEvents = false)\"\n >\n "
},
{
"path": "werewolf-frontend/src/components/PlayMemo.vue",
"chars": 904,
"preview": "<template>\n <UseMenu v-show=\"showMemo\" :onCancel=\"() => (showMemo = false)\">\n <span class=\"title\">备忘录</span>\n <te"
},
{
"path": "werewolf-frontend/src/components/RoomCharacterTile.vue",
"chars": 2099,
"preview": "<template>\n <div class=\"room-character-tile\">\n <character-avatar :character=\"character\"></character-avatar>\n <div"
},
{
"path": "werewolf-frontend/src/components/RoomPlayerList.vue",
"chars": 3758,
"preview": "<template>\n <div class=\"room-player-list\">\n <div\n v-for=\"item in playerList\"\n :key=\"item.index\"\n clas"
},
{
"path": "werewolf-frontend/src/components/UseBorder.vue",
"chars": 1073,
"preview": "<template>\n <span class=\"use-border\">\n <slot></slot>\n <div class=\"use-border-mask\"></div>\n </span>\n</template>\n\n"
},
{
"path": "werewolf-frontend/src/components/UseMenu.vue",
"chars": 1250,
"preview": "<template>\n <div class=\"use-menu\">\n <UseBorder>\n <slot></slot>\n <img\n @click=\"onCancel\"\n cla"
},
{
"path": "werewolf-frontend/src/http/_request.ts",
"chars": 1372,
"preview": "import axios, { AxiosRequestConfig } from \"axios\";\n\nimport { IDHeaderName, RoomNumberHeaderName, SERVER_BASE_URL } from "
},
{
"path": "werewolf-frontend/src/http/action.ts",
"chars": 437,
"preview": "import { HttpRes } from \"../../shared/httpMsg/_httpResTemplate\";\nimport CharacterAct from \"../../shared/httpMsg/Characte"
},
{
"path": "werewolf-frontend/src/http/gameGetHint.ts",
"chars": 961,
"preview": "import { showDialog } from \"../reactivity/dialog\";\nimport request from \"./_request\";\n\n/**\n * 获得狼人杀人结果并显示弹窗\n * @returns 是"
},
{
"path": "werewolf-frontend/src/http/gameStatus.ts",
"chars": 421,
"preview": "import { GameStatusRequest, GameStatusResponse } from \"../../shared/httpMsg/GameStatusMsg\";\nimport request from \"./_requ"
},
{
"path": "werewolf-frontend/src/http/room.ts",
"chars": 1170,
"preview": "import { CreateRoomRequest, CreateRoomResponse } from \"../../shared/httpMsg/CreateRoomMsg\";\nimport { InitRoomRequest, In"
},
{
"path": "werewolf-frontend/src/index.css",
"chars": 708,
"preview": ".wrapper.dark {\n --bg: #111;\n --on-bg: #f8f8f8;\n --secondary: #222;\n}\n\n.wrapper {\n --bg: #f8f8f8;\n --on-bg: #111;\n "
},
{
"path": "werewolf-frontend/src/main.js",
"chars": 215,
"preview": "import { createApp } from \"vue\";\n// import App from \"./t.vue\";\nimport App from \"./App.vue\";\nimport router from \"./router"
},
{
"path": "werewolf-frontend/src/normalize.css",
"chars": 6142,
"preview": "/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */\n\n/* Document\n ==========================="
},
{
"path": "werewolf-frontend/src/pages/CreateRoom.vue",
"chars": 2456,
"preview": "<template>\n <div class=\"createroom\">\n <span class=\"title\">角色设置</span>\n <!-- TODO 警长, 屠边屠城 -->\n <div class=\"til"
},
{
"path": "werewolf-frontend/src/pages/Home.vue",
"chars": 1215,
"preview": "<template>\n <div class=\"main-page\">\n <img\n :src=\"`/assets/werewolf${theme}.svg`\"\n alt=\"logo\"\n class=\""
},
{
"path": "werewolf-frontend/src/pages/JoinRoom.vue",
"chars": 2376,
"preview": "<template>\n <div class=\"joinroom\">\n <div class=\"title\">加入房间</div>\n <div class=\"number\">\n <span class=\"hint\">"
},
{
"path": "werewolf-frontend/src/pages/Play.vue",
"chars": 4578,
"preview": "<template>\n <div class=\"play\">\n <PlayerList :playerList=\"players\"></PlayerList>\n\n <div class=\"date\">\n Day {{"
},
{
"path": "werewolf-frontend/src/pages/Review.vue",
"chars": 2091,
"preview": "<template>\n <div class=\"review\">\n <div class=\"title\">游戏记录</div>\n <div class=\"events\" v-if=\"records.length\">\n "
},
{
"path": "werewolf-frontend/src/pages/ReviewDetail.vue",
"chars": 2442,
"preview": "<template>\n <div class=\"review-detail\">\n <h2 class=\"title\">\n 对局记录\n </h2>\n <div class=\"review\" v-if=\"recor"
},
{
"path": "werewolf-frontend/src/pages/WaitRoom.vue",
"chars": 2744,
"preview": "<template>\n <div class=\"waitroom\">\n <RoomPlayerList :playerList=\"playerList\"></RoomPlayerList>\n <div class=\"room-"
},
{
"path": "werewolf-frontend/src/reactivity/computeGameEvents.ts",
"chars": 5852,
"preview": "import { computed } from \"vue\";\n\nimport {\n CharacterEvent, GameEvent, GuardStatus, HunterStatus, PlayerDef, SeerStatu"
},
{
"path": "werewolf-frontend/src/reactivity/createRoom.ts",
"chars": 2185,
"preview": "import sha256 from \"sha256\";\nimport { reactive, ref } from \"vue\";\n\nimport { SetableCharacters } from \"../../shared/GameD"
},
{
"path": "werewolf-frontend/src/reactivity/dialog.ts",
"chars": 539,
"preview": "import { computed, ref } from \"vue\";\n\nexport const dialogTimeLeft = ref(0);\nexport const toShowContents = ref<\n { conte"
},
{
"path": "werewolf-frontend/src/reactivity/game.ts",
"chars": 1252,
"preview": "import { computed, ref, Ref, watchEffect } from \"vue\";\n\nimport { Character, GameStatus, TIMEOUT } from \"../../shared/Gam"
},
{
"path": "werewolf-frontend/src/reactivity/joinRoom.ts",
"chars": 1304,
"preview": "import sha256 from \"sha256\";\nimport { ref } from \"vue\";\n\nimport { joinRoom } from \"../http/room\";\nimport router from \".."
},
{
"path": "werewolf-frontend/src/reactivity/playAction.ts",
"chars": 1097,
"preview": "import { ref } from \"vue\";\n\nimport { GameStatus, Potion } from \"../../shared/GameDefs\";\nimport { index } from \"../../sha"
},
{
"path": "werewolf-frontend/src/reactivity/playPage.ts",
"chars": 342,
"preview": "import { ref, watch } from \"vue\";\n\nexport const showMemo = ref(false);\nexport const memoContent = ref(\"\");\nwatch(memoCon"
},
{
"path": "werewolf-frontend/src/reactivity/record.ts",
"chars": 2461,
"preview": "/* 将游戏记录存在 localStorage 的相关操作 */\n\nimport { onMounted, ref, Ref } from \"vue\";\n\nimport { Character } from \"../../shared/Ga"
},
{
"path": "werewolf-frontend/src/reactivity/theme.ts",
"chars": 196,
"preview": "import { computed } from \"vue\";\nimport { date } from \"./game\";\n\nexport const DARK = \"-dark\";\nexport const LIGHT = \"\";\n\ne"
},
{
"path": "werewolf-frontend/src/router.ts",
"chars": 1305,
"preview": "import { createRouter, createWebHistory } from \"vue-router\";\n\nimport CreateRoom from \"./pages/CreateRoom.vue\";\nimport Ho"
},
{
"path": "werewolf-frontend/src/socket/changeStatus.ts",
"chars": 1039,
"preview": "import { Character, GameStatus, TIMEOUT } from \"../../shared/GameDefs\";\nimport { ChangeStatusMsg } from \"../../shared/WS"
},
{
"path": "werewolf-frontend/src/socket/gameBegin.ts",
"chars": 153,
"preview": "import { gameBegin as begin } from \"../reactivity/joinRoom\";\n\nexport default function gameBegin() {\n // console.log(\"#w"
},
{
"path": "werewolf-frontend/src/socket/gameEnd.ts",
"chars": 1030,
"preview": "import { PlayerDef } from \"../../shared/ModelDefs\";\nimport { GameEndMsg } from \"../../shared/WSMsg/GameEnd\";\nimport { gr"
},
{
"path": "werewolf-frontend/src/socket/index.ts",
"chars": 978,
"preview": "import io from \"socket.io-client\";\n\n// const io = require(\"socket.io-client\");\nimport { SERVER_DOMAIN, WS_PATH } from \"."
},
{
"path": "werewolf-frontend/src/socket/roomJoin.ts",
"chars": 224,
"preview": "import { RoomJoinMsg } from \"../../shared/WSMsg/RoomJoin\";\n\nimport { players } from \"../reactivity/game\";\n\nexport defaul"
},
{
"path": "werewolf-frontend/src/socket/showWSMsg.ts",
"chars": 201,
"preview": "import { ShowMsg } from \"../../shared/WSMsg/ShowMsg\";\nimport { showDialog } from \"../reactivity/dialog\";\n\nexport default"
},
{
"path": "werewolf-frontend/src/utils/setObj.ts",
"chars": 517,
"preview": "/* \nbefore:\n\neditingItem._id = item._id;\neditingItem.name = item.name;\neditingItem.price = item.price;\neditingItem.summa"
},
{
"path": "werewolf-frontend/src/utils/token.ts",
"chars": 848,
"preview": "import { TokenDef } from \"../../shared/ModelDefs\";\n\nconst KEY = \"_werewolf_token_\";\n\nexport function setToken(ID: string"
},
{
"path": "werewolf-frontend/src/utils/votes.ts",
"chars": 628,
"preview": "import { index } from \"../../shared/ModelDefs\";\n\n/**\n * 返回票型, key 投票的*目标*, value 为投给这个玩家的人\n * 选择弃票的玩家的*目标*为 0\n * @param "
},
{
"path": "werewolf-frontend/src/werewolf.d.ts",
"chars": 148,
"preview": "declare module \"*.vue\" {\n import { ComponentOptions } from \"vue\";\n const componentOptions: ComponentOptions;\n export "
},
{
"path": "werewolf-frontend/tsconfig.json",
"chars": 263,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"esnext\",\n \"module\": \"esnext\",\n // 这样就可以对 `this` 上的数据属性进行更严格的"
},
{
"path": "werewolf-frontend/vite.config.js",
"chars": 118,
"preview": "/**\n * @type {import('vite').UserConfig}\n */\nconst config = {\n // base: \"/werewolf/game\",\n};\n\nexport default config;\n"
}
]
About this extraction
This page contains the full source code of the xiong35/werewolf GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 134 files (169.3 KB), approximately 52.8k tokens, and a symbol index with 172 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.