poi-plugin-gimmick-tracker 0.1.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/index.js ADDED
@@ -0,0 +1,1405 @@
1
+ //#region rolldown:runtime
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") {
10
+ for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
+ key = keys[i];
12
+ if (!__hasOwnProp.call(to, key) && key !== except) {
13
+ __defProp(to, key, {
14
+ get: ((k) => from[k]).bind(null, key),
15
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
16
+ });
17
+ }
18
+ }
19
+ }
20
+ return to;
21
+ };
22
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
23
+ value: mod,
24
+ enumerable: true
25
+ }) : target, mod));
26
+
27
+ //#endregion
28
+ let views_create_store = require("views/create-store");
29
+ let views_env = require("views/env");
30
+ let fs = require("fs");
31
+ let path = require("path");
32
+ let __blueprintjs_core = require("@blueprintjs/core");
33
+ let react = require("react");
34
+ react = __toESM(react);
35
+ let react_jsx_runtime = require("react/jsx-runtime");
36
+
37
+ //#region src/core/guide-parser.ts
38
+ const AIR_TEXT_TO_STATE = {
39
+ 空确: 1,
40
+ 空優: 2,
41
+ 空优: 2,
42
+ 空均: 3,
43
+ 空劣: 4
44
+ };
45
+ const TOKEN_PATTERN = /([A-Za-z]\d*)\s*点\s*(到达|到達)\s*(\d+)\s*次|([A-Za-z]\d*)\s*点\s*(SS|S|A|B|C|D|E)\s*胜\s*(\d+)\s*次|([A-Za-z]\d*)\s*点\s*(空确|空優|空优|空均|空劣)\s*(\d+)\s*次|(守家|基地防空)\s*(空确|空優|空优|空均|空劣)\s*(\d+)\s*次/gi;
46
+ function normalizeNodeLabel(value) {
47
+ return value.trim().toUpperCase();
48
+ }
49
+ function parseGuideText(text) {
50
+ const normalized = text.replace(/[,;]/g, (value) => value === "," ? "," : ";");
51
+ const parsed = [];
52
+ let match;
53
+ TOKEN_PATTERN.lastIndex = 0;
54
+ while ((match = TOKEN_PATTERN.exec(normalized)) !== null) {
55
+ const rawText = match[0].replace(/\s+/g, "");
56
+ if (match[1] != null) {
57
+ parsed.push({
58
+ kind: "node-arrival",
59
+ nodeLabel: normalizeNodeLabel(match[1]),
60
+ requiredCount: Number(match[3]),
61
+ rawText
62
+ });
63
+ continue;
64
+ }
65
+ if (match[4] != null) {
66
+ parsed.push({
67
+ kind: "victory-rank",
68
+ nodeLabel: normalizeNodeLabel(match[4]),
69
+ requiredRank: match[5].toUpperCase(),
70
+ requiredCount: Number(match[6]),
71
+ rawText
72
+ });
73
+ continue;
74
+ }
75
+ if (match[7] != null) {
76
+ parsed.push({
77
+ kind: "node-air-state",
78
+ nodeLabel: normalizeNodeLabel(match[7]),
79
+ requiredAirState: AIR_TEXT_TO_STATE[match[8]],
80
+ requiredCount: Number(match[9]),
81
+ rawText
82
+ });
83
+ continue;
84
+ }
85
+ parsed.push({
86
+ kind: "base-air-defense",
87
+ requiredAirState: AIR_TEXT_TO_STATE[match[11]],
88
+ requiredCount: Number(match[12]),
89
+ rawText
90
+ });
91
+ }
92
+ return parsed.filter((item) => Number.isInteger(item.requiredCount) && item.requiredCount > 0);
93
+ }
94
+
95
+ //#endregion
96
+ //#region src/core/conditions.ts
97
+ const RANK_SCORE = {
98
+ E: 0,
99
+ D: 1,
100
+ C: 2,
101
+ B: 3,
102
+ A: 4,
103
+ S: 5,
104
+ SS: 6
105
+ };
106
+ const AIR_LABEL = {
107
+ 1: "空确",
108
+ 2: "空优",
109
+ 3: "空均",
110
+ 4: "空劣"
111
+ };
112
+ function airStateLabel(state) {
113
+ return AIR_LABEL[state];
114
+ }
115
+ function meetsRank(actual, required) {
116
+ return RANK_SCORE[actual] >= RANK_SCORE[required];
117
+ }
118
+ function meetsAirState(actual, required) {
119
+ return actual <= required;
120
+ }
121
+ function conditionSignature(map, item) {
122
+ return [
123
+ map.mapId,
124
+ item.kind,
125
+ item.nodeLabel == null ? "" : normalizeNodeLabel(item.nodeLabel),
126
+ item.requiredRank ?? "",
127
+ item.requiredAirState ?? ""
128
+ ].join(":");
129
+ }
130
+ function observationMatches(condition, observation) {
131
+ if (condition.mapId !== observation.mapId || condition.kind !== observation.kind) return false;
132
+ if (condition.nodeLabel != null && normalizeNodeLabel(condition.nodeLabel) !== normalizeNodeLabel(observation.nodeLabel ?? "")) return false;
133
+ if (condition.kind === "victory-rank") return observation.kind === "victory-rank" && condition.requiredRank != null && meetsRank(observation.rank, condition.requiredRank);
134
+ if (condition.kind === "node-air-state" || condition.kind === "base-air-defense") return (observation.kind === "node-air-state" || observation.kind === "base-air-defense") && condition.requiredAirState != null && meetsAirState(observation.airState, condition.requiredAirState);
135
+ return observation.kind === "node-arrival";
136
+ }
137
+ function guideConditionLabel(condition) {
138
+ const node = condition.nodeLabel == null ? "" : `${condition.nodeLabel}点`;
139
+ switch (condition.kind) {
140
+ case "node-arrival": return `${node}到达 ${condition.requiredCount}次`;
141
+ case "victory-rank": return `${node}${condition.requiredRank}胜 ${condition.requiredCount}次`;
142
+ case "node-air-state": return `${node}${airStateLabel(condition.requiredAirState)} ${condition.requiredCount}次`;
143
+ case "base-air-defense": return `守家${airStateLabel(condition.requiredAirState)} ${condition.requiredCount}次`;
144
+ }
145
+ }
146
+ function observationLabel(observation) {
147
+ switch (observation.kind) {
148
+ case "node-arrival": return `${observation.nodeLabel}点到达`;
149
+ case "victory-rank": return `${observation.nodeLabel}点${observation.rank}胜`;
150
+ case "node-air-state": return `${observation.nodeLabel}点${airStateLabel(observation.airState)}`;
151
+ case "base-air-defense": return `守家${airStateLabel(observation.airState)}`;
152
+ }
153
+ }
154
+ function buildReviewCandidates(state, mapId, observationIds) {
155
+ const evidence = state.observations.filter((item) => observationIds.includes(item.id));
156
+ const guides = state.guideConditions.filter((condition) => condition.mapId === mapId && !condition.confirmed);
157
+ if (guides.length > 0) return guides.map((condition) => ({
158
+ id: `guide:${condition.id}`,
159
+ mode: "guide",
160
+ kind: condition.kind,
161
+ label: guideConditionLabel(condition),
162
+ conditionId: condition.id,
163
+ matchedInSortie: evidence.some((item) => observationMatches(condition, item)),
164
+ observedCount: condition.observedCount,
165
+ requiredCount: condition.requiredCount
166
+ })).sort((left, right) => Number(right.matchedInSortie) - Number(left.matchedInSortie));
167
+ return evidence.map((observation) => ({
168
+ id: `free:${observation.id}`,
169
+ mode: "free",
170
+ kind: observation.kind,
171
+ label: observationLabel(observation),
172
+ observationId: observation.id,
173
+ matchedInSortie: true
174
+ }));
175
+ }
176
+
177
+ //#endregion
178
+ //#region src/core/events.ts
179
+ const MAP_PATHS = new Set([
180
+ "/kcsapi/api_req_map/start",
181
+ "/kcsapi/api_req_map/next",
182
+ "/kcsapi/api_req_map/air_raid"
183
+ ]);
184
+ const BATTLE_SUFFIXES = new Set([
185
+ "battle",
186
+ "airbattle",
187
+ "ld_airbattle",
188
+ "night_to_day",
189
+ "sp_midnight",
190
+ "midnight",
191
+ "midnight_battle",
192
+ "battle_water",
193
+ "each_battle",
194
+ "each_battle_water",
195
+ "ec_battle",
196
+ "ec_midnight_battle",
197
+ "ec_night_to_day"
198
+ ]);
199
+ function isRecord(value) {
200
+ return typeof value === "object" && value !== null && !Array.isArray(value);
201
+ }
202
+ function asRecord(value) {
203
+ if (isRecord(value)) return value;
204
+ if (typeof value !== "string") return void 0;
205
+ try {
206
+ const parsed = JSON.parse(value);
207
+ return isRecord(parsed) ? parsed : void 0;
208
+ } catch {
209
+ return;
210
+ }
211
+ }
212
+ function asNumber(value) {
213
+ if (typeof value === "number" && Number.isFinite(value)) return value;
214
+ if (typeof value === "string" && value.trim() !== "") {
215
+ const parsed = Number(value);
216
+ return Number.isFinite(parsed) ? parsed : void 0;
217
+ }
218
+ }
219
+ function isRelevantPath(path$1) {
220
+ return path$1 === "/kcsapi/api_port/port" || MAP_PATHS.has(path$1) || isBattlePath(path$1) || isBattleResultPath(path$1);
221
+ }
222
+ function isBattleResultPath(path$1) {
223
+ return path$1.startsWith("/kcsapi/") && path$1.endsWith("/battleresult");
224
+ }
225
+ function isBattlePath(path$1) {
226
+ if (!/^\/kcsapi\/api_req_(sortie|combined_battle|battle_midnight)\//.test(path$1)) return false;
227
+ return BATTLE_SUFFIXES.has(path$1.slice(path$1.lastIndexOf("/") + 1));
228
+ }
229
+ function eventKey(detail) {
230
+ return `${detail.path}|${detail.time}`;
231
+ }
232
+ function extractMapRef(detail, fallback) {
233
+ const mapArea = asNumber(detail.body.api_maparea_id) ?? asNumber(detail.postBody?.api_maparea_id) ?? fallback?.mapArea ?? 0;
234
+ const mapNo = asNumber(detail.body.api_mapinfo_no) ?? asNumber(detail.postBody?.api_mapinfo_no) ?? fallback?.mapNo ?? 0;
235
+ return {
236
+ mapArea,
237
+ mapNo,
238
+ mapId: mapArea * 10 + mapNo
239
+ };
240
+ }
241
+ function extractNodeNo(body) {
242
+ return asNumber(body.api_no);
243
+ }
244
+ function extractBattleAirState(body) {
245
+ const value = asNumber(asRecord(asRecord(body.api_kouku)?.api_stage1)?.api_disp_seiku);
246
+ return value != null && value >= 1 && value <= 4 ? value : void 0;
247
+ }
248
+ function extractVictoryRank(body) {
249
+ const value = typeof body.api_win_rank === "string" ? body.api_win_rank.toUpperCase() : "";
250
+ return [
251
+ "SS",
252
+ "S",
253
+ "A",
254
+ "B",
255
+ "C",
256
+ "D",
257
+ "E"
258
+ ].includes(value) ? value : void 0;
259
+ }
260
+ function extractDestructionBattles(body) {
261
+ const raw = body.api_destruction_battle;
262
+ return (Array.isArray(raw) ? raw : raw == null ? [] : [raw]).map(asRecord).filter((item) => item != null);
263
+ }
264
+ function extractDefenseAirState(entry) {
265
+ const value = asNumber(asRecord(asRecord(entry.api_air_base_attack)?.api_stage1)?.api_disp_seiku);
266
+ return value != null && value >= 1 && value <= 4 ? value : void 0;
267
+ }
268
+ function hasPortSignal(detail) {
269
+ const eventObject = asRecord(detail.body.api_event_object);
270
+ return detail.path === "/kcsapi/api_port/port" && asNumber(eventObject?.api_m_flag2) === 1;
271
+ }
272
+ function hasBattleResultSignal(detail) {
273
+ return isBattleResultPath(detail.path) && asNumber(detail.body.api_m2) === 1;
274
+ }
275
+ function hasDefenseSignal(entry) {
276
+ return asNumber(entry.api_m2) === 1;
277
+ }
278
+ function extractPortMemberId(detail) {
279
+ if (detail.path !== "/kcsapi/api_port/port") return void 0;
280
+ return asRecord(detail.body.api_basic)?.api_member_id;
281
+ }
282
+
283
+ //#endregion
284
+ //#region src/core/types.ts
285
+ const PLUGIN_ID = "poi-plugin-gimmick-tracker";
286
+ const SCHEMA_VERSION = 1;
287
+ function createInitialState(now = 0) {
288
+ return {
289
+ schemaVersion: SCHEMA_VERSION,
290
+ observations: [],
291
+ guideConditions: [],
292
+ reviews: [],
293
+ freeCompletions: [],
294
+ processedEventKeys: [],
295
+ updatedAt: now
296
+ };
297
+ }
298
+ function cloneState(state) {
299
+ return JSON.parse(JSON.stringify(state));
300
+ }
301
+ function mapLabel(map) {
302
+ return map.mapArea > 0 && map.mapNo > 0 ? `${map.mapArea}-${map.mapNo}` : `地图 #${map.mapId}`;
303
+ }
304
+
305
+ //#endregion
306
+ //#region src/core/state-machine.ts
307
+ const MAX_PROCESSED_EVENT_KEYS = 2048;
308
+ function safeNodeLabel(resolver, map, apiNo) {
309
+ try {
310
+ const value = resolver({
311
+ mapArea: map.mapArea,
312
+ mapNo: map.mapNo,
313
+ apiNo
314
+ }).trim();
315
+ return value === "" ? `#${apiNo}` : value;
316
+ } catch {
317
+ return `#${apiNo}`;
318
+ }
319
+ }
320
+ function createSortie(detail, map, captureComplete) {
321
+ return {
322
+ ...map,
323
+ id: `${captureComplete ? "sortie" : "partial"}:${map.mapId}:${detail.time}`,
324
+ startedAt: detail.time,
325
+ captureComplete,
326
+ route: [],
327
+ timeline: [],
328
+ observationIds: []
329
+ };
330
+ }
331
+ function appendObservation(state, sortie, observation) {
332
+ if (state.observations.some((item) => item.id === observation.id)) return false;
333
+ state.observations.push(observation);
334
+ sortie.observationIds.push(observation.id);
335
+ for (const condition of state.guideConditions) if (observationMatches(condition, observation)) condition.observedCount += 1;
336
+ return true;
337
+ }
338
+ function currentNode(sortie) {
339
+ return sortie.route.at(-1);
340
+ }
341
+ function addNodeVisit(state, sortie, detail, nodeResolver) {
342
+ const nodeNo = extractNodeNo(detail.body);
343
+ if (nodeNo == null) return;
344
+ const id = `${eventKey(detail)}:node`;
345
+ if (sortie.route.some((visit$1) => visit$1.id === id)) return;
346
+ const nodeLabel = safeNodeLabel(nodeResolver, sortie, nodeNo);
347
+ const visit = {
348
+ id,
349
+ mapArea: sortie.mapArea,
350
+ mapNo: sortie.mapNo,
351
+ mapId: sortie.mapId,
352
+ nodeNo,
353
+ nodeLabel,
354
+ arrivedAt: detail.time,
355
+ sourcePath: detail.path
356
+ };
357
+ sortie.route.push(visit);
358
+ sortie.timeline.push({
359
+ id: `${id}:timeline`,
360
+ kind: "node",
361
+ occurredAt: detail.time,
362
+ sourcePath: detail.path,
363
+ nodeVisitId: id,
364
+ nodeNo,
365
+ nodeLabel
366
+ });
367
+ appendObservation(state, sortie, {
368
+ id: `${id}:arrival`,
369
+ kind: "node-arrival",
370
+ mapArea: sortie.mapArea,
371
+ mapNo: sortie.mapNo,
372
+ mapId: sortie.mapId,
373
+ nodeNo,
374
+ nodeLabel,
375
+ observedAt: detail.time,
376
+ sourcePath: detail.path,
377
+ sortieId: sortie.id
378
+ });
379
+ }
380
+ function addBattle(state, sortie, detail) {
381
+ const battle = {
382
+ timestamp: detail.time,
383
+ path: detail.path,
384
+ kind: detail.path.endsWith("/ld_airbattle") ? "fleet-air-raid" : "battle"
385
+ };
386
+ sortie.lastBattle = battle;
387
+ const node = currentNode(sortie);
388
+ if (node != null) {
389
+ node.battle = battle;
390
+ (node.battleEvents ??= []).push(battle);
391
+ }
392
+ const airState = extractBattleAirState(detail.body);
393
+ if (battle.kind === "fleet-air-raid") sortie.timeline.push({
394
+ id: `${eventKey(detail)}:fleet-air-raid`,
395
+ kind: "fleet-air-raid",
396
+ occurredAt: detail.time,
397
+ sourcePath: detail.path,
398
+ nodeVisitId: node?.id,
399
+ nodeNo: node?.nodeNo,
400
+ nodeLabel: node?.nodeLabel,
401
+ airState
402
+ });
403
+ if (node == null || airState == null) return;
404
+ node.airState = airState;
405
+ appendObservation(state, sortie, {
406
+ id: `${eventKey(detail)}:node-air`,
407
+ kind: "node-air-state",
408
+ mapArea: sortie.mapArea,
409
+ mapNo: sortie.mapNo,
410
+ mapId: sortie.mapId,
411
+ nodeNo: node.nodeNo,
412
+ nodeLabel: node.nodeLabel,
413
+ airState,
414
+ observedAt: detail.time,
415
+ sourcePath: detail.path,
416
+ sortieId: sortie.id,
417
+ battle
418
+ });
419
+ }
420
+ function addBattleResult(state, sortie, detail) {
421
+ const rank = extractVictoryRank(detail.body);
422
+ const node = currentNode(sortie);
423
+ if (rank == null || node == null) return;
424
+ node.rank = rank;
425
+ appendObservation(state, sortie, {
426
+ id: `${eventKey(detail)}:victory`,
427
+ kind: "victory-rank",
428
+ mapArea: sortie.mapArea,
429
+ mapNo: sortie.mapNo,
430
+ mapId: sortie.mapId,
431
+ nodeNo: node.nodeNo,
432
+ nodeLabel: node.nodeLabel,
433
+ rank,
434
+ observedAt: detail.time,
435
+ sourcePath: detail.path,
436
+ sortieId: sortie.id,
437
+ battle: sortie.lastBattle
438
+ });
439
+ }
440
+ function addDefenseBattles(state, sortie, detail) {
441
+ const entries = extractDestructionBattles(detail.body);
442
+ let signal = false;
443
+ entries.forEach((entry, index) => {
444
+ signal ||= hasDefenseSignal(entry);
445
+ const airState = extractDefenseAirState(entry);
446
+ const node = currentNode(sortie);
447
+ sortie.timeline.push({
448
+ id: `${eventKey(detail)}:defense:${index}:timeline`,
449
+ kind: "base-air-defense",
450
+ occurredAt: detail.time,
451
+ sourcePath: detail.path,
452
+ nodeVisitId: node?.id,
453
+ nodeNo: node?.nodeNo,
454
+ nodeLabel: node?.nodeLabel,
455
+ airState
456
+ });
457
+ if (node != null) (node.baseDefenseEvents ??= []).push({
458
+ timestamp: detail.time,
459
+ path: detail.path,
460
+ airState
461
+ });
462
+ if (airState == null) return;
463
+ appendObservation(state, sortie, {
464
+ id: `${eventKey(detail)}:defense:${index}`,
465
+ kind: "base-air-defense",
466
+ mapArea: sortie.mapArea,
467
+ mapNo: sortie.mapNo,
468
+ mapId: sortie.mapId,
469
+ airState,
470
+ observedAt: detail.time,
471
+ sourcePath: detail.path,
472
+ sortieId: sortie.id,
473
+ battle: {
474
+ timestamp: detail.time,
475
+ path: detail.path,
476
+ kind: "base-air-defense"
477
+ }
478
+ });
479
+ });
480
+ return { signal };
481
+ }
482
+ function reviewFromSortie(state, sortie, source, detectedAt, sealed) {
483
+ return {
484
+ id: `review:${sortie.id}`,
485
+ sortieId: sortie.id,
486
+ startedAt: sortie.startedAt,
487
+ mapArea: sortie.mapArea,
488
+ mapNo: sortie.mapNo,
489
+ mapId: sortie.mapId,
490
+ detectedAt,
491
+ triggerSources: [source],
492
+ sealed,
493
+ routeComplete: sortie.captureComplete,
494
+ route: cloneState({
495
+ schemaVersion: 1,
496
+ observations: [],
497
+ guideConditions: [],
498
+ reviews: [],
499
+ freeCompletions: [],
500
+ processedEventKeys: [],
501
+ updatedAt: 0,
502
+ activeSortie: sortie
503
+ }).activeSortie.route,
504
+ timeline: JSON.parse(JSON.stringify(sortie.timeline)),
505
+ observationIds: [...sortie.observationIds],
506
+ candidates: buildReviewCandidates(state, sortie.mapId, sortie.observationIds),
507
+ status: "pending"
508
+ };
509
+ }
510
+ function orphanReview(state, source, detectedAt) {
511
+ const sortieId = `orphan:${detectedAt}`;
512
+ return {
513
+ id: `review:${sortieId}`,
514
+ sortieId,
515
+ startedAt: detectedAt,
516
+ mapArea: 0,
517
+ mapNo: 0,
518
+ mapId: 0,
519
+ detectedAt,
520
+ triggerSources: [source],
521
+ sealed: source === "port",
522
+ routeComplete: false,
523
+ route: [],
524
+ timeline: [],
525
+ observationIds: [],
526
+ candidates: buildReviewCandidates(state, 0, []),
527
+ status: "pending"
528
+ };
529
+ }
530
+ function triggerReview(state, sortie, source, detectedAt, sealed) {
531
+ if (sortie == null && source === "port") {
532
+ const recentOrphan = [...state.reviews].reverse().find((item) => item.status === "pending" && item.sortieId.startsWith("orphan:") && !item.sealed && item.triggerSources.includes("battleresult") && !item.triggerSources.includes("port") && detectedAt - item.detectedAt <= 600 * 1e3);
533
+ if (recentOrphan != null) {
534
+ if (!recentOrphan.triggerSources.includes(source)) recentOrphan.triggerSources.push(source);
535
+ recentOrphan.sealed ||= sealed || source === "port";
536
+ return false;
537
+ }
538
+ }
539
+ const candidate = sortie == null ? orphanReview(state, source, detectedAt) : reviewFromSortie(state, sortie, source, detectedAt, sealed);
540
+ const existing = state.reviews.find((item) => item.id === candidate.id);
541
+ if (existing == null) {
542
+ state.reviews.push(candidate);
543
+ return true;
544
+ }
545
+ if (!existing.triggerSources.includes(source)) existing.triggerSources.push(source);
546
+ existing.sealed ||= sealed;
547
+ if (sortie != null) {
548
+ existing.routeComplete = sortie.captureComplete;
549
+ existing.route = candidate.route;
550
+ existing.timeline = candidate.timeline;
551
+ existing.observationIds = candidate.observationIds;
552
+ if (existing.status === "pending") existing.candidates = candidate.candidates;
553
+ }
554
+ return false;
555
+ }
556
+ function syncReviewForActiveSortie(state, sealed = false) {
557
+ const sortie = state.activeSortie;
558
+ if (sortie == null) return;
559
+ const review = state.reviews.find((item) => item.sortieId === sortie.id);
560
+ if (review == null) return;
561
+ review.sealed ||= sealed;
562
+ review.routeComplete = sortie.captureComplete;
563
+ review.route = JSON.parse(JSON.stringify(sortie.route));
564
+ review.timeline = JSON.parse(JSON.stringify(sortie.timeline));
565
+ review.observationIds = [...sortie.observationIds];
566
+ if (review.status === "pending") review.candidates = buildReviewCandidates(state, sortie.mapId, sortie.observationIds);
567
+ }
568
+ function handleMapEvent(state, detail, nodeResolver) {
569
+ if (detail.path === "/kcsapi/api_req_map/start") state.activeSortie = createSortie(detail, extractMapRef(detail), true);
570
+ else if (state.activeSortie == null) state.activeSortie = createSortie(detail, extractMapRef(detail), false);
571
+ else {
572
+ const map = extractMapRef(detail, state.activeSortie);
573
+ Object.assign(state.activeSortie, map);
574
+ }
575
+ const sortie = state.activeSortie;
576
+ if (detail.path !== "/kcsapi/api_req_map/air_raid") addNodeVisit(state, sortie, detail, nodeResolver);
577
+ const defense = addDefenseBattles(state, sortie, detail);
578
+ let shouldFocus = false;
579
+ if (defense.signal) shouldFocus = triggerReview(state, sortie, "air-defense", detail.time, false);
580
+ syncReviewForActiveSortie(state);
581
+ return shouldFocus;
582
+ }
583
+ function applyGameResponse(previous, detail, nodeResolver) {
584
+ if (!isRelevantPath(detail.path)) return {
585
+ state: previous,
586
+ changed: false,
587
+ shouldFocus: false
588
+ };
589
+ const key = eventKey(detail);
590
+ if (previous.processedEventKeys.includes(key)) return {
591
+ state: previous,
592
+ changed: false,
593
+ shouldFocus: false
594
+ };
595
+ const state = cloneState(previous);
596
+ state.processedEventKeys.push(key);
597
+ if (state.processedEventKeys.length > MAX_PROCESSED_EVENT_KEYS) state.processedEventKeys.splice(0, state.processedEventKeys.length - MAX_PROCESSED_EVENT_KEYS);
598
+ let shouldFocus = false;
599
+ if (MAP_PATHS.has(detail.path)) shouldFocus = handleMapEvent(state, detail, nodeResolver);
600
+ else if (isBattleResultPath(detail.path)) {
601
+ if (state.activeSortie != null) addBattleResult(state, state.activeSortie, detail);
602
+ if (hasBattleResultSignal(detail)) shouldFocus = triggerReview(state, state.activeSortie, "battleresult", detail.time, false);
603
+ syncReviewForActiveSortie(state);
604
+ } else if (isBattlePath(detail.path)) {
605
+ if (state.activeSortie != null) {
606
+ addBattle(state, state.activeSortie, detail);
607
+ syncReviewForActiveSortie(state);
608
+ }
609
+ } else if (detail.path === "/kcsapi/api_port/port") {
610
+ if (hasPortSignal(detail)) shouldFocus = triggerReview(state, state.activeSortie, "port", detail.time, true);
611
+ syncReviewForActiveSortie(state, true);
612
+ state.activeSortie = void 0;
613
+ }
614
+ state.updatedAt = detail.time;
615
+ return {
616
+ state,
617
+ changed: true,
618
+ shouldFocus
619
+ };
620
+ }
621
+ function refreshPendingCandidates(state) {
622
+ for (const review of state.reviews) {
623
+ if (review.status !== "pending") continue;
624
+ review.candidates = buildReviewCandidates(state, review.mapId, review.observationIds);
625
+ }
626
+ }
627
+ function importGuideConditions(previous, text, map, now) {
628
+ const parsed = parseGuideText(text);
629
+ if (parsed.length === 0) return {
630
+ state: previous,
631
+ importedCount: 0
632
+ };
633
+ const state = cloneState(previous);
634
+ const unique = /* @__PURE__ */ new Map();
635
+ for (const item of parsed) unique.set(conditionSignature(map, item), item);
636
+ for (const [signature, item] of unique) {
637
+ const id = `guide:${signature}`;
638
+ const existing = state.guideConditions.find((condition$1) => condition$1.id === id);
639
+ if (existing != null) {
640
+ existing.rawText = item.rawText;
641
+ existing.requiredCount = item.requiredCount;
642
+ existing.observedCount = state.observations.filter((observation) => observationMatches(existing, observation)).length;
643
+ continue;
644
+ }
645
+ const condition = {
646
+ ...map,
647
+ id,
648
+ kind: item.kind,
649
+ rawText: item.rawText,
650
+ nodeLabel: item.nodeLabel,
651
+ requiredRank: item.requiredRank,
652
+ requiredAirState: item.requiredAirState,
653
+ requiredCount: item.requiredCount,
654
+ observedCount: 0,
655
+ confirmed: false,
656
+ createdAt: now
657
+ };
658
+ condition.observedCount = state.observations.filter((observation) => observationMatches(condition, observation)).length;
659
+ state.guideConditions.push(condition);
660
+ }
661
+ refreshPendingCandidates(state);
662
+ state.updatedAt = now;
663
+ return {
664
+ state,
665
+ importedCount: unique.size
666
+ };
667
+ }
668
+ function confirmReview(previous, reviewId, candidateIds, now) {
669
+ if (candidateIds.length === 0) return previous;
670
+ const state = cloneState(previous);
671
+ const review = state.reviews.find((item) => item.id === reviewId);
672
+ if (review == null || review.status !== "pending") return previous;
673
+ const selected = review.candidates.filter((candidate) => candidateIds.includes(candidate.id));
674
+ if (selected.length === 0) return previous;
675
+ const mapHasGuide = state.guideConditions.some((condition) => condition.mapId === review.mapId);
676
+ for (const candidate of selected) {
677
+ if (candidate.mode === "guide" && candidate.conditionId != null) {
678
+ const condition = state.guideConditions.find((item) => item.id === candidate.conditionId);
679
+ if (condition != null && !condition.confirmed) {
680
+ condition.confirmed = true;
681
+ condition.confirmedAt = now;
682
+ condition.confirmedByReviewId = review.id;
683
+ }
684
+ continue;
685
+ }
686
+ if (mapHasGuide || candidate.observationId == null) continue;
687
+ if (state.observations.find((item) => item.id === candidate.observationId) == null) continue;
688
+ const completion = {
689
+ mapArea: review.mapArea,
690
+ mapNo: review.mapNo,
691
+ mapId: review.mapId,
692
+ id: `free-completion:${review.id}:${candidate.observationId}`,
693
+ kind: candidate.kind,
694
+ label: candidate.label,
695
+ reviewId: review.id,
696
+ observationId: candidate.observationId,
697
+ confirmedAt: now
698
+ };
699
+ if (!state.freeCompletions.some((item) => item.id === completion.id)) state.freeCompletions.push(completion);
700
+ }
701
+ review.status = "confirmed";
702
+ review.selectedCandidateIds = selected.map((candidate) => candidate.id);
703
+ review.resolvedAt = now;
704
+ refreshPendingCandidates(state);
705
+ state.updatedAt = now;
706
+ return state;
707
+ }
708
+ function ignoreReview(previous, reviewId, now) {
709
+ const state = cloneState(previous);
710
+ const review = state.reviews.find((item) => item.id === reviewId);
711
+ if (review == null || review.status !== "pending") return previous;
712
+ review.status = "ignored";
713
+ review.resolvedAt = now;
714
+ state.updatedAt = now;
715
+ return state;
716
+ }
717
+
718
+ //#endregion
719
+ //#region src/storage/index.ts
720
+ function normalizeMemberId(value) {
721
+ if (value == null) return void 0;
722
+ const normalized = typeof value === "number" ? String(value) : typeof value === "string" ? value : "";
723
+ return /^\d+$/.test(normalized) ? normalized : void 0;
724
+ }
725
+ function hydrateState(value) {
726
+ if (typeof value !== "object" || value == null) return createInitialState();
727
+ const raw = value;
728
+ if (raw.schemaVersion !== SCHEMA_VERSION) return createInitialState();
729
+ const reviews = Array.isArray(raw.reviews) ? raw.reviews.map((review) => ({
730
+ ...review,
731
+ startedAt: typeof review.startedAt === "number" ? review.startedAt : review.route?.[0]?.arrivedAt ?? review.detectedAt,
732
+ timeline: Array.isArray(review.timeline) ? review.timeline : []
733
+ })) : [];
734
+ const activeSortie = raw.activeSortie != null ? {
735
+ ...raw.activeSortie,
736
+ timeline: Array.isArray(raw.activeSortie.timeline) ? raw.activeSortie.timeline : []
737
+ } : void 0;
738
+ return {
739
+ ...createInitialState(typeof raw.updatedAt === "number" ? raw.updatedAt : 0),
740
+ ...raw,
741
+ schemaVersion: SCHEMA_VERSION,
742
+ observations: Array.isArray(raw.observations) ? raw.observations : [],
743
+ guideConditions: Array.isArray(raw.guideConditions) ? raw.guideConditions : [],
744
+ reviews,
745
+ freeCompletions: Array.isArray(raw.freeCompletions) ? raw.freeCompletions : [],
746
+ processedEventKeys: Array.isArray(raw.processedEventKeys) ? raw.processedEventKeys : [],
747
+ activeSortie
748
+ };
749
+ }
750
+ var FileTrackerStorage = class {
751
+ constructor(appDataPath) {
752
+ this.appDataPath = appDataPath;
753
+ }
754
+ statePath(memberId) {
755
+ const safe = normalizeMemberId(memberId);
756
+ if (safe == null) throw new Error("Invalid memberId");
757
+ return (0, path.join)(this.appDataPath, "gimmick-tracker", safe, "state-v1.json");
758
+ }
759
+ load(memberId) {
760
+ const path$1 = this.statePath(memberId);
761
+ if (!(0, fs.existsSync)(path$1)) return createInitialState();
762
+ try {
763
+ return hydrateState(JSON.parse((0, fs.readFileSync)(path$1, "utf8")));
764
+ } catch {
765
+ return createInitialState();
766
+ }
767
+ }
768
+ save(memberId, state) {
769
+ const path$1 = this.statePath(memberId);
770
+ (0, fs.mkdirSync)((0, path.join)(path$1, ".."), { recursive: true });
771
+ const temporary = `${path$1}.${process.pid}.${Date.now()}.tmp`;
772
+ try {
773
+ (0, fs.writeFileSync)(temporary, `${JSON.stringify(state, null, 2)}\n`, "utf8");
774
+ (0, fs.renameSync)(temporary, path$1);
775
+ } catch (error) {
776
+ try {
777
+ if ((0, fs.existsSync)(temporary)) (0, fs.unlinkSync)(temporary);
778
+ } catch {}
779
+ throw error;
780
+ }
781
+ }
782
+ };
783
+
784
+ //#endregion
785
+ //#region src/controller.ts
786
+ function parseEvent(event) {
787
+ if (typeof event !== "object" || event == null) return void 0;
788
+ const detail = event.detail;
789
+ if (typeof detail !== "object" || detail == null) return void 0;
790
+ const value = detail;
791
+ if (typeof value.path !== "string" || typeof value.time !== "number") return void 0;
792
+ return {
793
+ method: typeof value.method === "string" ? value.method : void 0,
794
+ path: value.path,
795
+ body: asRecord(value.body) ?? {},
796
+ postBody: asRecord(value.postBody),
797
+ time: value.time
798
+ };
799
+ }
800
+ function mergeById(stored, transient) {
801
+ const merged = new Map(stored.map((item) => [item.id, item]));
802
+ for (const item of transient) merged.set(item.id, item);
803
+ return [...merged.values()];
804
+ }
805
+ var TrackerController = class {
806
+ state = createInitialState();
807
+ subscribers = /* @__PURE__ */ new Set();
808
+ eventTarget;
809
+ memberId;
810
+ started = false;
811
+ handleResponse = (event) => {
812
+ const detail = parseEvent(event);
813
+ if (detail == null) return;
814
+ try {
815
+ const preferred = this.readPreferredMemberId();
816
+ const portMember = normalizeMemberId(extractPortMemberId(detail));
817
+ this.switchMember(portMember ?? preferred);
818
+ const result = applyGameResponse(this.state, detail, this.dependencies.nodeResolver);
819
+ if (!result.changed) return;
820
+ this.state = result.state;
821
+ this.persist();
822
+ this.emit();
823
+ if (result.shouldFocus) try {
824
+ this.dependencies.focus.focusPlugin(this.dependencies.pluginId);
825
+ } catch (error) {
826
+ this.report(error);
827
+ }
828
+ } catch (error) {
829
+ this.report(error);
830
+ }
831
+ };
832
+ constructor(dependencies) {
833
+ this.dependencies = dependencies;
834
+ }
835
+ start(target) {
836
+ if (this.started) return;
837
+ this.started = true;
838
+ this.eventTarget = target;
839
+ this.switchMember(this.readPreferredMemberId());
840
+ target.addEventListener("game.response", this.handleResponse);
841
+ this.emit();
842
+ }
843
+ stop() {
844
+ if (!this.started) return;
845
+ this.eventTarget?.removeEventListener("game.response", this.handleResponse);
846
+ this.eventTarget = void 0;
847
+ this.started = false;
848
+ }
849
+ getSnapshot = () => this.state;
850
+ subscribe = (subscriber) => {
851
+ this.subscribers.add(subscriber);
852
+ return () => this.subscribers.delete(subscriber);
853
+ };
854
+ currentMap() {
855
+ if (this.state.activeSortie != null && this.state.activeSortie.mapId > 0) return {
856
+ mapArea: this.state.activeSortie.mapArea,
857
+ mapNo: this.state.activeSortie.mapNo,
858
+ mapId: this.state.activeSortie.mapId
859
+ };
860
+ const review = [...this.state.reviews].reverse().find((item) => item.mapId > 0);
861
+ if (review != null) return {
862
+ mapArea: review.mapArea,
863
+ mapNo: review.mapNo,
864
+ mapId: review.mapId
865
+ };
866
+ const observation = this.state.observations.at(-1);
867
+ return observation == null ? void 0 : {
868
+ mapArea: observation.mapArea,
869
+ mapNo: observation.mapNo,
870
+ mapId: observation.mapId
871
+ };
872
+ }
873
+ importGuide(text, map = this.currentMap()) {
874
+ if (map == null) return 0;
875
+ const result = importGuideConditions(this.state, text, map, this.now());
876
+ if (result.state === this.state) return result.importedCount;
877
+ this.state = result.state;
878
+ this.persist();
879
+ this.emit();
880
+ return result.importedCount;
881
+ }
882
+ confirm(reviewId, candidateIds) {
883
+ const next = confirmReview(this.state, reviewId, candidateIds, this.now());
884
+ if (next === this.state) return;
885
+ this.state = next;
886
+ this.persist();
887
+ this.emit();
888
+ }
889
+ ignore(reviewId) {
890
+ const next = ignoreReview(this.state, reviewId, this.now());
891
+ if (next === this.state) return;
892
+ this.state = next;
893
+ this.persist();
894
+ this.emit();
895
+ }
896
+ now() {
897
+ return this.dependencies.now?.() ?? Date.now();
898
+ }
899
+ readPreferredMemberId() {
900
+ try {
901
+ return normalizeMemberId(this.dependencies.getMemberId?.());
902
+ } catch (error) {
903
+ this.report(error);
904
+ return;
905
+ }
906
+ }
907
+ switchMember(next) {
908
+ if (next == null || next === this.memberId) return;
909
+ if (this.memberId != null) this.persist();
910
+ const transient = this.memberId == null ? this.state : void 0;
911
+ this.memberId = next;
912
+ try {
913
+ const stored = this.dependencies.storage.load(next);
914
+ if (transient == null || transient.updatedAt === 0 && transient.activeSortie == null) {
915
+ this.state = stored;
916
+ return;
917
+ }
918
+ this.state = {
919
+ ...stored,
920
+ activeSortie: transient.activeSortie ?? stored.activeSortie,
921
+ observations: mergeById(stored.observations, transient.observations),
922
+ guideConditions: mergeById(stored.guideConditions, transient.guideConditions),
923
+ reviews: mergeById(stored.reviews, transient.reviews),
924
+ freeCompletions: mergeById(stored.freeCompletions, transient.freeCompletions),
925
+ processedEventKeys: [...new Set([...stored.processedEventKeys, ...transient.processedEventKeys])].slice(-2048),
926
+ updatedAt: Math.max(stored.updatedAt, transient.updatedAt)
927
+ };
928
+ } catch (error) {
929
+ this.state = transient ?? createInitialState();
930
+ this.report(error);
931
+ }
932
+ }
933
+ persist() {
934
+ if (this.memberId == null) return;
935
+ try {
936
+ this.dependencies.storage.save(this.memberId, this.state);
937
+ } catch (error) {
938
+ this.report(error);
939
+ }
940
+ }
941
+ emit() {
942
+ for (const subscriber of this.subscribers) subscriber();
943
+ }
944
+ report(error) {
945
+ this.dependencies.onError?.(error);
946
+ }
947
+ };
948
+
949
+ //#endregion
950
+ //#region src/host/focus.ts
951
+ function tryIpc(ipc$1, id) {
952
+ try {
953
+ const focus = ipc$1?.access("MainWindow")?.ipcFocusPlugin;
954
+ if (typeof focus !== "function") return false;
955
+ focus(id);
956
+ return true;
957
+ } catch {
958
+ return false;
959
+ }
960
+ }
961
+ function createFocusAdapter(dependencies) {
962
+ return { focusPlugin(id) {
963
+ if (tryIpc(dependencies.primaryIpc, id)) return "ipc";
964
+ if (tryIpc(dependencies.legacyIpc, id)) return "legacy-ipc";
965
+ try {
966
+ if (typeof dependencies.dispatch === "function") {
967
+ const tabInfo = Boolean(dependencies.getStore?.("config.poi.tabarea.double")) ? { activePluginName: id } : {
968
+ activeMainTab: id,
969
+ activePluginName: id
970
+ };
971
+ dependencies.dispatch({
972
+ type: "@@TabSwitch",
973
+ payload: {
974
+ tabInfo,
975
+ autoSwitch: false
976
+ }
977
+ });
978
+ return "dispatch";
979
+ }
980
+ } catch {}
981
+ try {
982
+ if (typeof dependencies.success === "function") {
983
+ dependencies.success("检测到机关变化,请打开机关条件追踪器确认。");
984
+ return "notice";
985
+ }
986
+ } catch {}
987
+ return "none";
988
+ } };
989
+ }
990
+
991
+ //#endregion
992
+ //#region src/host/node-resolver.ts
993
+ function createFcdNodeResolver(getStore$1) {
994
+ return ({ mapArea, mapNo, apiNo }) => {
995
+ const fallback = `#${apiNo}`;
996
+ try {
997
+ const fcdMap = getStore$1("fcd.map");
998
+ if (typeof fcdMap !== "object" || fcdMap == null) return fallback;
999
+ const map = fcdMap[`${mapArea}-${mapNo}`];
1000
+ if (typeof map !== "object" || map == null) return fallback;
1001
+ const route$1 = map.route;
1002
+ if (typeof route$1 !== "object" || route$1 == null) return fallback;
1003
+ const edge = route$1[String(apiNo)];
1004
+ if (!Array.isArray(edge) || typeof edge[1] !== "string" || edge[1].trim() === "") return fallback;
1005
+ return edge[1];
1006
+ } catch {
1007
+ return fallback;
1008
+ }
1009
+ };
1010
+ }
1011
+
1012
+ //#endregion
1013
+ //#region i18n/zh-CN.json
1014
+ var zh_CN_default = {
1015
+ 机关条件追踪器: "机关条件追踪器",
1016
+ 记录出击路线并在机关触发后请用户确认完成条件: "记录出击路线并在机关触发后请用户确认完成条件",
1017
+ title: "机关条件追踪器",
1018
+ subtitle: "平时安静记录;检测到机关变化后才请你确认。数据只保存在本机。",
1019
+ activeMap: "当前海域",
1020
+ pendingReviews: "待确认触发",
1021
+ pending: "待确认",
1022
+ noPending: "暂无待确认触发。普通出击不会自动切换到本页。",
1023
+ unknownMap: "未知海域",
1024
+ route: "本次路线",
1025
+ incompleteRoute: "未捕获到完整出击:插件可能在出击中途加载,或仅收到了回港信号。",
1026
+ emptyRoute: "没有可显示的节点路线。",
1027
+ candidates: "这次完成了哪一项?",
1028
+ chooseCandidate: "请选择本次完成条件(● 表示本次有直接证据)",
1029
+ noCandidates: "没有足够事实生成候选;可忽略本次,或先导入攻略条件后再观察下一次出击。",
1030
+ matchedThisSortie: "本次有证据",
1031
+ confirm: "确认完成",
1032
+ ignore: "忽略本次",
1033
+ ignored: "已忽略",
1034
+ guide: "导入攻略条件",
1035
+ guidePlaceholder: "例如:D点S胜2次;C2点到达2次 A2点空优1次,守家空优2次",
1036
+ "import": "导入到当前海域",
1037
+ importNeedMap: "尚未识别当前海域,请先捕获一次地图出击。",
1038
+ importSuccess: "已识别 {count} 条条件。",
1039
+ noMatch: "未识别到受支持的条件格式。",
1040
+ completed: "按海域的上次触发与完成清单",
1041
+ lastTriggeredSortie: "上一次触发出击",
1042
+ triggerHistory: "每次机关触发出击",
1043
+ noCompleted: "尚未导入攻略条件,也没有自由完成记录。",
1044
+ observed: "观测",
1045
+ confirmed: "已确认",
1046
+ unconfirmed: "未确认",
1047
+ freeRecord: "无攻略时的自由记录"
1048
+ };
1049
+
1050
+ //#endregion
1051
+ //#region src/ui/index.tsx
1052
+ const styles = {
1053
+ root: {
1054
+ padding: 16,
1055
+ height: "100%",
1056
+ overflow: "auto",
1057
+ boxSizing: "border-box"
1058
+ },
1059
+ header: {
1060
+ display: "flex",
1061
+ justifyContent: "space-between",
1062
+ gap: 16,
1063
+ alignItems: "center"
1064
+ },
1065
+ section: { marginTop: 16 },
1066
+ review: { marginTop: 12 },
1067
+ reviewGrid: {
1068
+ display: "grid",
1069
+ gridTemplateColumns: "minmax(260px, 1.2fr) minmax(280px, .8fr)",
1070
+ gap: 18
1071
+ },
1072
+ route: {
1073
+ display: "flex",
1074
+ flexWrap: "wrap",
1075
+ gap: 6,
1076
+ marginTop: 8
1077
+ },
1078
+ historyRow: {
1079
+ display: "grid",
1080
+ gridTemplateColumns: "minmax(280px, 1fr) auto",
1081
+ gap: 16,
1082
+ padding: "10px 0",
1083
+ borderBottom: "1px solid rgba(128,128,128,.18)"
1084
+ },
1085
+ candidate: {
1086
+ padding: "6px 0",
1087
+ borderBottom: "1px solid rgba(128,128,128,.18)"
1088
+ },
1089
+ actions: {
1090
+ display: "flex",
1091
+ gap: 8,
1092
+ marginTop: 12
1093
+ },
1094
+ guideRow: {
1095
+ display: "grid",
1096
+ gridTemplateColumns: "1fr auto auto",
1097
+ gap: 12,
1098
+ alignItems: "center"
1099
+ },
1100
+ muted: { color: "#738091" }
1101
+ };
1102
+ const t = (key) => zh_CN_default[key];
1103
+ function routeBadge(review, index) {
1104
+ const visit = review.route[index];
1105
+ const details = [visit.rank == null ? void 0 : `${visit.rank}胜`];
1106
+ if (visit.airState != null) details.push(airStateLabel(visit.airState));
1107
+ if (visit.battleEvents?.some((event) => event.kind === "fleet-air-raid")) details.push("舰队空袭");
1108
+ for (const defense of visit.baseDefenseEvents ?? []) details.push(`基地防空${defense.airState == null ? "" : ` · ${airStateLabel(defense.airState)}`}`);
1109
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__blueprintjs_core.Tag, {
1110
+ minimal: index !== review.route.length - 1,
1111
+ title: new Date(visit.arrivedAt).toLocaleString(),
1112
+ children: [visit.nodeLabel, details.filter(Boolean).length > 0 ? ` · ${details.filter(Boolean).join(" · ")}` : ""]
1113
+ }, visit.id);
1114
+ }
1115
+ function formatSortieTime(review) {
1116
+ const startedAt = review.startedAt ?? review.route[0]?.arrivedAt ?? review.detectedAt;
1117
+ return `${new Date(startedAt).toLocaleString()} ~ ${new Date(review.detectedAt).toLocaleString()}`;
1118
+ }
1119
+ function timelineBadge(review, event) {
1120
+ if (event.kind === "node") {
1121
+ const index = review.route.findIndex((visit) => visit.id === event.nodeVisitId);
1122
+ if (index >= 0) return routeBadge(review, index);
1123
+ }
1124
+ const label = event.kind === "fleet-air-raid" ? "舰队空袭" : "基地防空";
1125
+ const parts = [event.nodeLabel == null ? void 0 : `${event.nodeLabel}点`, label];
1126
+ if (event.airState != null) parts.push(airStateLabel(event.airState));
1127
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__blueprintjs_core.Tag, {
1128
+ intent: event.kind === "base-air-defense" ? "warning" : "primary",
1129
+ title: new Date(event.occurredAt).toLocaleString(),
1130
+ children: parts.filter(Boolean).join(" · ")
1131
+ }, event.id);
1132
+ }
1133
+ function sortieTimeline(review) {
1134
+ const timeline = review.timeline ?? [];
1135
+ if (timeline.length > 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1136
+ style: styles.route,
1137
+ children: timeline.map((event) => timelineBadge(review, event))
1138
+ });
1139
+ if (review.route.length > 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1140
+ style: styles.route,
1141
+ children: review.route.map((_, index) => routeBadge(review, index))
1142
+ });
1143
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1144
+ style: styles.muted,
1145
+ children: t("emptyRoute")
1146
+ });
1147
+ }
1148
+ function PendingCard({ review, selected, setSelected, controller: controller$1 }) {
1149
+ const selectedCandidate = review.candidates.find((candidate) => candidate.id === selected);
1150
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__blueprintjs_core.Card, {
1151
+ style: styles.review,
1152
+ elevation: __blueprintjs_core.Elevation.ONE,
1153
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1154
+ style: styles.header,
1155
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
1156
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: review.mapId > 0 ? mapLabel(review) : t("unknownMap") }),
1157
+ " ",
1158
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__blueprintjs_core.Tag, {
1159
+ intent: "warning",
1160
+ children: t("pending")
1161
+ }),
1162
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1163
+ style: styles.muted,
1164
+ children: formatSortieTime(review)
1165
+ })
1166
+ ] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1167
+ style: styles.route,
1168
+ children: review.triggerSources.map((source) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__blueprintjs_core.Tag, {
1169
+ minimal: true,
1170
+ children: source
1171
+ }, source))
1172
+ })]
1173
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1174
+ style: styles.reviewGrid,
1175
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
1176
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", { children: t("route") }),
1177
+ !review.routeComplete && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__blueprintjs_core.Callout, {
1178
+ intent: "warning",
1179
+ children: t("incompleteRoute")
1180
+ }),
1181
+ sortieTimeline(review)
1182
+ ] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
1183
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", { children: t("candidates") }),
1184
+ review.candidates.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__blueprintjs_core.Callout, { children: t("noCandidates") }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__blueprintjs_core.HTMLSelect, {
1185
+ fill: true,
1186
+ value: selected,
1187
+ onChange: (event) => setSelected(event.currentTarget.value),
1188
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1189
+ value: "",
1190
+ children: t("chooseCandidate")
1191
+ }), review.candidates.map((candidate) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
1192
+ value: candidate.id,
1193
+ children: [
1194
+ candidate.matchedInSortie ? "● " : "",
1195
+ candidate.label,
1196
+ candidate.observedCount == null ? "" : ` · ${t("observed")} ${candidate.observedCount}/${candidate.requiredCount}`
1197
+ ]
1198
+ }, candidate.id))]
1199
+ }), selectedCandidate?.matchedInSortie && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__blueprintjs_core.Callout, {
1200
+ style: { marginTop: 8 },
1201
+ intent: "success",
1202
+ children: t("matchedThisSortie")
1203
+ })] }),
1204
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1205
+ style: styles.actions,
1206
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(__blueprintjs_core.Button, {
1207
+ intent: "primary",
1208
+ disabled: selected === "",
1209
+ onClick: () => controller$1.confirm(review.id, [selected]),
1210
+ children: t("confirm")
1211
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__blueprintjs_core.Button, {
1212
+ onClick: () => controller$1.ignore(review.id),
1213
+ children: t("ignore")
1214
+ })]
1215
+ })
1216
+ ] })]
1217
+ })]
1218
+ });
1219
+ }
1220
+ function createTrackerView(controller$1) {
1221
+ return function GimmickTrackerView() {
1222
+ const state = (0, react.useSyncExternalStore)(controller$1.subscribe, controller$1.getSnapshot, controller$1.getSnapshot);
1223
+ const [guideText, setGuideText] = (0, react.useState)("");
1224
+ const [message, setMessage] = (0, react.useState)("");
1225
+ const [selected, setSelected] = (0, react.useState)({});
1226
+ const pending$1 = state.reviews.filter((review) => review.status === "pending");
1227
+ const currentMap = controller$1.currentMap();
1228
+ const mapIds = (0, react.useMemo)(() => [...new Set([
1229
+ ...state.guideConditions.map((item) => item.mapId),
1230
+ ...state.freeCompletions.map((item) => item.mapId),
1231
+ ...state.reviews.filter((item) => item.mapId > 0).map((item) => item.mapId)
1232
+ ])].sort((left, right) => left - right), [
1233
+ state.guideConditions,
1234
+ state.freeCompletions,
1235
+ state.reviews
1236
+ ]);
1237
+ const importGuide = () => {
1238
+ if (currentMap == null) {
1239
+ setMessage(t("importNeedMap"));
1240
+ return;
1241
+ }
1242
+ const count = controller$1.importGuide(guideText, currentMap);
1243
+ setMessage(count > 0 ? t("importSuccess").replace("{count}", String(count)) : t("noMatch"));
1244
+ };
1245
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("main", {
1246
+ style: styles.root,
1247
+ children: [
1248
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
1249
+ style: styles.header,
1250
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
1251
+ style: { margin: 0 },
1252
+ children: t("title")
1253
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1254
+ style: {
1255
+ ...styles.muted,
1256
+ marginBottom: 0
1257
+ },
1258
+ children: t("subtitle")
1259
+ })] }), currentMap != null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__blueprintjs_core.Tag, {
1260
+ large: true,
1261
+ children: [
1262
+ t("activeMap"),
1263
+ ": ",
1264
+ mapLabel(currentMap)
1265
+ ]
1266
+ })]
1267
+ }),
1268
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1269
+ style: styles.section,
1270
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: t("pendingReviews") }), pending$1.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__blueprintjs_core.Callout, { children: t("noPending") }) : pending$1.map((review) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PendingCard, {
1271
+ review,
1272
+ controller: controller$1,
1273
+ selected: selected[review.id] ?? "",
1274
+ setSelected: (id) => setSelected((value) => ({
1275
+ ...value,
1276
+ [review.id]: id
1277
+ }))
1278
+ }, review.id))]
1279
+ }),
1280
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1281
+ style: styles.section,
1282
+ children: [
1283
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: t("guide") }),
1284
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__blueprintjs_core.TextArea, {
1285
+ fill: true,
1286
+ rows: 4,
1287
+ value: guideText,
1288
+ placeholder: t("guidePlaceholder"),
1289
+ onChange: (event) => setGuideText(event.currentTarget.value)
1290
+ }),
1291
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1292
+ style: styles.actions,
1293
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(__blueprintjs_core.Button, {
1294
+ intent: "primary",
1295
+ onClick: importGuide,
1296
+ children: t("import")
1297
+ }), message !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1298
+ style: styles.muted,
1299
+ children: message
1300
+ })]
1301
+ })
1302
+ ]
1303
+ }),
1304
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1305
+ style: styles.section,
1306
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: t("completed") }), mapIds.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__blueprintjs_core.Callout, { children: t("noCompleted") }) : mapIds.map((mapId) => {
1307
+ const guides = state.guideConditions.filter((item) => item.mapId === mapId);
1308
+ const free = state.freeCompletions.filter((item) => item.mapId === mapId);
1309
+ const reviews = state.reviews.filter((item) => item.mapId === mapId);
1310
+ const lastReview = reviews.at(-1);
1311
+ const map = guides[0] ?? free[0] ?? lastReview;
1312
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(__blueprintjs_core.Card, {
1313
+ style: styles.review,
1314
+ elevation: __blueprintjs_core.Elevation.ZERO,
1315
+ children: [
1316
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", { children: map == null ? `#${mapId}` : mapLabel(map) }),
1317
+ reviews.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1318
+ style: { marginBottom: 16 },
1319
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: t("triggerHistory") }), [...reviews].reverse().map((review) => {
1320
+ const selectedLabels = review.candidates.filter((candidate) => review.selectedCandidateIds?.includes(candidate.id)).map((candidate) => candidate.label);
1321
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1322
+ style: styles.historyRow,
1323
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: sortieTimeline(review) }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1324
+ style: { textAlign: "right" },
1325
+ children: [
1326
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1327
+ style: styles.muted,
1328
+ children: formatSortieTime(review)
1329
+ }),
1330
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__blueprintjs_core.Tag, {
1331
+ intent: review.status === "confirmed" ? "success" : review.status === "pending" ? "warning" : "none",
1332
+ children: review.status === "confirmed" ? t("confirmed") : review.status === "pending" ? t("pending") : t("ignored")
1333
+ }),
1334
+ selectedLabels.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: selectedLabels.join("、") })
1335
+ ]
1336
+ })]
1337
+ }, review.id);
1338
+ })]
1339
+ }),
1340
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__blueprintjs_core.HTMLTable, {
1341
+ compact: true,
1342
+ striped: true,
1343
+ style: { width: "100%" },
1344
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tbody", { children: [guides.map((condition) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
1345
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: guideConditionLabel(condition) }),
1346
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("td", { children: [
1347
+ t("observed"),
1348
+ " ",
1349
+ condition.observedCount,
1350
+ "/",
1351
+ condition.requiredCount
1352
+ ] }),
1353
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__blueprintjs_core.Tag, {
1354
+ intent: condition.confirmed ? "success" : "none",
1355
+ children: condition.confirmed ? t("confirmed") : t("unconfirmed")
1356
+ }) })
1357
+ ] }, condition.id)), free.map((completion) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
1358
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: completion.label }),
1359
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: t("freeRecord") }),
1360
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__blueprintjs_core.Tag, {
1361
+ intent: "success",
1362
+ children: t("confirmed")
1363
+ }) })
1364
+ ] }, completion.id))] })
1365
+ })
1366
+ ]
1367
+ }, mapId);
1368
+ })]
1369
+ })
1370
+ ]
1371
+ });
1372
+ };
1373
+ }
1374
+
1375
+ //#endregion
1376
+ //#region index-src.ts
1377
+ const controller = new TrackerController({
1378
+ storage: new FileTrackerStorage(window.APPDATA_PATH ?? views_env.APPDATA_PATH),
1379
+ focus: createFocusAdapter({
1380
+ primaryIpc: views_env.ipc,
1381
+ legacyIpc: window.ipc,
1382
+ dispatch: views_create_store.dispatch,
1383
+ getStore: (path$1) => (0, views_create_store.getStore)(path$1),
1384
+ success: window.success
1385
+ }),
1386
+ pluginId: PLUGIN_ID,
1387
+ nodeResolver: createFcdNodeResolver((path$1) => (0, views_create_store.getStore)(path$1)),
1388
+ getMemberId: () => (0, views_create_store.getStore)("info.basic.api_member_id"),
1389
+ onError: (error) => console.error(`[${PLUGIN_ID}]`, error)
1390
+ });
1391
+ const reactClass = createTrackerView(controller);
1392
+ const windowMode = false;
1393
+ function pluginDidLoad() {
1394
+ controller.start(window);
1395
+ }
1396
+ function pluginWillUnload() {
1397
+ controller.stop();
1398
+ }
1399
+
1400
+ //#endregion
1401
+ exports.pluginDidLoad = pluginDidLoad;
1402
+ exports.pluginWillUnload = pluginWillUnload;
1403
+ exports.reactClass = reactClass;
1404
+ exports.windowMode = windowMode;
1405
+ //# sourceMappingURL=index.js.map