vimp-engine 0.40.0 → 0.42.0
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/core/Cargo.toml +1 -1
- package/package.json +1 -1
- package/src/client/components/controller/RoomMenu.js +38 -10
- package/src/client/components/controller/Vote.js +26 -0
- package/src/client/components/model/Controls.js +9 -1
- package/src/client/components/model/RoomMenu.js +42 -57
- package/src/client/components/model/Vote.js +202 -0
- package/src/client/components/view/Panel.js +176 -6
- package/src/client/components/view/RoomMenu.js +78 -24
- package/src/client/components/view/Voice.js +6 -1
- package/src/client/components/view/Vote.js +71 -2
- package/src/client/lib/autoRejoin.js +161 -0
- package/src/client/lib/hostUnloadGuard.js +40 -15
- package/src/client/lib/roomLink.js +32 -0
- package/src/client/main.js +120 -36
- package/src/client/network/HostConnectionManager.js +27 -1
- package/src/client/network/HostController.js +1 -30
- package/src/client/network/HostHealthPolicy.js +14 -59
- package/src/client/network/PlannedHandoff.js +15 -100
- package/src/client/network/SessionSupervisor.js +58 -9
- package/src/client/network/SignalingClient.js +1 -1
- package/src/client/network/WebRtcManager.js +38 -3
- package/src/client/session/GuestSession.js +42 -7
- package/src/client/session/HandoffFlow.js +26 -51
- package/src/client/session/HostRole.js +8 -6
- package/src/client/session/PromotionFlow.js +28 -1
- package/src/client/session/RouteBoot.js +28 -9
- package/src/client/style.css +112 -20
- package/src/client/views/includes/roomMenu.pug +7 -8
- package/src/client/voice/MicCapture.js +3 -1
- package/src/config/abiOps.js +42 -0
- package/src/config/chatMessages.js +1 -1
- package/src/config/lobby.js +22 -23
- package/src/config/menuName.js +3 -0
- package/src/devtools/ScenarioRunner.js +1 -0
- package/src/devtools/invariants.js +24 -2
- package/src/devtools/report.js +2 -2
- package/src/host/GameCoreAdapter.js +83 -4
- package/src/host/HostGame.js +103 -39
- package/src/host/PortMachine.js +34 -17
- package/src/host/host.worker.js +1 -13
- package/src/host/meta/core/RoundManager.js +8 -22
- package/src/host/meta/core/VoteCoordinator.js +56 -1
- package/src/host/meta/modules/TimerManager.js +16 -2
- package/src/host/meta/modules/Vote.js +65 -0
- package/src/host/meta/modules/chat/systemMessages.js +1 -1
- package/src/host/meta/player/ParticipantManager.js +19 -3
- package/src/lib/capabilities.js +19 -4
- package/src/lib/gameConfigView.js +6 -5
package/core/Cargo.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "vimp-engine-core"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.24.0"
|
|
4
4
|
edition = "2024"
|
|
5
5
|
description = "VIMP engine simulation core: physics, map, snapshot framing, interpolation/prediction/raycast primitives, navigation utilities. No wasm-bindgen — the game crate builds the WASM ABI wrappers."
|
|
6
6
|
license = "MIT"
|
package/package.json
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
let roomMenuCtrl;
|
|
4
4
|
|
|
5
5
|
export default class RoomMenuCtrl {
|
|
6
|
-
// actions — {
|
|
6
|
+
// actions — { onExit, onConfirmOpen, onConfirmClose } (client/main.js):
|
|
7
|
+
// выход подтверждён; окно открылось; окно закрыто без выхода
|
|
7
8
|
constructor(model, view, actions = {}) {
|
|
8
9
|
if (roomMenuCtrl) {
|
|
9
10
|
return roomMenuCtrl;
|
|
@@ -13,22 +14,41 @@ export default class RoomMenuCtrl {
|
|
|
13
14
|
|
|
14
15
|
this._model = model;
|
|
15
16
|
this._view = view;
|
|
17
|
+
this._actions = actions;
|
|
16
18
|
|
|
17
19
|
this._vPublic = view.publisher;
|
|
18
|
-
this._vPublic.on('
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
this._vPublic.on('exit', () => {
|
|
21
|
+
if (model.openConfirm()) {
|
|
22
|
+
actions.onConfirmOpen?.();
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
this._vPublic.on('yes', () => {
|
|
26
|
+
if (!model.isConfirmOpen()) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
model.closeConfirm();
|
|
31
|
+
actions.onExit?.();
|
|
32
|
+
});
|
|
33
|
+
this._vPublic.on('cancel', () => {
|
|
34
|
+
if (!model.isConfirmOpen()) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
model.closeConfirm();
|
|
39
|
+
actions.onConfirmClose?.();
|
|
40
|
+
});
|
|
22
41
|
}
|
|
23
42
|
|
|
24
|
-
// абсолютная ссылка на комнату, в которой вкладка; null — скрыть
|
|
43
|
+
// абсолютная ссылка на комнату, в которой вкладка; null — скрыть кнопку
|
|
25
44
|
setLink(link) {
|
|
26
45
|
this._model.setLink(link);
|
|
27
46
|
}
|
|
28
47
|
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
|
|
48
|
+
// закрыть окно без выхода и без возврата клавиш: пауза сессии сама ими
|
|
49
|
+
// управляет
|
|
50
|
+
closeConfirm() {
|
|
51
|
+
this._model.closeConfirm();
|
|
32
52
|
}
|
|
33
53
|
|
|
34
54
|
// true — кнопка внутри панели (игра её показывает), false — в углу
|
|
@@ -36,8 +56,16 @@ export default class RoomMenuCtrl {
|
|
|
36
56
|
this._view.placeInPanel(inPanel);
|
|
37
57
|
}
|
|
38
58
|
|
|
39
|
-
// null | 'pending' | '
|
|
59
|
+
// null | 'pending' | 'switching' | 'slow'. Передача закрывает открытое
|
|
60
|
+
// окно — как Cancel: клавиши, выключенные окном, возвращает onConfirmClose
|
|
61
|
+
// (иначе срыв передачи до заморозки оставил бы хоста без управления)
|
|
40
62
|
setHandoff(status) {
|
|
63
|
+
const wasOpen = this._model.isConfirmOpen();
|
|
64
|
+
|
|
41
65
|
this._model.setHandoff(status);
|
|
66
|
+
|
|
67
|
+
if (wasOpen && !this._model.isConfirmOpen()) {
|
|
68
|
+
this._actions.onConfirmClose?.();
|
|
69
|
+
}
|
|
42
70
|
}
|
|
43
71
|
}
|
|
@@ -24,6 +24,27 @@ export default class VoteCtrl {
|
|
|
24
24
|
// {name: 'templateName', params: ['p1',..], values: ['v1',..] || 'values'}
|
|
25
25
|
// ['val1', 'val2'];
|
|
26
26
|
open(data) {
|
|
27
|
+
// личное меню хоста: проверяется раньше шаблона голосования (тоже объект)
|
|
28
|
+
if (typeof data === 'object' && data !== null && !Array.isArray(data)) {
|
|
29
|
+
if (Object.hasOwn(data, 'menu')) {
|
|
30
|
+
this._model.openPersonal(data.menu);
|
|
31
|
+
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (Object.hasOwn(data, 'menuClose')) {
|
|
36
|
+
this._model.closePersonal(data.menuClose);
|
|
37
|
+
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (Object.hasOwn(data, 'menuHint')) {
|
|
42
|
+
this._model.setHint(data.menuHint?.name, data.menuHint?.on);
|
|
43
|
+
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
27
48
|
// если данные - массив (values для созданного голосования)
|
|
28
49
|
if (Array.isArray(data)) {
|
|
29
50
|
this._model.updateValues(data);
|
|
@@ -64,6 +85,11 @@ export default class VoteCtrl {
|
|
|
64
85
|
this._model.closeEngineVote(name);
|
|
65
86
|
}
|
|
66
87
|
|
|
88
|
+
// CLEAR: личное меню закрывается, подсказки гаснут
|
|
89
|
+
clearPersonal() {
|
|
90
|
+
this._model.clearPersonal();
|
|
91
|
+
}
|
|
92
|
+
|
|
67
93
|
// голосования хоста не пережили переподключение
|
|
68
94
|
removeHostVotes() {
|
|
69
95
|
this._model.removeHostVotes();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Publisher from '../../../lib/Publisher.js';
|
|
2
|
+
import { VOTE_ANSWER_KEYS } from '../../../config/voteKeys.js';
|
|
2
3
|
|
|
3
4
|
// Singleton ControlsModel
|
|
4
5
|
|
|
@@ -120,8 +121,15 @@ export default class ControlsModel {
|
|
|
120
121
|
return;
|
|
121
122
|
}
|
|
122
123
|
|
|
124
|
+
// цифры открытого окна голосования/меню — только ответ окну, в игру
|
|
125
|
+
// не уходят; прочие клавиши идут и в игру (танк ездит при меню)
|
|
126
|
+
const isVoteAnswer =
|
|
127
|
+
this._currentModes.vote &&
|
|
128
|
+
keyCode >= VOTE_ANSWER_KEYS[0] &&
|
|
129
|
+
keyCode <= VOTE_ANSWER_KEYS[1];
|
|
130
|
+
|
|
123
131
|
// если клавиша ещё не зажата
|
|
124
|
-
if (!this._pressedKeys[keyCode]) {
|
|
132
|
+
if (!isVoteAnswer && !this._pressedKeys[keyCode]) {
|
|
125
133
|
const name = this._currentKeySet[keyCode];
|
|
126
134
|
|
|
127
135
|
if (name) {
|
|
@@ -2,19 +2,18 @@ import Publisher from '../../../lib/Publisher.js';
|
|
|
2
2
|
|
|
3
3
|
// Singleton RoomMenuModel
|
|
4
4
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// люди и мастер назначил бету (иначе передавать некому).
|
|
10
|
-
// Пока идёт передача, пункты заблокированы, на их месте статус. Только
|
|
5
|
+
// Выход из комнаты внутри матча: одна кнопка Exit у всех ролей, видна, пока
|
|
6
|
+
// вкладка в комнате (ссылка на комнату — признак «в комнате»). Клик
|
|
7
|
+
// открывает окно подтверждения «Leave the game?». Пока идёт плановая
|
|
8
|
+
// передача хоста, кнопка заблокирована, на её месте статус. Только
|
|
11
9
|
// лобби-режим: в solo ссылки на комнату нет
|
|
12
10
|
|
|
13
|
-
// handoff: null | 'pending'
|
|
11
|
+
// handoff: null | 'pending' (Exit хоста) | 'switching' (автопередача,
|
|
12
|
+
// вкладка остаётся) | 'slow' (бета долго не отвечает)
|
|
14
13
|
const STATUS_TEXT = {
|
|
15
|
-
pending: '
|
|
14
|
+
pending: 'Leaving…',
|
|
15
|
+
switching: 'Switching host…',
|
|
16
16
|
slow: 'Slow connection…',
|
|
17
|
-
failed: 'Host handover failed',
|
|
18
17
|
};
|
|
19
18
|
|
|
20
19
|
let roomMenuModel;
|
|
@@ -28,20 +27,18 @@ export default class RoomMenuModel {
|
|
|
28
27
|
roomMenuModel = this;
|
|
29
28
|
|
|
30
29
|
this._link = null;
|
|
31
|
-
this._open = false;
|
|
32
|
-
this._role = null; // null | 'host' | 'guest'
|
|
33
|
-
this._othersPresent = false;
|
|
34
|
-
this._hasSuccessor = false;
|
|
35
30
|
this._handoff = null;
|
|
31
|
+
// окно подтверждения открыто
|
|
32
|
+
this._confirm = false;
|
|
36
33
|
this.publisher = new Publisher();
|
|
37
34
|
}
|
|
38
35
|
|
|
39
|
-
// абсолютная ссылка на комнату; null — вкладка не в комнате,
|
|
36
|
+
// абсолютная ссылка на комнату; null — вкладка не в комнате, кнопка скрыта
|
|
40
37
|
setLink(link) {
|
|
41
38
|
this._link = link || null;
|
|
42
39
|
|
|
43
40
|
if (!this._link) {
|
|
44
|
-
this.
|
|
41
|
+
this._confirm = false;
|
|
45
42
|
}
|
|
46
43
|
|
|
47
44
|
this._emitState();
|
|
@@ -51,70 +48,58 @@ export default class RoomMenuModel {
|
|
|
51
48
|
return this._link;
|
|
52
49
|
}
|
|
53
50
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
* @param {'host'|'guest'|null} state.role
|
|
58
|
-
* @param {boolean} [state.othersPresent] - в комнате есть другие люди.
|
|
59
|
-
* @param {boolean} [state.hasSuccessor] - мастер назначил бету.
|
|
60
|
-
*/
|
|
61
|
-
setRole({ role, othersPresent = false, hasSuccessor = false }) {
|
|
62
|
-
this._role = role ?? null;
|
|
63
|
-
this._othersPresent = othersPresent === true;
|
|
64
|
-
this._hasSuccessor = hasSuccessor === true;
|
|
65
|
-
this._emitState();
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// ход плановой передачи: null | 'pending' | 'slow' | 'failed'
|
|
51
|
+
// ход плановой передачи: null | 'pending' | 'switching' | 'slow'. Идущая
|
|
52
|
+
// передача закрывает окно: Yes посреди неё сорвал бы плановую передачу в
|
|
53
|
+
// аварийную
|
|
69
54
|
setHandoff(status) {
|
|
70
55
|
this._handoff = status ?? null;
|
|
56
|
+
|
|
57
|
+
if (this._handoff !== null) {
|
|
58
|
+
this._confirm = false;
|
|
59
|
+
}
|
|
60
|
+
|
|
71
61
|
this._emitState();
|
|
72
62
|
}
|
|
73
63
|
|
|
74
|
-
|
|
75
|
-
|
|
64
|
+
isConfirmOpen() {
|
|
65
|
+
return this._confirm;
|
|
66
|
+
}
|
|
76
67
|
|
|
77
|
-
|
|
78
|
-
|
|
68
|
+
// окно открывается только у доступной кнопки; true — открылось сейчас
|
|
69
|
+
openConfirm() {
|
|
70
|
+
if (this._confirm || this._link === null || this._busy()) {
|
|
71
|
+
return false;
|
|
79
72
|
}
|
|
80
73
|
|
|
81
|
-
this.
|
|
82
|
-
this.
|
|
74
|
+
this._confirm = true;
|
|
75
|
+
this._emitState();
|
|
83
76
|
|
|
84
|
-
|
|
85
|
-
if (!next && this._handoff === 'failed') {
|
|
86
|
-
this.setHandoff(null);
|
|
87
|
-
}
|
|
77
|
+
return true;
|
|
88
78
|
}
|
|
89
79
|
|
|
90
|
-
|
|
91
|
-
this.
|
|
80
|
+
closeConfirm() {
|
|
81
|
+
this._confirm = false;
|
|
82
|
+
this._emitState();
|
|
92
83
|
}
|
|
93
84
|
|
|
94
85
|
/**
|
|
95
86
|
* Что показывать.
|
|
96
|
-
* @returns {{ visible: boolean,
|
|
97
|
-
* status: string|null
|
|
87
|
+
* @returns {{ visible: boolean, exit: { disabled: boolean },
|
|
88
|
+
* status: string|null, confirm: boolean }}
|
|
98
89
|
*/
|
|
99
90
|
getState() {
|
|
100
|
-
const inRoom = this._link !== null;
|
|
101
|
-
const busy = this._handoff === 'pending' || this._handoff === 'slow';
|
|
102
|
-
|
|
103
91
|
return {
|
|
104
|
-
visible:
|
|
105
|
-
|
|
106
|
-
handover: {
|
|
107
|
-
visible:
|
|
108
|
-
inRoom &&
|
|
109
|
-
this._role === 'host' &&
|
|
110
|
-
this._othersPresent &&
|
|
111
|
-
this._hasSuccessor,
|
|
112
|
-
disabled: busy,
|
|
113
|
-
},
|
|
92
|
+
visible: this._link !== null,
|
|
93
|
+
exit: { disabled: this._busy() },
|
|
114
94
|
status: STATUS_TEXT[this._handoff] ?? null,
|
|
95
|
+
confirm: this._confirm,
|
|
115
96
|
};
|
|
116
97
|
}
|
|
117
98
|
|
|
99
|
+
_busy() {
|
|
100
|
+
return this._handoff !== null;
|
|
101
|
+
}
|
|
102
|
+
|
|
118
103
|
_emitState() {
|
|
119
104
|
this.publisher.emit('state', this.getState());
|
|
120
105
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Publisher from '../../../lib/Publisher.js';
|
|
2
2
|
import { VOTE_ANSWER_KEYS } from '../../../config/voteKeys.js';
|
|
3
|
+
import { MENU_NAME_PATTERN } from '../../../config/menuName.js';
|
|
3
4
|
|
|
4
5
|
// Singleton VoteModel
|
|
5
6
|
|
|
@@ -7,6 +8,44 @@ import { VOTE_ANSWER_KEYS } from '../../../config/voteKeys.js';
|
|
|
7
8
|
// его ведёт мастер, а не хост, и игра таких имён не объявляет
|
|
8
9
|
const isEngineVote = name => typeof name === 'string' && name.startsWith('@');
|
|
9
10
|
|
|
11
|
+
// ограничения данных личного меню: хост недоверенный
|
|
12
|
+
const PERSONAL_MAX_ITEMS = 7;
|
|
13
|
+
const PERSONAL_MAX_DEPTH = 2;
|
|
14
|
+
const PERSONAL_MAX_TEXT = 64;
|
|
15
|
+
|
|
16
|
+
// узлы личного меню: усечённые копии, глубина ≤ 2, пунктов ≤ 7
|
|
17
|
+
function sanitizeNodes(nodes, depth) {
|
|
18
|
+
if (!Array.isArray(nodes)) {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (nodes.length > PERSONAL_MAX_ITEMS) {
|
|
23
|
+
console.warn(
|
|
24
|
+
`[vote] personal menu level has ${nodes.length} items, max ${PERSONAL_MAX_ITEMS}`,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return nodes.slice(0, PERSONAL_MAX_ITEMS).map(node => {
|
|
29
|
+
const out = {
|
|
30
|
+
text: String(node?.text ?? '').slice(0, PERSONAL_MAX_TEXT),
|
|
31
|
+
disabled: node?.disabled === true,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
if (depth < PERSONAL_MAX_DEPTH) {
|
|
35
|
+
const children = sanitizeNodes(node?.children, depth + 1);
|
|
36
|
+
|
|
37
|
+
if (children.length > 0) {
|
|
38
|
+
out.children = children;
|
|
39
|
+
} else if (node?.children !== undefined) {
|
|
40
|
+
// объявленная группа без валидных детей не должна стать листом
|
|
41
|
+
out.disabled = true;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return out;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
10
49
|
let voteModel;
|
|
11
50
|
|
|
12
51
|
export default class VoteModel {
|
|
@@ -41,6 +80,11 @@ export default class VoteModel {
|
|
|
41
80
|
// затирает чужое и не затирается им (этап 10)
|
|
42
81
|
this._queue = [];
|
|
43
82
|
|
|
83
|
+
// личное меню хоста (type 'personal'): { name, style, title, items,
|
|
84
|
+
// path }, path — индексы открытых групп; подсказки — имя → горит ли
|
|
85
|
+
this._personal = null;
|
|
86
|
+
this._hints = new Map();
|
|
87
|
+
|
|
44
88
|
this._title = null; // заголовок голосования
|
|
45
89
|
this._values = []; // все значения голосования
|
|
46
90
|
|
|
@@ -124,6 +168,137 @@ export default class VoteModel {
|
|
|
124
168
|
if (!(this._type === 'vote' && isEngineVote(this._voteName))) {
|
|
125
169
|
this.complete();
|
|
126
170
|
}
|
|
171
|
+
|
|
172
|
+
this._clearHints();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Личное меню хоста: заменяет любое содержимое окна и начинается с
|
|
177
|
+
* 1-го уровня; открытое голосование движка уходит в начало очереди.
|
|
178
|
+
* @param {Object} menu - { name, style, title, items }.
|
|
179
|
+
* @returns {boolean} данные приняты.
|
|
180
|
+
*/
|
|
181
|
+
openPersonal(menu) {
|
|
182
|
+
if (
|
|
183
|
+
!menu ||
|
|
184
|
+
!MENU_NAME_PATTERN.test(menu.name) ||
|
|
185
|
+
!MENU_NAME_PATTERN.test(menu.style)
|
|
186
|
+
) {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (
|
|
191
|
+
this._showing &&
|
|
192
|
+
this._type === 'vote' &&
|
|
193
|
+
isEngineVote(this._voteName)
|
|
194
|
+
) {
|
|
195
|
+
this._queue.unshift({
|
|
196
|
+
engine: {
|
|
197
|
+
name: this._voteName,
|
|
198
|
+
title: this._title,
|
|
199
|
+
values: this._values,
|
|
200
|
+
deadline: this._deadline,
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
this._waitingValues = false;
|
|
206
|
+
this._timeOff = true;
|
|
207
|
+
this._deadline = null;
|
|
208
|
+
this._type = 'personal';
|
|
209
|
+
this._voteName = '';
|
|
210
|
+
this._personal = {
|
|
211
|
+
name: menu.name,
|
|
212
|
+
style: menu.style,
|
|
213
|
+
title: String(menu.title ?? '').slice(0, PERSONAL_MAX_TEXT),
|
|
214
|
+
items: sanitizeNodes(menu.items, 1),
|
|
215
|
+
path: [],
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
this.show();
|
|
219
|
+
this.open();
|
|
220
|
+
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// закрывает окно, если оно показывает личное меню `name`
|
|
225
|
+
closePersonal(name) {
|
|
226
|
+
if (
|
|
227
|
+
this._showing &&
|
|
228
|
+
this._type === 'personal' &&
|
|
229
|
+
this._personal.name === name
|
|
230
|
+
) {
|
|
231
|
+
this.complete();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// зажигает/гасит подсказку меню
|
|
236
|
+
setHint(name, on) {
|
|
237
|
+
if (!MENU_NAME_PATTERN.test(name)) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
this._hints.set(name, on === true);
|
|
242
|
+
this.publisher.emit('hint', { name, on: on === true });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// CLEAR: личное меню закрывается, подсказки гаснут
|
|
246
|
+
clearPersonal() {
|
|
247
|
+
if (this._showing && this._type === 'personal') {
|
|
248
|
+
this.complete();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
this._clearHints();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
_clearHints() {
|
|
255
|
+
for (const [name, on] of this._hints) {
|
|
256
|
+
if (on) {
|
|
257
|
+
this.setHint(name, false);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// пункты текущего уровня личного меню
|
|
263
|
+
_personalLevel() {
|
|
264
|
+
let nodes = this._personal.items;
|
|
265
|
+
|
|
266
|
+
for (const index of this._personal.path) {
|
|
267
|
+
nodes = nodes[index].children;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return nodes;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// цифра в личном меню: 0 — выход, 8/9 — страниц нет, 1–7 — пункт
|
|
274
|
+
_updatePersonal(number) {
|
|
275
|
+
if (number === 0) {
|
|
276
|
+
this.complete();
|
|
277
|
+
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const node = this._personalLevel()[number - 1];
|
|
282
|
+
|
|
283
|
+
if (number >= 8 || !node || node.disabled) {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (node.children) {
|
|
288
|
+
this._personal.path.push(number - 1);
|
|
289
|
+
this.show();
|
|
290
|
+
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const answer = [...this._personal.path, number - 1]
|
|
295
|
+
.map(index => index + 1)
|
|
296
|
+
.join('.');
|
|
297
|
+
const name = this._personal.name;
|
|
298
|
+
|
|
299
|
+
// окно закрывается ДО отправки (синхронный транспорт, см. ниже)
|
|
300
|
+
this.complete();
|
|
301
|
+
this.publisher.emit('socket', [`menu:${name}`, answer]);
|
|
127
302
|
}
|
|
128
303
|
|
|
129
304
|
_startEngineVote({ name, title, values, deadline }) {
|
|
@@ -220,6 +395,12 @@ export default class VoteModel {
|
|
|
220
395
|
number = String.fromCharCode(keyCode);
|
|
221
396
|
number = parseInt(number, 10);
|
|
222
397
|
|
|
398
|
+
if (this._type === 'personal') {
|
|
399
|
+
this._updatePersonal(number);
|
|
400
|
+
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
|
|
223
404
|
// exit
|
|
224
405
|
if (number === 0) {
|
|
225
406
|
this.complete();
|
|
@@ -277,6 +458,27 @@ export default class VoteModel {
|
|
|
277
458
|
|
|
278
459
|
// отображает голосование
|
|
279
460
|
show() {
|
|
461
|
+
if (this._type === 'personal') {
|
|
462
|
+
this._showing = true;
|
|
463
|
+
this.publisher.emit('clear', this._timerId);
|
|
464
|
+
|
|
465
|
+
this.publisher.emit('vote', {
|
|
466
|
+
title: this._personal.title,
|
|
467
|
+
list: this._personalLevel().map(node => ({
|
|
468
|
+
text: node.text,
|
|
469
|
+
disabled: node.disabled,
|
|
470
|
+
group: Boolean(node.children),
|
|
471
|
+
})),
|
|
472
|
+
back: false,
|
|
473
|
+
more: false,
|
|
474
|
+
time: null,
|
|
475
|
+
style: this._personal.style,
|
|
476
|
+
level: this._personal.path.length + 1,
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
|
|
280
482
|
const begin = this._currentPage * 7;
|
|
281
483
|
const max = begin + 7;
|
|
282
484
|
let currentValues = [];
|