forked from OverlayPlugin/cactbot
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathoopsyraidsy_config.ts
466 lines (421 loc) · 14.7 KB
/
oopsyraidsy_config.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
import { UnreachableCode } from '../../resources/not_reached';
import UserConfig, { OptionsTemplate, UserFileCallback } from '../../resources/user_config';
import { BaseOptions } from '../../types/data';
import { LooseOopsyTriggerSet, OopsyFileData } from '../../types/oopsy';
import {
CactbotConfigurator,
ConfigLooseOopsyTriggerSet,
ConfigProcessedFile,
ConfigProcessedFileMap,
} from '../config/config';
import { generateBuffTriggerIds } from './buff_map';
import oopsyFileData from './data/oopsy_manifest.txt';
import { OopsyOptions } from './oopsy_options';
const oopsyHelpers: (keyof LooseOopsyTriggerSet)[] = [
'damageWarn',
'damageFail',
'shareWarn',
'shareFail',
'gainsEffectWarn',
'gainsEffectFail',
];
// This could be a checkbox, but it's possible we could add more things here,
// like changing fail->warning or who knows what.
const kTriggerOptions = {
default: {
label: {
en: '✔ Defaults',
de: '✔ Standards',
fr: '✔ Défauts',
ja: '✔ 初期設定',
cn: '✔ 默认设置',
ko: '✔ 기본',
},
},
disabled: {
label: {
en: '❌ Disabled',
de: '❌ Deaktiviert',
fr: '❌ Désactivé',
ja: '❌ 無効',
cn: '❌ 禁用',
ko: '❌ 비활성화',
},
},
};
class OopsyConfigurator {
private base: CactbotConfigurator;
private readonly optionKey = 'oopsyraidsy';
constructor(cactbotConfigurator: CactbotConfigurator) {
this.base = cactbotConfigurator;
}
buildUI(container: HTMLElement, files: OopsyFileData) {
const fileMap = this.processOopsyFiles(files);
const expansionDivs: { [expansion: string]: HTMLElement } = {};
for (const info of Object.values(fileMap)) {
const expansion = info.prefix;
if (!info.triggers || Object.keys(info.triggers).length === 0)
continue;
let expansionDiv = expansionDivs[expansion];
if (!expansionDiv) {
const expansionContainer = document.createElement('div');
expansionContainer.classList.add('trigger-expansion-container', 'collapsed');
container.appendChild(expansionContainer);
const expansionHeader = document.createElement('div');
expansionHeader.classList.add('trigger-expansion-header');
expansionHeader.onclick = () => {
expansionContainer.classList.toggle('collapsed');
};
expansionHeader.innerText = expansion;
expansionContainer.appendChild(expansionHeader);
expansionDiv = expansionDivs[expansion] = expansionContainer;
}
const triggerContainer = document.createElement('div');
triggerContainer.classList.add('trigger-file-container', 'collapsed');
expansionDiv.appendChild(triggerContainer);
const headerDiv = document.createElement('div');
headerDiv.classList.add('trigger-file-header');
headerDiv.onclick = () => {
triggerContainer.classList.toggle('collapsed');
};
const parts = [info.title, info.type, expansion];
for (const part of parts) {
if (part === undefined)
continue;
const partDiv = document.createElement('div');
partDiv.classList.add('trigger-file-header-part');
partDiv.innerText = part;
headerDiv.appendChild(partDiv);
}
triggerContainer.appendChild(headerDiv);
const triggerOptions = document.createElement('div');
triggerOptions.classList.add('trigger-file-options');
triggerContainer.appendChild(triggerOptions);
for (const id of Object.keys(info.triggers ?? {})) {
// Build the trigger label.
const triggerDiv = document.createElement('div');
triggerDiv.innerHTML = id;
triggerDiv.classList.add('trigger');
triggerOptions.appendChild(triggerDiv);
// Build the trigger comment
const comment = info.triggers[id]?.comment;
if (comment) {
const trigComment = comment[this.base.lang] ?? comment?.en ?? '';
const triggerComment = document.createElement('div');
triggerComment.innerHTML = trigComment;
triggerComment.classList.add('comment');
triggerDiv.appendChild(triggerComment);
}
// Container for the right side ui (select boxes, all of the info).
const triggerDetails = document.createElement('div');
triggerDetails.classList.add('trigger-details');
triggerOptions.appendChild(triggerDetails);
triggerDetails.appendChild(this.buildTriggerOptions(id, triggerDiv));
}
}
}
buildTriggerOptions(id: string, labelDiv: HTMLElement): HTMLElement {
const kField = 'Output';
const div = document.createElement('div');
div.classList.add('trigger-options');
const updateLabel = (input: HTMLOptionElement | HTMLSelectElement) => {
if (input.value === 'hidden' || input.value === 'disabled')
labelDiv.classList.add('disabled');
else
labelDiv.classList.remove('disabled');
};
const input = document.createElement('select');
div.appendChild(input);
const selectValue = this.base.getOption(this.optionKey, ['triggers', id, kField], 'default');
for (const [key, value] of Object.entries(kTriggerOptions)) {
const elem = document.createElement('option');
elem.innerHTML = this.base.translate(value.label);
elem.value = key;
elem.selected = key === selectValue;
input.appendChild(elem);
updateLabel(input);
input.onchange = () => {
updateLabel(input);
let value = input.value;
if (value.includes('default'))
value = 'default';
this.base.setOption(this.optionKey, ['triggers', id, kField], input.value);
};
}
return div;
}
processOopsyFiles(files: OopsyFileData): ConfigProcessedFileMap<LooseOopsyTriggerSet> {
const map = this.base.processFiles(files);
// Hackily insert "missed buffs" into the list of triggers.
const generalEntry = map['00-misc-general'];
if (!generalEntry)
throw new UnreachableCode();
const fakeBuffs: ConfigProcessedFile<LooseOopsyTriggerSet> = {
...generalEntry,
fileKey: '00-misc-buffs',
filename: 'buff_map.ts',
title: this.base.translate({
en: 'Missed Buffs',
de: 'Verfehlte Buffs',
fr: 'Buffs manqués',
ja: '欠けバフ',
cn: '遗漏Buff',
ko: '놓친 버프 알림',
}),
triggerSet: {
triggers: generateBuffTriggerIds().map((id) => {
return { id: id };
}),
},
};
map[fakeBuffs.fileKey] = fakeBuffs;
for (const item of Object.values(map)) {
item.triggers = {};
const triggerSet = item.triggerSet;
for (const prop of oopsyHelpers) {
const obj = triggerSet[prop];
if (obj === undefined || obj === null)
continue;
if (typeof obj === 'object') {
for (const id in obj)
item.triggers[id] = { id: id };
}
}
if (!triggerSet.triggers)
continue;
for (const trigger of triggerSet.triggers) {
if (!trigger.id)
continue;
// Skip triggers that just set data, but include triggers that are just ids.
if (trigger.run && !trigger.mistake)
continue;
item.triggers[trigger.id] = trigger;
}
}
return map;
}
}
const templateOptions: OptionsTemplate = {
buildExtraUI: (base, container) => {
const builder = new OopsyConfigurator(base);
builder.buildUI(container, oopsyFileData);
},
processExtraOptions: (baseOptions, savedConfig) => {
// TODO: Rewrite user_config to be templated on option type so that this function knows
// what type of options it is using. Without this, perTriggerAutoConfig is unknown.
const options = baseOptions as OopsyOptions;
const perTriggerAutoConfig = options['PerTriggerAutoConfig'] ??= {};
if (typeof savedConfig !== 'object' || Array.isArray(savedConfig))
return;
const triggers = savedConfig['triggers'];
if (typeof triggers !== 'object' || Array.isArray(triggers))
return;
for (const [id, entry] of Object.entries(triggers)) {
if (typeof entry !== 'object' || Array.isArray(entry))
continue;
const output = entry['Output'];
if (output === undefined)
continue;
perTriggerAutoConfig[id] = {
enabled: output !== 'disabled',
};
}
},
options: [
{
id: 'Debug',
name: {
en: 'Enable debug mode',
de: 'Aktiviere Debugmodus',
fr: 'Activer le mode debug',
ja: 'デバッグモードを有効にする',
cn: '启用调试模式',
ko: '디버그 모드 활성화',
},
type: 'checkbox',
debugOnly: true,
default: false,
},
{
id: 'DefaultPlayerLabel',
name: {
en: 'Default Player Label',
de: 'Standard Spieler Label',
fr: 'Label par défaut du joueur',
ja: '基本プレイヤーラベル',
cn: '默认玩家代称',
ko: '플레이어를 언급하는 기본 방법',
},
type: 'select',
options: {
en: {
'Nickname (Tini)': 'nick',
'Role (Tank)': 'role',
'Job (WAR)': 'job',
'Full Job (Warrior)': 'jobFull',
'Full Name (Tini Poutini)': 'name',
},
fr: {
'Pseudo (Tini)': 'nick',
'Role (Tank)': 'role',
'Job (GUE)': 'job',
'Job complet (Guerrier)': 'jobFull',
'Nom complet (Tini Poutini)': 'name',
},
ja: {
'あだ名 (Tini)': 'nick',
'ロール (ヒーラー)': 'role',
'簡略ジョブ (白魔)': 'job',
'ジョブ (白魔導士)': 'jobFull',
'名前 (Tini Poutini)': 'name',
},
cn: {
'昵称 (弗雷)': 'nick',
'职能 (坦克)': 'role',
'职业简称 (暗骑)': 'job',
'职业全称 (暗黑骑士)': 'jobFull',
'全名 (弗雷)': 'name',
},
ko: {
'닉네임 (Tini)': 'nick',
'역할 (탱커)': 'role',
'직업 (암기)': 'job',
'직업 전체 (암흑기사)': 'jobFull',
'이름 전체 (Tini Poutini)': 'name',
},
},
default: 'nick',
},
{
id: 'NumLiveListItemsInCombat',
name: {
en: 'Number of mistakes to show in combat',
de: 'Anzahl der Fehler, die während des Kampfes angezeigt werden',
fr: 'Nombre de fautes à afficher en combat',
ja: '戦闘中に表示するミスをした回数',
cn: '战斗中显示的错误数量',
ko: '전투 중 표시할 실수들의 개수',
},
type: 'integer',
default: 5,
},
{
id: 'MinimumTimeForPullMistake',
name: {
en: 'Minimum time to show early pull (seconds)',
de: 'Minimum Zeit in der Early-Pulls angezeigt werden (in Sekunden)',
fr: 'Durée minimale pour afficher l\'early pull (secondes)',
ja: 'タゲ取るのが早かったら、ミスとして表示する、カウントダウンとの最短時間 (秒)',
cn: '显示提前开怪最小时间 (秒)',
ko: '풀링이 빠르다고 표시 할 최소 시간 (초)',
},
type: 'float',
default: 0.4,
},
{
id: 'TimeToShowDeathReportSeconds',
name: {
en: 'Seconds to show death report on death (0=none)',
de: 'Sekunden um den Todesreport beim Tot anzuzeigen (0=niemals)',
fr: 'Durée d’affichage (en secondes) du rapport de mort (0 = aucun)',
ja: '倒れた時にデスレポートを表示 (0=非表示)',
cn: '死亡时显示死亡报告的秒数 (0=不显示)',
ko: '죽었을 때 사망 보고서를 보여주는 시간(초) (0=비활성화)',
},
type: 'float',
default: 4,
setterFunc: (value, options) => {
let seconds;
if (typeof value === 'string')
seconds = parseFloat(value);
else if (typeof value === 'number')
seconds = value;
else
return;
// Store in a separate variable with a different unit.
options['TimeToShowDeathReportMs'] = seconds * 1000;
},
},
{
id: 'DeathReportSide',
name: {
en: 'How to show the death report',
de: 'Wie zeige ich den Todesreport an',
fr: 'Où afficher le rapport de mort',
ja: 'デスレポートの表示方法',
cn: '死亡报告的显示方式',
ko: '사망 보고서 표시 위치',
},
type: 'select',
options: {
en: {
'Left Side': 'left',
'Right Side': 'right',
'❌ Disabled': 'disabled',
},
de: {
'Left Side': 'links',
'Right Side': 'rechts',
'❌ Disabled': 'deaktiviert',
},
fr: {
'Côté gauche': 'gauche',
'Côté droit': 'droite',
'❌ Disabled': 'désactivé',
},
ja: {
'左側': 'left',
'右側': 'right',
'❌ 無効': 'disabled',
},
cn: {
'左侧': 'left',
'右侧': 'right',
'❌ 禁用': 'disabled',
},
ko: {
'왼쪽': 'left',
'오른쪽': 'right',
'❌ 비활성화': 'disabled',
},
},
default: 'left',
},
{
id: 'MinimumTimeForOverwrittenMit',
name: {
en: 'Minimum time to show overwritten mit (seconds)',
de: 'Minimum Zeit überschriebene Mitigation anzuzeigen (Sekunden)',
fr: 'Temps minimum pour afficher l\'écrasement des mitigation (s)',
ja: 'バフの上書き通知を表示する時間 (秒)',
cn: '显示被顶减伤最小时间 (秒)',
ko: '파티 생존기 덮어씀 경고를 표시할 기준 시간 (초)',
},
type: 'float',
default: 2,
},
],
};
const userFileHandler: UserFileCallback = (
name: string,
_files: { [filename: string]: string },
baseOptions: BaseOptions & Partial<OopsyOptions>,
basePath: string,
) => {
// TODO: Rewrite user_config to be templated on option type so that this function knows
// what type of options it is using.
if (!baseOptions.Triggers)
return;
for (const baseTriggerSet of baseOptions.Triggers) {
const set: ConfigLooseOopsyTriggerSet = baseTriggerSet;
// Annotate triggers with where they came from. Note, options is passed in repeatedly
// as multiple sets of user files add triggers, so only process each file once.
if (set.isUserTriggerSet)
continue;
// `filename` here is just cosmetic for better debug printing to make it more clear
// where a trigger or an override is coming from.
set.filename = `${basePath}${name}`;
set.isUserTriggerSet = true;
}
};
UserConfig.registerOptions('oopsyraidsy', templateOptions, userFileHandler);