[add] first templates
This commit is contained in:
commit
d797a2a41a
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
dist
|
||||
.DS_STORE
|
||||
*.meta
|
11
.mocharc.js
Normal file
11
.mocharc.js
Normal file
@ -0,0 +1,11 @@
|
||||
module.exports = {
|
||||
require: [
|
||||
'ts-node/register',
|
||||
],
|
||||
timeout: 999999,
|
||||
exit: true,
|
||||
spec: [
|
||||
'./test/**/*.test.ts'
|
||||
],
|
||||
'preserve-symlinks': true
|
||||
}
|
30
.vscode/launch.json
vendored
Normal file
30
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "mocha current file",
|
||||
"program": "${workspaceFolder}/node_modules/mocha/bin/_mocha",
|
||||
"args": [
|
||||
"${file}"
|
||||
],
|
||||
"internalConsoleOptions": "openOnSessionStart",
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "ts-node current file",
|
||||
"protocol": "inspector",
|
||||
"args": [
|
||||
"${relativeFile}"
|
||||
],
|
||||
"cwd": "${workspaceRoot}",
|
||||
"runtimeArgs": [
|
||||
"-r",
|
||||
"ts-node/register"
|
||||
],
|
||||
"internalConsoleOptions": "openOnSessionStart"
|
||||
}
|
||||
]
|
||||
}
|
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"typescript.tsdk": "node_modules\\typescript\\lib"
|
||||
}
|
30
Dockerfile
Normal file
30
Dockerfile
Normal file
@ -0,0 +1,30 @@
|
||||
FROM node
|
||||
|
||||
# 使用淘宝 NPM 镜像(国内机器构建推荐启用)
|
||||
# RUN npm config set registry https://registry.npm.taobao.org/
|
||||
|
||||
# npm install
|
||||
ADD package*.json /src/
|
||||
WORKDIR /src
|
||||
RUN npm i
|
||||
|
||||
# build
|
||||
ADD . /src
|
||||
RUN npm run build
|
||||
|
||||
# clean
|
||||
RUN npm prune --production
|
||||
|
||||
# move
|
||||
RUN rm -rf /app \
|
||||
&& mv dist /app \
|
||||
&& mv node_modules /app/ \
|
||||
&& rm -rf /src
|
||||
|
||||
# ENV
|
||||
ENV NODE_ENV production
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
WORKDIR /app
|
||||
CMD node index.js
|
49
README.md
Normal file
49
README.md
Normal file
@ -0,0 +1,49 @@
|
||||
# TSRPC Server
|
||||
|
||||
## Usage
|
||||
### Local dev server
|
||||
|
||||
Dev server would restart automatically when code changed.
|
||||
|
||||
```
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Run unit Test
|
||||
Execute `npm run dev` first, then execute:
|
||||
```
|
||||
npm run test
|
||||
```
|
||||
|
||||
### Build
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Additional Scripts
|
||||
|
||||
### Generate API document
|
||||
|
||||
Generate API document in swagger/openapi and markdown format.
|
||||
|
||||
```shell
|
||||
npm run doc
|
||||
```
|
||||
|
||||
### Generate ServiceProto
|
||||
```
|
||||
npm run proto
|
||||
```
|
||||
|
||||
### Generate API templates
|
||||
```
|
||||
npm run api
|
||||
```
|
||||
|
||||
### Manually sync shared code
|
||||
|
||||
```
|
||||
npm run sync
|
||||
```
|
3672
package-lock.json
generated
Normal file
3672
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
package.json
Normal file
27
package.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "backend-.",
|
||||
"version": "0.1.0",
|
||||
"main": "index.js",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"proto": "tsrpc proto",
|
||||
"sync": "tsrpc link",
|
||||
"api": "tsrpc api",
|
||||
"doc": "tsrpc doc",
|
||||
"dev": "tsrpc dev",
|
||||
"test": "mocha test/**/*.test.ts",
|
||||
"build": "tsrpc build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mocha": "^8.2.3",
|
||||
"@types/node": "^15.14.9",
|
||||
"mocha": "^9.1.3",
|
||||
"onchange": "^7.1.0",
|
||||
"ts-node": "^10.4.0",
|
||||
"tsrpc-cli": "^2.2.2",
|
||||
"typescript": "^4.5.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"tsrpc": "^3.1.3"
|
||||
}
|
||||
}
|
12
src/api/ApiJoin.ts
Normal file
12
src/api/ApiJoin.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { ApiCallWs } from "tsrpc";
|
||||
import { roomInstance } from "..";
|
||||
import { ReqJoin, ResJoin } from "../shared/protocols/PtlJoin";
|
||||
|
||||
export async function ApiJoin(call: ApiCallWs<ReqJoin, ResJoin>) {
|
||||
let playerId = roomInstance.join(call.req, call.conn);
|
||||
|
||||
call.succ({
|
||||
playerId: playerId,
|
||||
gameState: roomInstance.gameSystem.state
|
||||
})
|
||||
}
|
39
src/index.ts
Normal file
39
src/index.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import 'k8w-extend-native';
|
||||
import * as path from "path";
|
||||
import { WsConnection, WsServer } from "tsrpc";
|
||||
import { Room } from './models/Room';
|
||||
import { serviceProto, ServiceType } from './shared/protocols/serviceProto';
|
||||
|
||||
// 创建 TSRPC WebSocket Server
|
||||
export const server = new WsServer(serviceProto, {
|
||||
port: 3000,
|
||||
json: true
|
||||
});
|
||||
|
||||
// 断开连接后退出房间
|
||||
server.flows.postDisconnectFlow.push(v => {
|
||||
let conn = v.conn as WsConnection<ServiceType>;
|
||||
if (conn.playerId) {
|
||||
roomInstance.leave(conn.playerId, conn);
|
||||
}
|
||||
|
||||
return v;
|
||||
});
|
||||
|
||||
export const roomInstance = new Room(server);
|
||||
|
||||
// 初始化
|
||||
async function init() {
|
||||
// 挂载 API 接口
|
||||
await server.autoImplementApi(path.resolve(__dirname, 'api'));
|
||||
|
||||
// TODO
|
||||
// Prepare something... (e.g. connect the db)
|
||||
};
|
||||
|
||||
// 启动入口点
|
||||
async function main() {
|
||||
await init();
|
||||
await server.start();
|
||||
}
|
||||
main();
|
101
src/models/Room.ts
Normal file
101
src/models/Room.ts
Normal file
@ -0,0 +1,101 @@
|
||||
import { WsConnection, WsServer } from "tsrpc";
|
||||
import { gameConfig } from "../shared/game/gameConfig";
|
||||
import { GameSystem, GameSystemInput, PlayerJoin } from "../shared/game/GameSystem";
|
||||
import { ReqJoin } from "../shared/protocols/PtlJoin";
|
||||
import { ServiceType } from "../shared/protocols/serviceProto";
|
||||
|
||||
/**
|
||||
* 服务端 - 房间 - 逻辑系统
|
||||
*/
|
||||
export class Room {
|
||||
|
||||
// 帧同步频率,次数/秒
|
||||
syncRate = gameConfig.syncRate;
|
||||
nextPlayerId = 1;
|
||||
|
||||
gameSystem = new GameSystem();
|
||||
|
||||
server: WsServer<ServiceType>;
|
||||
conns: WsConnection<ServiceType>[] = [];
|
||||
pendingInputs: GameSystemInput[] = [];
|
||||
playerLastSn: { [playerId: number]: number | undefined } = {};
|
||||
lastSyncTime?: number;
|
||||
|
||||
constructor(server: WsServer<ServiceType>) {
|
||||
this.server = server;
|
||||
setInterval(() => { this.sync() }, 1000 / this.syncRate);
|
||||
}
|
||||
|
||||
/** 加入房间 */
|
||||
join(req: ReqJoin, conn: WsConnection<ServiceType>) {
|
||||
let input: PlayerJoin = {
|
||||
type: 'PlayerJoin',
|
||||
playerId: this.nextPlayerId++,
|
||||
// 初始位置随机
|
||||
pos: {
|
||||
x: Math.random() * 10 - 5,
|
||||
y: Math.random() * 10 - 5
|
||||
}
|
||||
}
|
||||
this.applyInput(input);
|
||||
|
||||
this.conns.push(conn);
|
||||
conn.playerId = input.playerId;
|
||||
conn.listenMsg('client/ClientInput', call => {
|
||||
this.playerLastSn[input.playerId] = call.msg.sn;
|
||||
call.msg.inputs.forEach(v => {
|
||||
this.applyInput({
|
||||
...v,
|
||||
playerId: input.playerId
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
return input.playerId;
|
||||
}
|
||||
|
||||
applyInput(input: GameSystemInput) {
|
||||
this.pendingInputs.push(input);
|
||||
}
|
||||
|
||||
sync() {
|
||||
let inputs = this.pendingInputs;
|
||||
this.pendingInputs = [];
|
||||
|
||||
// Apply inputs
|
||||
inputs.forEach(v => {
|
||||
this.gameSystem.applyInput(v)
|
||||
});
|
||||
|
||||
// Apply TimePast
|
||||
let now = process.uptime() * 1000;
|
||||
this.applyInput({
|
||||
type: 'TimePast',
|
||||
dt: now - (this.lastSyncTime ?? now)
|
||||
});
|
||||
this.lastSyncTime = now;
|
||||
|
||||
// 发送同步帧
|
||||
this.conns.forEach(v => {
|
||||
v.sendMsg('server/Frame', {
|
||||
inputs: inputs,
|
||||
lastSn: this.playerLastSn[v.playerId!]
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
/** 离开房间 */
|
||||
leave(playerId: number, conn: WsConnection<ServiceType>) {
|
||||
this.conns.removeOne(v => v.playerId === playerId);
|
||||
this.applyInput({
|
||||
type: 'PlayerLeave',
|
||||
playerId: playerId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'tsrpc' {
|
||||
export interface WsConnection {
|
||||
playerId?: number;
|
||||
}
|
||||
}
|
143
src/shared/game/GameSystem.ts
Normal file
143
src/shared/game/GameSystem.ts
Normal file
@ -0,0 +1,143 @@
|
||||
import { gameConfig } from "./gameConfig";
|
||||
import { ArrowState } from "./state/ArrowState";
|
||||
import { PlayerState } from "./state/PlayerState";
|
||||
|
||||
// 状态定义
|
||||
export interface GameSystemState {
|
||||
// 当前的时间(游戏时间)
|
||||
now: number,
|
||||
// 玩家
|
||||
players: PlayerState[],
|
||||
// 飞行中的箭矢
|
||||
arrows: ArrowState[],
|
||||
// 箭矢的 ID 生成
|
||||
nextArrowId: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 前后端复用的状态计算模块
|
||||
*/
|
||||
export class GameSystem {
|
||||
|
||||
// 当前状态
|
||||
private _state: GameSystemState = {
|
||||
now: 0,
|
||||
players: [],
|
||||
arrows: [],
|
||||
nextArrowId: 1
|
||||
}
|
||||
get state(): Readonly<GameSystemState> {
|
||||
return this._state
|
||||
}
|
||||
|
||||
// 重设状态
|
||||
reset(state: GameSystemState) {
|
||||
this._state = Object.merge({}, state);
|
||||
}
|
||||
|
||||
// 应用输入,计算状态变更
|
||||
applyInput(input: GameSystemInput) {
|
||||
if (input.type === 'PlayerMove') {
|
||||
let player = this._state.players.find(v => v.id === input.playerId);
|
||||
if (!player) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (player.dizzyEndTime && player.dizzyEndTime > this._state.now) {
|
||||
return;
|
||||
}
|
||||
player.pos.x += input.speed.x * input.dt;
|
||||
player.pos.y += input.speed.y * input.dt;
|
||||
}
|
||||
else if (input.type === 'PlayerAttack') {
|
||||
let player = this._state.players.find(v => v.id === input.playerId);
|
||||
if (player) {
|
||||
let newArrow: ArrowState = {
|
||||
id: this._state.nextArrowId++,
|
||||
fromPlayerId: input.playerId,
|
||||
targetPos: { ...input.targetPos },
|
||||
targetTime: input.targetTime
|
||||
};
|
||||
this._state.arrows.push(newArrow);
|
||||
this.onNewArrow.forEach(v => v(newArrow));
|
||||
}
|
||||
}
|
||||
else if (input.type === 'PlayerJoin') {
|
||||
this.state.players.push({
|
||||
id: input.playerId,
|
||||
pos: { ...input.pos }
|
||||
})
|
||||
}
|
||||
else if (input.type === 'PlayerLeave') {
|
||||
this.state.players.remove(v => v.id === input.playerId);
|
||||
}
|
||||
else if (input.type === 'TimePast') {
|
||||
this._state.now += input.dt;
|
||||
|
||||
// 落地的 Arrow
|
||||
for (let i = this._state.arrows.length - 1; i > -1; --i) {
|
||||
let arrow = this._state.arrows[i];
|
||||
if (arrow.targetTime <= this._state.now) {
|
||||
// 伤害判定
|
||||
let damagedPlayers = this._state.players.filter(v => {
|
||||
return (v.pos.x - arrow.targetPos.x) * (v.pos.x - arrow.targetPos.x) + (v.pos.y - arrow.targetPos.y) * (v.pos.y - arrow.targetPos.y) <= gameConfig.arrowAttackRadius * gameConfig.arrowAttackRadius
|
||||
});
|
||||
damagedPlayers.forEach(p => {
|
||||
// 设置击晕状态
|
||||
p.dizzyEndTime = this._state.now + gameConfig.arrowDizzyTime;
|
||||
|
||||
// Event
|
||||
})
|
||||
|
||||
this._state.arrows.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 事件
|
||||
* 某些转瞬即逝的事件,可能不会直观的体现在前后两帧状态的变化中,但表面层又需要知晓。
|
||||
* 例如一颗狙击枪的子弹,在少于一帧的时间内创建和销毁,前后两帧的状态中都不包含这颗子弹;但表现层却需要绘制出子弹的弹道。
|
||||
* 此时,可以通过事件的方式通知表现层。
|
||||
*/
|
||||
// 发射箭矢
|
||||
onNewArrow: ((arrow: ArrowState) => void)[] = [];
|
||||
|
||||
}
|
||||
|
||||
export interface PlayerMove {
|
||||
type: 'PlayerMove',
|
||||
playerId: number,
|
||||
speed: { x: number, y: number },
|
||||
// 移动的时间 (秒)
|
||||
dt: number,
|
||||
}
|
||||
export interface PlayerAttack {
|
||||
type: 'PlayerAttack',
|
||||
playerId: number,
|
||||
// 落点坐标
|
||||
targetPos: { x: number, y: number },
|
||||
// 落点时间(游戏时间)
|
||||
targetTime: number
|
||||
}
|
||||
export interface PlayerJoin {
|
||||
type: 'PlayerJoin',
|
||||
playerId: number,
|
||||
pos: { x: number, y: number }
|
||||
}
|
||||
export interface PlayerLeave {
|
||||
type: 'PlayerLeave',
|
||||
playerId: number
|
||||
}
|
||||
// 时间流逝
|
||||
export interface TimePast {
|
||||
type: 'TimePast',
|
||||
dt: number
|
||||
}
|
||||
// 输入定义
|
||||
export type GameSystemInput = PlayerMove
|
||||
| PlayerAttack
|
||||
| PlayerJoin
|
||||
| PlayerLeave
|
||||
| TimePast;
|
14
src/shared/game/gameConfig.ts
Normal file
14
src/shared/game/gameConfig.ts
Normal file
@ -0,0 +1,14 @@
|
||||
export const gameConfig = {
|
||||
syncRate: 10,
|
||||
|
||||
moveSpeed: 10,
|
||||
|
||||
// 箭矢飞行时间(毫秒)
|
||||
arrowFlyTime: 500,
|
||||
// 箭矢投掷距离
|
||||
arrowDistance: 8,
|
||||
// 箭矢落地命中判定半径
|
||||
arrowAttackRadius: 2,
|
||||
// 被箭矢几种后的晕眩时间(毫秒)
|
||||
arrowDizzyTime: 1000
|
||||
}
|
9
src/shared/game/state/ArrowState.ts
Normal file
9
src/shared/game/state/ArrowState.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export type ArrowState = {
|
||||
id: number,
|
||||
// 谁发出的箭
|
||||
fromPlayerId: number,
|
||||
// 落地时间(游戏时间)
|
||||
targetTime: number,
|
||||
// 落点位置(游戏位置)
|
||||
targetPos: { x: number, y: number }
|
||||
}
|
7
src/shared/game/state/PlayerState.ts
Normal file
7
src/shared/game/state/PlayerState.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export interface PlayerState {
|
||||
id: number,
|
||||
// 位置
|
||||
pos: { x: number, y: number },
|
||||
// 晕眩结束时间
|
||||
dizzyEndTime?: number,
|
||||
}
|
15
src/shared/protocols/PtlJoin.ts
Normal file
15
src/shared/protocols/PtlJoin.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { GameSystemState } from "../game/GameSystem";
|
||||
|
||||
/** 加入房间 */
|
||||
export interface ReqJoin {
|
||||
|
||||
}
|
||||
|
||||
export interface ResJoin {
|
||||
/** 加入房间后,自己的 ID */
|
||||
playerId: number,
|
||||
/** 状态同步:一次性同步当前状态 */
|
||||
gameState: GameSystemState
|
||||
}
|
||||
|
||||
// export const conf = {}
|
9
src/shared/protocols/client/MsgClientInput.ts
Normal file
9
src/shared/protocols/client/MsgClientInput.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { PlayerAttack, PlayerMove } from "../../game/GameSystem";
|
||||
|
||||
/** 发送自己的输入 */
|
||||
export interface MsgClientInput {
|
||||
sn: number,
|
||||
inputs: ClientInput[]
|
||||
};
|
||||
|
||||
export type ClientInput = Omit<PlayerMove, 'playerId'> | Omit<PlayerAttack, 'playerId'>;
|
11
src/shared/protocols/server/MsgFrame.ts
Normal file
11
src/shared/protocols/server/MsgFrame.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { GameSystemInput } from "../../game/GameSystem";
|
||||
|
||||
/**
|
||||
* 服务端定期广播的同步帧
|
||||
* 包含了这一段期间所有输入
|
||||
*/
|
||||
export interface MsgFrame {
|
||||
inputs: GameSystemInput[],
|
||||
/** 当前用户提交的,经服务端确认的最后一条输入的 SN */
|
||||
lastSn?: number
|
||||
}
|
496
src/shared/protocols/serviceProto.ts
Normal file
496
src/shared/protocols/serviceProto.ts
Normal file
@ -0,0 +1,496 @@
|
||||
import { ServiceProto } from 'tsrpc-proto';
|
||||
import { MsgClientInput } from './client/MsgClientInput';
|
||||
import { ReqJoin, ResJoin } from './PtlJoin';
|
||||
import { MsgFrame } from './server/MsgFrame';
|
||||
|
||||
export interface ServiceType {
|
||||
api: {
|
||||
"Join": {
|
||||
req: ReqJoin,
|
||||
res: ResJoin
|
||||
}
|
||||
},
|
||||
msg: {
|
||||
"client/ClientInput": MsgClientInput,
|
||||
"server/Frame": MsgFrame
|
||||
}
|
||||
}
|
||||
|
||||
export const serviceProto: ServiceProto<ServiceType> = {
|
||||
"services": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "client/ClientInput",
|
||||
"type": "msg"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Join",
|
||||
"type": "api"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "server/Frame",
|
||||
"type": "msg"
|
||||
}
|
||||
],
|
||||
"types": {
|
||||
"client/MsgClientInput/MsgClientInput": {
|
||||
"type": "Interface",
|
||||
"properties": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "sn",
|
||||
"type": {
|
||||
"type": "Number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "inputs",
|
||||
"type": {
|
||||
"type": "Array",
|
||||
"elementType": {
|
||||
"type": "Reference",
|
||||
"target": "client/MsgClientInput/ClientInput"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"client/MsgClientInput/ClientInput": {
|
||||
"type": "Union",
|
||||
"members": [
|
||||
{
|
||||
"id": 0,
|
||||
"type": {
|
||||
"target": {
|
||||
"type": "Reference",
|
||||
"target": "../game/GameSystem/PlayerMove"
|
||||
},
|
||||
"keys": [
|
||||
"playerId"
|
||||
],
|
||||
"type": "Omit"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"type": {
|
||||
"target": {
|
||||
"type": "Reference",
|
||||
"target": "../game/GameSystem/PlayerAttack"
|
||||
},
|
||||
"keys": [
|
||||
"playerId"
|
||||
],
|
||||
"type": "Omit"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"../game/GameSystem/PlayerMove": {
|
||||
"type": "Interface",
|
||||
"properties": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "type",
|
||||
"type": {
|
||||
"type": "Literal",
|
||||
"literal": "PlayerMove"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "playerId",
|
||||
"type": {
|
||||
"type": "Number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "speed",
|
||||
"type": {
|
||||
"type": "Interface",
|
||||
"properties": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "x",
|
||||
"type": {
|
||||
"type": "Number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "y",
|
||||
"type": {
|
||||
"type": "Number"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "dt",
|
||||
"type": {
|
||||
"type": "Number"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"../game/GameSystem/PlayerAttack": {
|
||||
"type": "Interface",
|
||||
"properties": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "type",
|
||||
"type": {
|
||||
"type": "Literal",
|
||||
"literal": "PlayerAttack"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "playerId",
|
||||
"type": {
|
||||
"type": "Number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "targetPos",
|
||||
"type": {
|
||||
"type": "Interface",
|
||||
"properties": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "x",
|
||||
"type": {
|
||||
"type": "Number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "y",
|
||||
"type": {
|
||||
"type": "Number"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "targetTime",
|
||||
"type": {
|
||||
"type": "Number"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"PtlJoin/ReqJoin": {
|
||||
"type": "Interface"
|
||||
},
|
||||
"PtlJoin/ResJoin": {
|
||||
"type": "Interface",
|
||||
"properties": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "playerId",
|
||||
"type": {
|
||||
"type": "Number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "gameState",
|
||||
"type": {
|
||||
"type": "Reference",
|
||||
"target": "../game/GameSystem/GameSystemState"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"../game/GameSystem/GameSystemState": {
|
||||
"type": "Interface",
|
||||
"properties": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "now",
|
||||
"type": {
|
||||
"type": "Number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "players",
|
||||
"type": {
|
||||
"type": "Array",
|
||||
"elementType": {
|
||||
"type": "Reference",
|
||||
"target": "../game/state/PlayerState/PlayerState"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "arrows",
|
||||
"type": {
|
||||
"type": "Array",
|
||||
"elementType": {
|
||||
"type": "Reference",
|
||||
"target": "../game/state/ArrowState/ArrowState"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "nextArrowId",
|
||||
"type": {
|
||||
"type": "Number"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"../game/state/PlayerState/PlayerState": {
|
||||
"type": "Interface",
|
||||
"properties": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "id",
|
||||
"type": {
|
||||
"type": "Number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "pos",
|
||||
"type": {
|
||||
"type": "Interface",
|
||||
"properties": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "x",
|
||||
"type": {
|
||||
"type": "Number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "y",
|
||||
"type": {
|
||||
"type": "Number"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "dizzyEndTime",
|
||||
"type": {
|
||||
"type": "Number"
|
||||
},
|
||||
"optional": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"../game/state/ArrowState/ArrowState": {
|
||||
"type": "Interface",
|
||||
"properties": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "id",
|
||||
"type": {
|
||||
"type": "Number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "fromPlayerId",
|
||||
"type": {
|
||||
"type": "Number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "targetTime",
|
||||
"type": {
|
||||
"type": "Number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "targetPos",
|
||||
"type": {
|
||||
"type": "Interface",
|
||||
"properties": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "x",
|
||||
"type": {
|
||||
"type": "Number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "y",
|
||||
"type": {
|
||||
"type": "Number"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"server/MsgFrame/MsgFrame": {
|
||||
"type": "Interface",
|
||||
"properties": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "inputs",
|
||||
"type": {
|
||||
"type": "Array",
|
||||
"elementType": {
|
||||
"type": "Reference",
|
||||
"target": "../game/GameSystem/GameSystemInput"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "lastSn",
|
||||
"type": {
|
||||
"type": "Number"
|
||||
},
|
||||
"optional": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"../game/GameSystem/GameSystemInput": {
|
||||
"type": "Union",
|
||||
"members": [
|
||||
{
|
||||
"id": 0,
|
||||
"type": {
|
||||
"type": "Reference",
|
||||
"target": "../game/GameSystem/PlayerMove"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"type": {
|
||||
"type": "Reference",
|
||||
"target": "../game/GameSystem/PlayerAttack"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": {
|
||||
"type": "Reference",
|
||||
"target": "../game/GameSystem/PlayerJoin"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": {
|
||||
"type": "Reference",
|
||||
"target": "../game/GameSystem/PlayerLeave"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": {
|
||||
"type": "Reference",
|
||||
"target": "../game/GameSystem/TimePast"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"../game/GameSystem/PlayerJoin": {
|
||||
"type": "Interface",
|
||||
"properties": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "type",
|
||||
"type": {
|
||||
"type": "Literal",
|
||||
"literal": "PlayerJoin"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "playerId",
|
||||
"type": {
|
||||
"type": "Number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "pos",
|
||||
"type": {
|
||||
"type": "Interface",
|
||||
"properties": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "x",
|
||||
"type": {
|
||||
"type": "Number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "y",
|
||||
"type": {
|
||||
"type": "Number"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"../game/GameSystem/PlayerLeave": {
|
||||
"type": "Interface",
|
||||
"properties": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "type",
|
||||
"type": {
|
||||
"type": "Literal",
|
||||
"literal": "PlayerLeave"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "playerId",
|
||||
"type": {
|
||||
"type": "Number"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"../game/GameSystem/TimePast": {
|
||||
"type": "Interface",
|
||||
"properties": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "type",
|
||||
"type": {
|
||||
"type": "Literal",
|
||||
"literal": "TimePast"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "dt",
|
||||
"type": {
|
||||
"type": "Number"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
39
test/api/ApiSend.test.ts
Normal file
39
test/api/ApiSend.test.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import assert from "assert";
|
||||
import { TsrpcError, WsClient } from "tsrpc";
|
||||
import { serviceProto, ServiceType } from "../../src/shared/protocols/serviceProto";
|
||||
|
||||
// 1. EXECUTE `npm run dev` TO START A LOCAL DEV SERVER
|
||||
// 2. EXECUTE `npm test` TO START UNIT TEST
|
||||
|
||||
describe("ApiSend", function (): void {
|
||||
let client: WsClient<ServiceType> = new WsClient(serviceProto, {
|
||||
server: "ws://127.0.0.1:3000",
|
||||
logger: console
|
||||
});
|
||||
|
||||
before(async function (): Promise<void> {
|
||||
let res: any = await client.connect();
|
||||
assert.strictEqual(res.isSucc, true, "Failed to connect to server, have you executed `npm run dev` already?");
|
||||
});
|
||||
|
||||
it("Success", async function (): Promise<void> {
|
||||
let ret: any = await client.callApi("Send", {
|
||||
content: "Test"
|
||||
});
|
||||
assert.ok(ret.isSucc)
|
||||
});
|
||||
|
||||
it("Check content is empty", async function (): Promise<void> {
|
||||
let ret: any = await client.callApi("Send", {
|
||||
content: ""
|
||||
});
|
||||
assert.deepStrictEqual(ret, {
|
||||
isSucc: false,
|
||||
err: new TsrpcError("Content is empty")
|
||||
});
|
||||
});
|
||||
|
||||
after(async function () {
|
||||
await client.disconnect();
|
||||
});
|
||||
});
|
15
test/tsconfig.json
Normal file
15
test/tsconfig.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
"es2018"
|
||||
],
|
||||
"module": "commonjs",
|
||||
"target": "es2018",
|
||||
"outDir": "dist",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"moduleResolution": "node"
|
||||
}
|
||||
}
|
18
tsconfig.json
Normal file
18
tsconfig.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
"es2018"
|
||||
],
|
||||
"module": "commonjs",
|
||||
"target": "es2018",
|
||||
"outDir": "dist",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
116
tslint.json
Normal file
116
tslint.json
Normal file
@ -0,0 +1,116 @@
|
||||
{
|
||||
"defaultSeverity": "warning",
|
||||
"rules": {
|
||||
"ban": [
|
||||
true,
|
||||
[
|
||||
"_",
|
||||
"extend"
|
||||
],
|
||||
[
|
||||
"_",
|
||||
"isNull"
|
||||
],
|
||||
[
|
||||
"_",
|
||||
"isDefined"
|
||||
]
|
||||
],
|
||||
"class-name": false,
|
||||
"comment-format": [
|
||||
true,
|
||||
"check-space"
|
||||
],
|
||||
"curly": true,
|
||||
"eofline": false,
|
||||
"forin": false,
|
||||
"indent": [
|
||||
true,
|
||||
4
|
||||
],
|
||||
"interface-name": [
|
||||
true,
|
||||
"never-prefix"
|
||||
],
|
||||
"jsdoc-format": true,
|
||||
"label-position": true,
|
||||
"label-undefined": true,
|
||||
"max-line-length": [
|
||||
false,
|
||||
140
|
||||
],
|
||||
"no-arg": true,
|
||||
"no-bitwise": false,
|
||||
"no-console": [
|
||||
true,
|
||||
"debug",
|
||||
"info",
|
||||
"time",
|
||||
"timeEnd",
|
||||
"trace"
|
||||
],
|
||||
"no-construct": true,
|
||||
"no-debugger": true,
|
||||
"no-duplicate-key": true,
|
||||
"no-duplicate-variable": true,
|
||||
"no-empty": true,
|
||||
// "no-eval": true,
|
||||
"no-string-literal": false,
|
||||
"no-trailing-comma": true,
|
||||
"no-trailing-whitespace": true,
|
||||
"no-unused-expression": false,
|
||||
"no-unused-variable": true,
|
||||
"no-unreachable": true,
|
||||
"no-use-before-declare": false,
|
||||
"one-line": [
|
||||
true,
|
||||
"check-open-brace",
|
||||
"check-catch",
|
||||
"check-else",
|
||||
"check-whitespace"
|
||||
],
|
||||
"quotemark": [
|
||||
true,
|
||||
"double"
|
||||
],
|
||||
"radix": true,
|
||||
"semicolon": [
|
||||
true,
|
||||
"always"
|
||||
],
|
||||
"triple-equals": [
|
||||
true,
|
||||
"allow-null-check"
|
||||
],
|
||||
"typedef": [
|
||||
true,
|
||||
"call-signature",
|
||||
"parameter",
|
||||
"property-declaration",
|
||||
"variable-declaration"
|
||||
],
|
||||
"typedef-whitespace": [
|
||||
true,
|
||||
{
|
||||
"call-signature": "nospace"
|
||||
},
|
||||
{
|
||||
"index-signature": "space"
|
||||
}
|
||||
],
|
||||
"use-strict": [
|
||||
true,
|
||||
"check-module",
|
||||
"check-function"
|
||||
],
|
||||
"variable-name": false,
|
||||
"whitespace": [
|
||||
false,
|
||||
"check-branch",
|
||||
"check-decl",
|
||||
"check-operator",
|
||||
"check-separator",
|
||||
"check-type"
|
||||
]
|
||||
}
|
||||
}
|
39
tsrpc.config.ts
Normal file
39
tsrpc.config.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { TsrpcConfig } from 'tsrpc-cli';
|
||||
|
||||
const tsrpcConf: TsrpcConfig = {
|
||||
// Generate ServiceProto
|
||||
proto: [
|
||||
{
|
||||
ptlDir: 'src/shared/protocols', // Protocol dir
|
||||
output: 'src/shared/protocols/serviceProto.ts', // Path for generated ServiceProto
|
||||
apiDir: 'src/api', // API dir
|
||||
docDir: 'docs', // API documents dir
|
||||
// ptlTemplate: CodeTemplate.getExtendedPtl(),
|
||||
// msgTemplate: CodeTemplate.getExtendedMsg(),
|
||||
}
|
||||
],
|
||||
// Sync shared code
|
||||
sync: [
|
||||
{
|
||||
from: 'src/shared',
|
||||
to: '../frontend/assets/scripts/shared',
|
||||
type: 'symlink' // Change this to 'copy' if your environment not support symlink
|
||||
}
|
||||
],
|
||||
// Dev server
|
||||
dev: {
|
||||
autoProto: true, // Auto regenerate proto
|
||||
autoSync: true, // Auto sync when file changed
|
||||
autoApi: true, // Auto create API when ServiceProto updated
|
||||
watch: 'src', // Restart dev server when these files changed
|
||||
entry: 'src/index.ts', // Dev server command: node -r ts-node/register {entry}
|
||||
},
|
||||
// Build config
|
||||
build: {
|
||||
autoProto: true, // Auto generate proto before build
|
||||
autoSync: true, // Auto sync before build
|
||||
autoApi: true, // Auto generate API before build
|
||||
outDir: 'dist', // Clean this dir before build
|
||||
}
|
||||
}
|
||||
export default tsrpcConf;
|
Loading…
Reference in New Issue
Block a user