drgame-cc 1.0.16 → 1.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build-templates/web-mobile/bg.png +0 -0
- package/build-templates/web-mobile/index.html +3 -3
- package/build-templates/web-mobile/jz-jdt-3.png +0 -0
- package/build-templates/web-mobile/jz-jdt-4.png +0 -0
- package/build-templates/web-mobile/logo.png +0 -0
- package/build-templates/web-mobile/style-mobile.css +85 -73
- package/drscript/PlatformAdapter.ts +11 -0
- package/drscript/focus/FocusManager.ts +8 -8
- package/drscript/focus/FocusVisualLayer.ts +40 -40
- package/drscript/input/KeyboardMove.ts +55 -0
- package/drscript/virtualgamepad/VirtualGamepadClient.ts +214 -0
- package/drscript/virtualgamepad/VirtualGamepadClient.ts.meta +10 -0
- package/drscript/virtualgamepad/VirtualGamepadConfig.ts +70 -0
- package/drscript/virtualgamepad/VirtualGamepadConfig.ts.meta +10 -0
- package/drscript/virtualgamepad/VirtualGamepadManager.ts +809 -0
- package/drscript/virtualgamepad/VirtualGamepadManager.ts.meta +10 -0
- package/drscript/virtualgamepad/VirtualGamepadView.ts +346 -0
- package/drscript/virtualgamepad/VirtualGamepadView.ts.meta +10 -0
- package/package.json +4 -4
- package/scripts/sync-back.js +85 -0
- package/build-templates/web-mobile/bg1.jpg +0 -0
- package/build-templates/web-mobile/splash_en - /345/211/257/346/234/254.png +0 -0
- package/drscript/README - /345/211/257/346/234/254.md" +0 -814
|
Binary file
|
|
@@ -45,15 +45,15 @@
|
|
|
45
45
|
<body>
|
|
46
46
|
<canvas id="GameCanvas" oncontextmenu="event.preventDefault()" tabindex="-1" style="outline: none;"></canvas>
|
|
47
47
|
<div id="splash">
|
|
48
|
-
|
|
48
|
+
<img id="logo" src="logo.png" alt="Logo">
|
|
49
49
|
<div id="splash-info">首次加载时间较长,请耐心等待</div>
|
|
50
50
|
<div id="progress-status">正在启动引擎...0%</div>
|
|
51
51
|
<div class="progress-bar">
|
|
52
52
|
<span style="width: 0%"></span>
|
|
53
53
|
</div>
|
|
54
54
|
|
|
55
|
-
<div id="icp">备案号:豫ICP备18029698号-5A</div>
|
|
56
|
-
<img id="age-rating" src="16+.png" alt="16+">
|
|
55
|
+
<!-- <div id="icp">备案号:豫ICP备18029698号-5A</div> -->
|
|
56
|
+
<!-- <img id="age-rating" src="16+.png" alt="16+"> -->
|
|
57
57
|
</div>
|
|
58
58
|
<script src="src/settings.js" charset="utf-8"></script>
|
|
59
59
|
<script src="vconsole.min.js"></script>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -2,7 +2,9 @@ html {
|
|
|
2
2
|
-ms-touch-action: none;
|
|
3
3
|
}
|
|
4
4
|
|
|
5
|
-
body,
|
|
5
|
+
body,
|
|
6
|
+
canvas,
|
|
7
|
+
div {
|
|
6
8
|
display: block;
|
|
7
9
|
outline: none;
|
|
8
10
|
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
|
@@ -18,9 +20,10 @@ body, canvas, div {
|
|
|
18
20
|
/* Remove spin of input type number */
|
|
19
21
|
input::-webkit-outer-spin-button,
|
|
20
22
|
input::-webkit-inner-spin-button {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
/* display: none; <- Crashes Chrome on hover */
|
|
24
|
+
-webkit-appearance: none;
|
|
25
|
+
margin: 0;
|
|
26
|
+
/* <-- Apparently some margin are still there even though it's hidden */
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
body {
|
|
@@ -42,7 +45,7 @@ body {
|
|
|
42
45
|
|
|
43
46
|
display: flex;
|
|
44
47
|
flex-direction: column;
|
|
45
|
-
|
|
48
|
+
|
|
46
49
|
/* fix bug: https://github.com/cocos-creator/2d-tasks/issues/791 */
|
|
47
50
|
/* overflow cannot be applied in Cocos2dGameContainer,
|
|
48
51
|
otherwise child elements will be hidden when Cocos2dGameContainer rotated 90 deg */
|
|
@@ -65,11 +68,13 @@ canvas {
|
|
|
65
68
|
background-color: rgba(0, 0, 0, 0);
|
|
66
69
|
}
|
|
67
70
|
|
|
68
|
-
a:link,
|
|
71
|
+
a:link,
|
|
72
|
+
a:visited {
|
|
69
73
|
color: #666;
|
|
70
74
|
}
|
|
71
75
|
|
|
72
|
-
a:active,
|
|
76
|
+
a:active,
|
|
77
|
+
a:hover {
|
|
73
78
|
color: #666;
|
|
74
79
|
}
|
|
75
80
|
|
|
@@ -87,101 +92,108 @@ p.footer {
|
|
|
87
92
|
left: 0;
|
|
88
93
|
width: 100%;
|
|
89
94
|
height: 100%;
|
|
90
|
-
background: #171717 url(./
|
|
95
|
+
background: #171717 url(./bg.png) no-repeat center center;
|
|
91
96
|
/* 固定 splash.png 大小为 300x300 并保持居中 */
|
|
92
97
|
background-size: cover;
|
|
93
|
-
|
|
98
|
+
|
|
94
99
|
}
|
|
95
100
|
|
|
96
101
|
.progress-bar {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
102
|
+
position: absolute;
|
|
103
|
+
left: 50%;
|
|
104
|
+
width: 40%;
|
|
105
|
+
height: 12px;
|
|
106
|
+
bottom: 13%;
|
|
107
|
+
transform: translateX(-50%);
|
|
108
|
+
border-radius: 3px;
|
|
109
|
+
/*margin: 0 -175px;
|
|
105
110
|
padding: 5px;
|
|
106
111
|
box-shadow: 0 1px 5px rgba(0, 0, 0, .6) inset, 0 1px 0 rgba(255, 255, .2);*/
|
|
107
|
-
|
|
108
|
-
|
|
112
|
+
background: url("jz-jdt-4.png") no-repeat center center;
|
|
113
|
+
background-size: 100% 100%;
|
|
109
114
|
}
|
|
110
115
|
|
|
111
116
|
.progress-bar span {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
117
|
+
display: block;
|
|
118
|
+
height: 100%;
|
|
119
|
+
width: 0%;
|
|
120
|
+
border-radius: 3px;
|
|
121
|
+
/*box-shadow: 0 1px 0 rgba(255, 255, 255, .5) inset;*/
|
|
122
|
+
transition: width .4s ease-in-out;
|
|
123
|
+
background: url("jz-jdt-3.png") no-repeat center center;
|
|
124
|
+
background-size: 100% 100%;
|
|
120
125
|
}
|
|
121
126
|
|
|
122
127
|
#splash-info {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
128
|
+
position: absolute;
|
|
129
|
+
left: 50%;
|
|
130
|
+
bottom: 16%;
|
|
131
|
+
transform: translateX(-50%);
|
|
132
|
+
color: #fdecc8;
|
|
133
|
+
font-size: clamp(12px, 1.5vw, 20px);
|
|
134
|
+
z-index: 1000;
|
|
135
|
+
|
|
131
136
|
}
|
|
132
137
|
|
|
133
138
|
#logo {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
139
|
+
position: absolute;
|
|
140
|
+
left: 50%;
|
|
141
|
+
top: 40%;
|
|
142
|
+
transform: translate(-50%, -50%);
|
|
143
|
+
z-index: 1000;
|
|
144
|
+
max-width: 46vw;
|
|
145
|
+
max-height: 24vh;
|
|
146
|
+
width: auto;
|
|
147
|
+
height: auto;
|
|
142
148
|
}
|
|
143
149
|
|
|
144
150
|
#progress-status {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
151
|
+
position: absolute;
|
|
152
|
+
left: 50%;
|
|
153
|
+
bottom: 9%;
|
|
154
|
+
transform: translateX(-50%);
|
|
155
|
+
color: #fdecc8;
|
|
156
|
+
font-size: clamp(12px, 1.5vw, 20px);
|
|
157
|
+
z-index: 1000;
|
|
158
|
+
|
|
153
159
|
}
|
|
154
160
|
|
|
155
161
|
|
|
156
162
|
.stripes span {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
+
background-size: 30px 30px;
|
|
164
|
+
background-image: linear-gradient(135deg, rgba(255, 255, 255, .15) 25%, transparent 25%,
|
|
165
|
+
transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%,
|
|
166
|
+
transparent 75%, transparent);
|
|
167
|
+
|
|
168
|
+
animation: animate-stripes 1s linear infinite;
|
|
163
169
|
}
|
|
164
170
|
|
|
165
171
|
@keyframes animate-stripes {
|
|
166
|
-
|
|
172
|
+
0% {
|
|
173
|
+
background-position: 0 0;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
100% {
|
|
177
|
+
background-position: 60px 0;
|
|
178
|
+
}
|
|
167
179
|
}
|
|
168
180
|
|
|
169
181
|
#icp {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
182
|
+
position: absolute;
|
|
183
|
+
bottom: 2%;
|
|
184
|
+
left: 50%;
|
|
185
|
+
transform: translateX(-50%);
|
|
186
|
+
color: #ef6e28;
|
|
187
|
+
z-index: 1000;
|
|
188
|
+
font-size: clamp(10px, 1.5vw, 18px);
|
|
189
|
+
|
|
178
190
|
}
|
|
179
191
|
|
|
180
192
|
#age-rating {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
193
|
+
position: absolute;
|
|
194
|
+
top: 50px;
|
|
195
|
+
left: 50px;
|
|
196
|
+
z-index: 1000;
|
|
197
|
+
height: clamp(32px, 4vw, 56px);
|
|
198
|
+
width: auto;
|
|
199
|
+
}
|
|
@@ -153,6 +153,7 @@ export class PlatformAdapter {
|
|
|
153
153
|
|
|
154
154
|
/** 刷新界面按钮事件, 用于界面按钮变化(列表,下拉)后,需要刷新按钮事件 */
|
|
155
155
|
public static refreshWindow(window: cc.Node) {
|
|
156
|
+
cc.Node.EventType.TOUCH_START
|
|
156
157
|
TvUi.focus.registerButtons(window);
|
|
157
158
|
}
|
|
158
159
|
|
|
@@ -162,4 +163,14 @@ export class PlatformAdapter {
|
|
|
162
163
|
exitWin.parent = cc.director.getScene();
|
|
163
164
|
exitWin.setPosition(cc.v2(cc.winSize.width / 2, cc.winSize.height / 2));
|
|
164
165
|
}
|
|
166
|
+
|
|
167
|
+
/** 设置启动页进度条 */
|
|
168
|
+
public static setProgress(progress: number, tip: string) {
|
|
169
|
+
if (window["setProgress"]) window["setProgress"](progress, tip);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** 关闭启动页 */
|
|
173
|
+
public static closeLoading() {
|
|
174
|
+
if (window["closeLoading"]) window["closeLoading"]();
|
|
175
|
+
}
|
|
165
176
|
}
|
|
@@ -450,16 +450,16 @@ export default class FocusManager {
|
|
|
450
450
|
|
|
451
451
|
private static invokeButton(button: cc.Button) {
|
|
452
452
|
if (!button || !button.node || !button.node.activeInHierarchy || !button.interactable) return;
|
|
453
|
+
var event = new cc.Event.EventCustom(cc.Node.EventType.TOUCH_END, true);
|
|
454
|
+
event.target = button.node;
|
|
453
455
|
if (button.clickEvents && button.clickEvents.length > 0) {
|
|
454
|
-
cc.Component.EventHandler.emitEvents(button.clickEvents,
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
button.
|
|
458
|
-
|
|
459
|
-
// 模拟一个touch end事件
|
|
460
|
-
var event = new cc.Event.EventCustom(cc.Node.EventType.TOUCH_END, true);
|
|
461
|
-
button.node.emit(cc.Node.EventType.TOUCH_END, event);
|
|
456
|
+
cc.Component.EventHandler.emitEvents(button.clickEvents, event);
|
|
457
|
+
}
|
|
458
|
+
if (button instanceof cc.Toggle) {
|
|
459
|
+
button.isChecked = !button.isChecked;
|
|
460
|
+
cc.Component.EventHandler.emitEvents(button.checkEvents, event);
|
|
462
461
|
}
|
|
462
|
+
button.node.emit(cc.Node.EventType.TOUCH_END, event);
|
|
463
463
|
}
|
|
464
464
|
|
|
465
465
|
private static emitFocusChanged(item: FocusRuntimeItem | null, previous?: FocusRuntimeItem | null) {
|
|
@@ -167,59 +167,59 @@ export default class FocusVisualLayer extends cc.Component {
|
|
|
167
167
|
this.graphics.stroke();
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
private positionIcon(item: FocusRuntimeItem, iconConfig: TvUiIconItem) {
|
|
171
|
-
if (!item || !item.node || !item.node.isValid) return;
|
|
172
|
-
let focusConfig = TvUiConfigManager.getConfig().focus || {};
|
|
173
|
-
|
|
174
|
-
let node = item.node;
|
|
175
|
-
let worldPos = node.parent.convertToWorldSpaceAR(node.position);
|
|
176
|
-
let local = this.node.convertToNodeSpaceAR(worldPos);
|
|
177
|
-
|
|
178
|
-
let contentSize = node.getContentSize();
|
|
179
|
-
let scaleX = node.scaleX;
|
|
180
|
-
let scaleY = node.scaleY;
|
|
181
|
-
let width = contentSize.width * scaleX;
|
|
182
|
-
let height = contentSize.height * scaleY;
|
|
183
|
-
|
|
184
|
-
let iconWidth = iconConfig.width || 60;
|
|
185
|
-
let iconHeight = iconConfig.height || 60;
|
|
186
|
-
let offsetX = iconConfig.offsetX;
|
|
187
|
-
let offsetY = iconConfig.offsetY;
|
|
188
|
-
if (offsetX === undefined) offsetX = focusConfig.iconOffsetX || 0;
|
|
189
|
-
if (offsetY === undefined) offsetY = focusConfig.iconOffsetY || 0;
|
|
190
|
-
if (!this.iconNode) return;
|
|
191
|
-
|
|
192
|
-
let x = local.x + width / 2 + offsetX;
|
|
193
|
-
let y = local.y + height / 2 + offsetY;
|
|
194
|
-
if ((focusConfig.iconPlacement || "bottomRight") == "bottomRight") {
|
|
195
|
-
y = local.y - height / 2 - offsetY;
|
|
196
|
-
}
|
|
197
|
-
this.iconNode.setContentSize(iconWidth, iconHeight);
|
|
198
|
-
this.iconNode.setPosition(x, y);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
170
|
// private positionIcon(item: FocusRuntimeItem, iconConfig: TvUiIconItem) {
|
|
202
171
|
// if (!item || !item.node || !item.node.isValid) return;
|
|
203
172
|
// let focusConfig = TvUiConfigManager.getConfig().focus || {};
|
|
204
|
-
|
|
205
|
-
// let
|
|
206
|
-
// let
|
|
207
|
-
// let
|
|
208
|
-
|
|
173
|
+
|
|
174
|
+
// let node = item.node;
|
|
175
|
+
// let worldPos = node.parent.convertToWorldSpaceAR(node.position);
|
|
176
|
+
// let local = this.node.convertToNodeSpaceAR(worldPos);
|
|
177
|
+
|
|
178
|
+
// let contentSize = node.getContentSize();
|
|
179
|
+
// let scaleX = node.scaleX;
|
|
180
|
+
// let scaleY = node.scaleY;
|
|
181
|
+
// let width = contentSize.width * scaleX;
|
|
182
|
+
// let height = contentSize.height * scaleY;
|
|
183
|
+
|
|
184
|
+
// let iconWidth = iconConfig.width || 60;
|
|
185
|
+
// let iconHeight = iconConfig.height || 60;
|
|
209
186
|
// let offsetX = iconConfig.offsetX;
|
|
210
187
|
// let offsetY = iconConfig.offsetY;
|
|
211
188
|
// if (offsetX === undefined) offsetX = focusConfig.iconOffsetX || 0;
|
|
212
189
|
// if (offsetY === undefined) offsetY = focusConfig.iconOffsetY || 0;
|
|
213
190
|
// if (!this.iconNode) return;
|
|
214
|
-
|
|
215
|
-
// let
|
|
191
|
+
|
|
192
|
+
// let x = local.x + width / 2 + offsetX;
|
|
193
|
+
// let y = local.y + height / 2 + offsetY;
|
|
216
194
|
// if ((focusConfig.iconPlacement || "bottomRight") == "bottomRight") {
|
|
217
|
-
// y = local.y -
|
|
195
|
+
// y = local.y - height / 2 - offsetY;
|
|
218
196
|
// }
|
|
219
|
-
// this.iconNode.setContentSize(
|
|
197
|
+
// this.iconNode.setContentSize(iconWidth, iconHeight);
|
|
220
198
|
// this.iconNode.setPosition(x, y);
|
|
221
199
|
// }
|
|
222
200
|
|
|
201
|
+
private positionIcon(item: FocusRuntimeItem, iconConfig: TvUiIconItem) {
|
|
202
|
+
if (!item || !item.node || !item.node.isValid) return;
|
|
203
|
+
let focusConfig = TvUiConfigManager.getConfig().focus || {};
|
|
204
|
+
let box = item.node.getBoundingBoxToWorld();
|
|
205
|
+
let center = cc.v2(box.x + box.width / 2, box.y + box.height / 2);
|
|
206
|
+
let local = this.node.convertToNodeSpaceAR(center);
|
|
207
|
+
let width = iconConfig.width || 60;
|
|
208
|
+
let height = iconConfig.height || 60;
|
|
209
|
+
let offsetX = iconConfig.offsetX;
|
|
210
|
+
let offsetY = iconConfig.offsetY;
|
|
211
|
+
if (offsetX === undefined) offsetX = focusConfig.iconOffsetX || 0;
|
|
212
|
+
if (offsetY === undefined) offsetY = focusConfig.iconOffsetY || 0;
|
|
213
|
+
if (!this.iconNode) return;
|
|
214
|
+
let x = local.x + box.width / 2 + offsetX;
|
|
215
|
+
let y = local.y + box.height / 2 + offsetY;
|
|
216
|
+
if ((focusConfig.iconPlacement || "bottomRight") == "bottomRight") {
|
|
217
|
+
y = local.y - box.height / 2 - offsetY;
|
|
218
|
+
}
|
|
219
|
+
this.iconNode.setContentSize(width, height);
|
|
220
|
+
this.iconNode.setPosition(x, y);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
223
|
private resizeToParent() {
|
|
224
224
|
let parent = this.node.parent;
|
|
225
225
|
let width = parent && parent.width ? parent.width : (cc.winSize && cc.winSize.width) || 1920;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import TvUi from "../core/TvUi";
|
|
2
|
+
import { TvRemoteKey } from "./KeyCodeAdapter";
|
|
3
|
+
|
|
4
|
+
const { ccclass, property } = cc._decorator;
|
|
5
|
+
|
|
6
|
+
@ccclass
|
|
7
|
+
export default class KeyboardMove extends cc.Component {
|
|
8
|
+
/**
|
|
9
|
+
* 按键移动回调
|
|
10
|
+
*/
|
|
11
|
+
public callback: (dir: cc.Vec2) => void;
|
|
12
|
+
private _keys: Set<TvRemoteKey> = new Set();
|
|
13
|
+
private _dir: cc.Vec2 = cc.v2(0, 0);
|
|
14
|
+
|
|
15
|
+
onLoad(): void {
|
|
16
|
+
cc.systemEvent.on(cc.SystemEvent.EventType.KEY_DOWN, this.onKeyDown, this);
|
|
17
|
+
cc.systemEvent.on(cc.SystemEvent.EventType.KEY_UP, this.onKeyUp, this);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
onDestroy(): void {
|
|
21
|
+
cc.systemEvent.off(cc.SystemEvent.EventType.KEY_DOWN, this.onKeyDown, this);
|
|
22
|
+
cc.systemEvent.off(cc.SystemEvent.EventType.KEY_UP, this.onKeyUp, this);
|
|
23
|
+
this._keys.clear();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private onKeyDown(event: cc.Event.EventKeyboard): void {
|
|
27
|
+
let key = TvUi.input.toRemoteKey(event.keyCode);
|
|
28
|
+
this._keys.add(key);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private onKeyUp(event: cc.Event.EventKeyboard): void {
|
|
32
|
+
let key = TvUi.input.toRemoteKey(event.keyCode);
|
|
33
|
+
this._keys.delete(key);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
update(dt: number): void {
|
|
37
|
+
if (this._keys.size === 0)
|
|
38
|
+
return;
|
|
39
|
+
|
|
40
|
+
const dir = this._dir;
|
|
41
|
+
dir.x = 0;
|
|
42
|
+
dir.y = 0;
|
|
43
|
+
|
|
44
|
+
if (this._keys.has(TvRemoteKey.Left)) dir.x -= 1;
|
|
45
|
+
if (this._keys.has(TvRemoteKey.Right)) dir.x += 1;
|
|
46
|
+
if (this._keys.has(TvRemoteKey.Down)) dir.y -= 1;
|
|
47
|
+
if (this._keys.has(TvRemoteKey.Up)) dir.y += 1;
|
|
48
|
+
|
|
49
|
+
if (dir.x !== 0 || dir.y !== 0) {
|
|
50
|
+
dir.normalizeSelf();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (this.callback) this.callback(dir);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
export interface VirtualGamepadClientOptions {
|
|
2
|
+
wsUrl: string;
|
|
3
|
+
roomId: string;
|
|
4
|
+
deviceId: string;
|
|
5
|
+
game: string;
|
|
6
|
+
userId: string;
|
|
7
|
+
userType: "xiaomi" | "guest";
|
|
8
|
+
guestId?: string;
|
|
9
|
+
token?: string;
|
|
10
|
+
maxMembers: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type VirtualGamepadConnectionState = "idle" | "connecting" | "open" | "joined" | "closed" | "error";
|
|
14
|
+
|
|
15
|
+
export default class VirtualGamepadClient {
|
|
16
|
+
private socket: WebSocket | null = null;
|
|
17
|
+
private options: VirtualGamepadClientOptions | null = null;
|
|
18
|
+
private manualClose: boolean = false;
|
|
19
|
+
private reconnectCount: number = 0;
|
|
20
|
+
private heartbeatTimer: any = -1;
|
|
21
|
+
private reconnectTimer: any = -1;
|
|
22
|
+
private readonly maxReconnectCount: number = 3;
|
|
23
|
+
private readonly heartbeatMs: number = 10000;
|
|
24
|
+
|
|
25
|
+
public onStateChange: ((state: VirtualGamepadConnectionState, reason?: string) => void) | null = null;
|
|
26
|
+
public onMessage: ((message: any) => void) | null = null;
|
|
27
|
+
|
|
28
|
+
public connect(options: VirtualGamepadClientOptions) {
|
|
29
|
+
this.options = options;
|
|
30
|
+
this.manualClose = false;
|
|
31
|
+
this.clearReconnect();
|
|
32
|
+
this.openSocket();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public close() {
|
|
36
|
+
this.manualClose = true;
|
|
37
|
+
this.clearHeartbeat();
|
|
38
|
+
this.clearReconnect();
|
|
39
|
+
if (this.socket) {
|
|
40
|
+
this.socket.onopen = null;
|
|
41
|
+
this.socket.onmessage = null;
|
|
42
|
+
this.socket.onclose = null;
|
|
43
|
+
this.socket.onerror = null;
|
|
44
|
+
try {
|
|
45
|
+
this.socket.close();
|
|
46
|
+
} catch (e) {
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
this.socket = null;
|
|
50
|
+
this.emitState("closed");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public send(message: any): boolean {
|
|
54
|
+
if (!this.socket || this.socket.readyState != WebSocket.OPEN) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
this.socket.send(JSON.stringify(message));
|
|
59
|
+
return true;
|
|
60
|
+
} catch (e) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private openSocket() {
|
|
66
|
+
if (!this.options || !this.options.wsUrl) return;
|
|
67
|
+
this.clearHeartbeat();
|
|
68
|
+
this.emitState("connecting");
|
|
69
|
+
let options = this.options;
|
|
70
|
+
let query: { [key: string]: string } = {
|
|
71
|
+
roomId: options.roomId,
|
|
72
|
+
deviceId: options.deviceId,
|
|
73
|
+
role: "game",
|
|
74
|
+
userId: options.userId,
|
|
75
|
+
userType: options.userType,
|
|
76
|
+
maxMembers: String(options.maxMembers)
|
|
77
|
+
};
|
|
78
|
+
if (options.guestId) query.guestId = options.guestId;
|
|
79
|
+
if (options.token) query.token = options.token;
|
|
80
|
+
let url = this.appendParams(options.wsUrl, query);
|
|
81
|
+
try {
|
|
82
|
+
this.socket = new WebSocket(url);
|
|
83
|
+
this.socket.onopen = this.handleOpen.bind(this);
|
|
84
|
+
this.socket.onmessage = this.handleMessage.bind(this);
|
|
85
|
+
this.socket.onclose = this.handleClose.bind(this);
|
|
86
|
+
this.socket.onerror = this.handleError.bind(this);
|
|
87
|
+
} catch (e) {
|
|
88
|
+
this.handleError(e);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private handleOpen() {
|
|
93
|
+
if (!this.options) return;
|
|
94
|
+
this.reconnectCount = 0;
|
|
95
|
+
this.emitState("open");
|
|
96
|
+
let joinMessage: any = {
|
|
97
|
+
type: "join",
|
|
98
|
+
roomId: this.options.roomId,
|
|
99
|
+
role: "game",
|
|
100
|
+
deviceId: this.options.deviceId,
|
|
101
|
+
userId: this.options.userId,
|
|
102
|
+
userType: this.options.userType,
|
|
103
|
+
maxMembers: this.options.maxMembers,
|
|
104
|
+
game: this.options.game
|
|
105
|
+
};
|
|
106
|
+
if (this.options.guestId) joinMessage.guestId = this.options.guestId;
|
|
107
|
+
if (this.options.token) joinMessage.token = this.options.token;
|
|
108
|
+
console.log("[VirtualGamepad][WSS][Send] join", {
|
|
109
|
+
type: joinMessage.type,
|
|
110
|
+
roomId: joinMessage.roomId,
|
|
111
|
+
role: joinMessage.role,
|
|
112
|
+
deviceId: joinMessage.deviceId,
|
|
113
|
+
userId: joinMessage.userId,
|
|
114
|
+
userType: joinMessage.userType,
|
|
115
|
+
guestId: joinMessage.guestId || "",
|
|
116
|
+
maxMembers: joinMessage.maxMembers,
|
|
117
|
+
game: joinMessage.game,
|
|
118
|
+
hasToken: !!joinMessage.token
|
|
119
|
+
});
|
|
120
|
+
this.send(joinMessage);
|
|
121
|
+
this.startHeartbeat();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private handleMessage(event: MessageEvent) {
|
|
125
|
+
let data = event && event.data;
|
|
126
|
+
let message: any = null;
|
|
127
|
+
try {
|
|
128
|
+
message = typeof data == "string" ? JSON.parse(data) : data;
|
|
129
|
+
} catch (e) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (!message) return;
|
|
133
|
+
if (message.type != "pong") {
|
|
134
|
+
console.log("[VirtualGamepad][WSS][Receive]", message);
|
|
135
|
+
}
|
|
136
|
+
if (message.type == "joined" || message.type == "welcome") {
|
|
137
|
+
this.emitState("joined");
|
|
138
|
+
}
|
|
139
|
+
if (this.onMessage) {
|
|
140
|
+
this.onMessage(message);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private handleClose(event?: any) {
|
|
145
|
+
this.clearHeartbeat();
|
|
146
|
+
this.socket = null;
|
|
147
|
+
this.emitState("closed", event && event.reason);
|
|
148
|
+
if (!this.manualClose) {
|
|
149
|
+
this.scheduleReconnect();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private handleError(event?: any) {
|
|
154
|
+
this.emitState("error", event && event.message);
|
|
155
|
+
if (!this.manualClose && (!this.socket || this.socket.readyState == WebSocket.CLOSED)) {
|
|
156
|
+
this.scheduleReconnect();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private startHeartbeat() {
|
|
161
|
+
this.clearHeartbeat();
|
|
162
|
+
this.heartbeatTimer = setInterval(() => {
|
|
163
|
+
if (!this.options) return;
|
|
164
|
+
this.send({
|
|
165
|
+
type: "ping",
|
|
166
|
+
roomId: this.options.roomId,
|
|
167
|
+
deviceId: this.options.deviceId,
|
|
168
|
+
role: "game",
|
|
169
|
+
timestamp: Date.now(),
|
|
170
|
+
ts: Date.now()
|
|
171
|
+
});
|
|
172
|
+
}, this.heartbeatMs);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private scheduleReconnect() {
|
|
176
|
+
if (this.reconnectCount >= this.maxReconnectCount) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
this.clearReconnect();
|
|
180
|
+
this.reconnectCount++;
|
|
181
|
+
this.reconnectTimer = setTimeout(() => {
|
|
182
|
+
this.openSocket();
|
|
183
|
+
}, 1200 * this.reconnectCount);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private appendParams(url: string, params: { [key: string]: string }): string {
|
|
187
|
+
let query = [];
|
|
188
|
+
for (let key in params) {
|
|
189
|
+
query.push(encodeURIComponent(key) + "=" + encodeURIComponent(params[key] || ""));
|
|
190
|
+
}
|
|
191
|
+
let separator = url.indexOf("?") >= 0 ? "&" : "?";
|
|
192
|
+
return url + separator + query.join("&");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private emitState(state: VirtualGamepadConnectionState, reason?: string) {
|
|
196
|
+
if (this.onStateChange) {
|
|
197
|
+
this.onStateChange(state, reason);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private clearHeartbeat() {
|
|
202
|
+
if (this.heartbeatTimer != -1) {
|
|
203
|
+
clearInterval(this.heartbeatTimer);
|
|
204
|
+
this.heartbeatTimer = -1;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private clearReconnect() {
|
|
209
|
+
if (this.reconnectTimer != -1) {
|
|
210
|
+
clearTimeout(this.reconnectTimer);
|
|
211
|
+
this.reconnectTimer = -1;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|