1 // Copyright (C) 2013, 2015 Kazuhiro Fujieda <fujieda@users.osdn.me>
\r
3 // Licensed under the Apache License, Version 2.0 (the "License");
\r
4 // you may not use this file except in compliance with the License.
\r
5 // You may obtain a copy of the License at
\r
7 // http://www.apache.org/licenses/LICENSE-2.0
\r
9 // Unless required by applicable law or agreed to in writing, software
\r
10 // distributed under the License is distributed on an "AS IS" BASIS,
\r
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
\r
12 // See the License for the specific language governing permissions and
\r
13 // limitations under the License.
\r
16 using System.Collections.Generic;
\r
17 using System.Drawing;
\r
19 using System.Windows.Forms;
\r
20 using System.Xml.Serialization;
\r
21 using static System.Math;
\r
23 namespace KancolleSniffer
\r
25 public class QuestStatus
\r
27 public int Id { get; set; }
\r
28 public int Category { get; set; }
\r
29 public string Name { get; set; }
\r
30 public string Detail { get; set; }
\r
31 public int Progress { get; set; }
\r
34 public QuestCount Count { get; set; }
\r
37 public Color Color { get; set; }
\r
40 public enum QuestInterval
\r
49 public class QuestSpec
\r
51 public QuestInterval Interval { get; set; }
\r
52 public int Max { get; set; }
\r
53 public int[] MaxArray { get; set; }
\r
54 public bool AdjustCount { get; set; } = true;
\r
55 public int Shift { get; set; }
\r
58 public class QuestSortie : QuestSpec
\r
60 public string Rank { get; set; }
\r
61 public int[] Maps { get; set; }
\r
62 public int[] ShipTypes { get; set; }
\r
64 public static int CompareRank(string a, string b)
\r
66 const string ranks = "SABCDE";
\r
67 return ranks.IndexOf(a, StringComparison.Ordinal) -
\r
68 ranks.IndexOf(b, StringComparison.Ordinal);
\r
71 public bool Check(string rank, int map, bool boss)
\r
73 return (Rank == null || CompareRank(rank, Rank) <= 0) &&
\r
74 (Maps == null || Maps.Contains(map) && boss);
\r
78 public class QuestEnemyType : QuestSpec
\r
80 public int[] EnemyType { get; set; } = new int[0];
\r
82 public int CountResult(IEnumerable<ShipStatus> enemyResult) =>
\r
83 enemyResult.Count(ship => ship.NowHp == 0 && EnemyType.Contains(ship.Spec.ShipType));
\r
86 public class QuestPractice : QuestSpec
\r
88 public bool Win { get; set; }
\r
89 public bool Check(string rank) => !Win || QuestSortie.CompareRank(rank, "B") <= 0;
\r
92 public class QuestMission : QuestSpec
\r
94 public int[] Ids { get; set; }
\r
95 public bool Check(int id) => Ids == null || Ids.Contains(id);
\r
98 public class QuestDestroyItem : QuestSpec
\r
100 public int[] Items { get; set; }
\r
101 public bool Check(int id) => Items == null || Items.Contains(id);
\r
104 public class QuestPowerup : QuestSpec
\r
108 public class QuestCount
\r
110 public int Id { get; set; }
\r
111 public int Now { get; set; }
\r
112 public int[] NowArray { get; set; }
\r
115 public QuestSpec Spec { get; set; }
\r
117 public bool AdjustCount(int progress)
\r
119 if (!Spec.AdjustCount)
\r
121 if (NowArray != null)
\r
123 if (progress != 100)
\r
125 NowArray = NowArray.Zip(Spec.MaxArray, Max).ToArray();
\r
144 var now = Now + Spec.Shift;
\r
145 var max = Spec.Max + Spec.Shift;
\r
146 var low = (int)Ceiling(max * progress / 100.0);
\r
147 var high = (int)Ceiling(max * next / 100.0);
\r
150 Now = low - Spec.Shift;
\r
155 Now = high - 1 - Spec.Shift;
\r
161 public override string ToString()
\r
164 return $"{NowArray.Count(n => n >= 1)}/{Spec.MaxArray.Length}";
\r
165 return NowArray != null
\r
166 ? string.Join(" ", NowArray.Zip(Spec.MaxArray, (n, m) => $"{n}/{m}"))
\r
167 : $"{Now}/{Spec.Max}";
\r
170 public string ToToolTip()
\r
175 new[] {"2-4", "6-1", "6-3", "6-4"}.Zip(NowArray, (map, n) => n >= 1 ? map : "")
\r
176 .Where(s => !string.IsNullOrEmpty(s)));
\r
179 public bool Cleared => NowArray?.Zip(Spec.MaxArray, (n, m) => n >= m).All(x => x) ?? Now >= Spec.Max;
\r
183 public class QuestCountList
\r
185 private const QuestInterval Daily = QuestInterval.Daily;
\r
186 private const QuestInterval Weekly = QuestInterval.Weekly;
\r
187 private const QuestInterval Monthly = QuestInterval.Monthly;
\r
188 private const QuestInterval Quarterly = QuestInterval.Quarterly;
\r
191 /// このテーブルは七四式電子観測儀を参考に作成した。
\r
192 /// https://github.com/andanteyk/ElectronicObserver/blob/develop/ElectronicObserver/Data/Quest/QuestProgressManager.cs
\r
194 private static readonly Dictionary<int, QuestSpec> QuestSpecs = new Dictionary<int, QuestSpec>
\r
196 {201, new QuestSortie {Interval = Daily, Max = 1, Rank = "B"}}, // 201: 敵艦隊を撃滅せよ!
\r
197 {216, new QuestSortie {Interval = Daily, Max = 1, Rank = "B"}}, // 216: 敵艦隊主力を撃滅せよ!
\r
198 {210, new QuestSortie {Interval = Daily, Max = 10}}, // 210: 敵艦隊を10回邀撃せよ!
\r
199 {211, new QuestEnemyType {Interval = Daily, Max = 3, EnemyType = new[] {7, 11}}}, // 211: 敵空母を3隻撃沈せよ!
\r
200 {212, new QuestEnemyType {Interval = Daily, Max = 5, EnemyType = new[] {15}}}, // 212: 敵輸送船団を叩け!
\r
201 {218, new QuestEnemyType {Interval = Daily, Max = 3, EnemyType = new[] {15}}}, // 218: 敵補給艦を3隻撃沈せよ!
\r
202 {226, new QuestSortie {Interval = Daily, Max = 5, Rank = "B", Maps = new[] {21, 22, 23, 24, 25}}}, // 226: 南西諸島海域の制海権を握れ!
\r
203 {230, new QuestEnemyType {Interval = Daily, Max = 6, EnemyType = new[] {13}}}, // 230: 敵潜水艦を制圧せよ!
\r
205 {213, new QuestEnemyType {Interval = Weekly, Max = 20, EnemyType = new[] {15}}}, // 213: 海上通商破壊作戦
\r
206 {214, new QuestSpec {Interval = Weekly, MaxArray = new[] {36, 6, 24, 12}}}, // 214: あ号作戦
\r
207 {220, new QuestEnemyType {Interval = Weekly, Max = 20, EnemyType = new[] {7, 11}}}, // 220: い号作戦
\r
208 {221, new QuestEnemyType {Interval = Weekly, Max = 50, EnemyType = new[] {15}}}, // 221: ろ号作戦
\r
209 {228, new QuestEnemyType {Interval = Weekly, Max = 15, EnemyType = new[] {13}}}, // 228: 海上護衛戦
\r
210 {229, new QuestSortie {Interval = Weekly, Max = 12, Rank = "B", Maps = new[] {41, 42, 43, 44, 45}}}, // 229: 敵東方艦隊を撃滅せよ!
\r
211 {241, new QuestSortie {Interval = Weekly, Max = 5, Rank = "B", Maps = new[] {33, 34, 35}}}, // 241: 敵北方艦隊主力を撃滅せよ!
\r
212 {242, new QuestSortie {Interval = Weekly, Max = 1, Rank = "B", Maps = new[] {44}}}, // 242: 敵東方中枢艦隊を撃破せよ!
\r
213 {243, new QuestSortie {Interval = Weekly, Max = 2, Rank = "S", Maps = new[] {52}}}, // 243: 南方海域珊瑚諸島沖の制空権を握れ!
\r
214 {256, new QuestSortie {Interval = Monthly, Max = 3, Rank = "S", Maps = new[] {61}}}, // 256: 「潜水艦隊」出撃せよ!
\r
215 {261, new QuestSortie {Interval = Weekly, Max = 3, Rank = "A", Maps = new[] {15}}}, // 261: 海上輸送路の安全確保に努めよ!
\r
216 {265, new QuestSortie {Interval = Monthly, Max = 10, Rank = "A", Maps = new[] {15}}}, // 265: 海上護衛強化月間
\r
218 {822, new QuestSortie {Interval = Quarterly, Max = 2, Rank = "S", Maps = new[] {24}}}, // 822: 沖ノ島海域迎撃戦
\r
219 {854, new QuestSpec {Interval = Quarterly, MaxArray = new[] {1, 1, 1, 1}}}, // 854: 戦果拡張任務!「Z作戦」前段作戦
\r
221 {303, new QuestPractice {Interval = Daily, Max = 3, Win = false}}, // 303: 「演習」で練度向上!
\r
222 {304, new QuestPractice {Interval = Daily, Max = 5, Win = true}}, // 304: 「演習」で他提督を圧倒せよ!
\r
223 {302, new QuestPractice {Interval = Weekly, Max = 20, Win = true}}, // 302: 大規模演習
\r
224 {311, new QuestPractice {Interval = Daily, Max = 7, Win = true}}, // 311: 精鋭艦隊演習
\r
226 {402, new QuestMission {Interval = Daily, Max = 3}}, // 402: 「遠征」を3回成功させよう!
\r
227 {403, new QuestMission {Interval = Daily, Max = 10}}, // 403: 「遠征」を10回成功させよう!
\r
228 {404, new QuestMission {Interval = Weekly, Max = 30}}, // 404: 大規模遠征作戦、発令!
\r
229 {410, new QuestMission {Interval = Weekly, Max = 1, Ids = new[] {37, 38}}}, // 410: 南方への輸送作戦を成功させよ!
\r
230 {411, new QuestMission {Interval = Weekly, Max = 6, Shift = 1, Ids = new[] {37, 38}}}, // 411: 南方への鼠輸送を継続実施せよ!
\r
231 {424, new QuestMission {Interval = Monthly, Max = 4, Shift = 1, Ids = new[] {5}}}, // 424: 輸送船団護衛を強化せよ!
\r
233 {503, new QuestSpec {Interval = Daily, Max = 5}}, // 503: 艦隊大整備!
\r
234 {504, new QuestSpec {Interval = Daily, Max = 15}}, // 504: 艦隊酒保祭り!
\r
236 {605, new QuestSpec {Interval = Daily, Max = 1}}, // 605: 新装備「開発」指令
\r
237 {606, new QuestSpec {Interval = Daily, Max = 1}}, // 606: 新造艦「建造」指令
\r
238 {607, new QuestSpec {Interval = Daily, Max = 3, Shift = 1}}, // 607: 装備「開発」集中強化!
\r
239 {608, new QuestSpec {Interval = Daily, Max = 3, Shift = 1}}, // 608: 艦娘「建造」艦隊強化!
\r
240 {609, new QuestSpec {Interval = Daily, Max = 2}}, // 609: 軍縮条約対応!
\r
241 {619, new QuestSpec {Interval = Daily, Max = 1}}, // 619: 装備の改修強化
\r
243 {613, new QuestSpec {Interval = Weekly, Max = 24}}, // 613: 資源の再利用
\r
244 {638, new QuestDestroyItem {Interval = Weekly, Max = 6, Items = new[] {21}}}, // 638: 対空機銃量産
\r
245 {673, new QuestDestroyItem {Interval = Daily, Max = 4, Items = new[] {1}, Shift = 1}}, // 673: 装備開発力の整備
\r
246 {674, new QuestDestroyItem {Interval = Daily, Max = 3, Items = new[] {21}, Shift = 2}}, // 674: 工廠環境の整備
\r
247 {675, new QuestSpec {Interval = Quarterly, MaxArray = new[] {6, 4}}}, // 675: 運用装備の統合整備
\r
248 {676, new QuestSpec {Interval = Weekly, MaxArray = new[] {3, 3, 1}}}, // 676: 装備開発力の集中整備
\r
249 {677, new QuestSpec {Interval = Weekly, MaxArray = new[] {4, 2, 3}}}, // 677: 継戦支援能力の整備
\r
251 {702, new QuestPowerup {Interval = Daily, Max = 2}}, // 702: 艦の「近代化改修」を実施せよ!
\r
252 {703, new QuestPowerup {Interval = Weekly, Max = 15}} // 703: 「近代化改修」を進め、戦備を整えよ!
\r
256 private readonly Dictionary<int, QuestCount> _countDict = new Dictionary<int, QuestCount>();
\r
258 public QuestCount GetCount(int id)
\r
260 if (_countDict.TryGetValue(id, out var value))
\r
262 if (QuestSpecs.TryGetValue(id, out var spec))
\r
264 var nowArray = spec.MaxArray?.Select(x => 0).ToArray();
\r
265 return _countDict[id] = new QuestCount
\r
269 NowArray = nowArray,
\r
273 return new QuestCount {Spec = new QuestSpec {AdjustCount = false}};
\r
276 public void Remove(int id)
\r
278 _countDict.Remove(id);
\r
281 public void Remove(QuestInterval interval)
\r
284 _countDict.Where(pair => pair.Value.Spec.Interval == interval).Select(pair => pair.Key).ToArray())
\r
286 _countDict.Remove(id);
\r
290 public IEnumerable<QuestCount> CountList
\r
292 get => _countDict.Values.Where(c => c.Now > 0 || (c.NowArray?.Any(n => n > 0) ?? false));
\r
297 foreach (var count in value)
\r
299 count.Spec = QuestSpecs[count.Id];
\r
300 _countDict[count.Id] = count;
\r
306 public class QuestInfo : IHaveState
\r
308 private readonly SortedDictionary<int, QuestStatus> _quests = new SortedDictionary<int, QuestStatus>();
\r
309 private readonly QuestCountList _countList = new QuestCountList();
\r
310 private readonly ItemInfo _itemInfo;
\r
311 private readonly BattleInfo _battleInfo;
\r
312 private readonly Func<DateTime> _nowFunc = () => DateTime.Now;
\r
313 private DateTime _lastReset;
\r
315 private readonly Color[] _color =
\r
317 Color.FromArgb(60, 141, 76), Color.FromArgb(232, 57, 41), Color.FromArgb(136, 204, 120),
\r
318 Color.FromArgb(52, 147, 185), Color.FromArgb(220, 198, 126), Color.FromArgb(168, 111, 76),
\r
319 Color.FromArgb(200, 148, 231), Color.FromArgb(232, 57, 41)
\r
322 public int AcceptMax { get; set; } = 5;
\r
324 public QuestStatus[] Quests => _quests.Values.ToArray();
\r
326 public QuestInfo(ItemInfo itemInfo, BattleInfo battleInfo, Func<DateTime> nowFunc = null)
\r
328 _itemInfo = itemInfo;
\r
329 _battleInfo = battleInfo;
\r
330 if (nowFunc != null)
\r
331 _nowFunc = nowFunc;
\r
334 public void InspectQuestList(dynamic json)
\r
337 if (json.api_list == null)
\r
339 for (var i = 0; i < 2; i++)
\r
341 foreach (var entry in json.api_list)
\r
343 if (entry is double) // -1の場合がある。
\r
346 var id = (int)entry.api_no;
\r
347 var state = (int)entry.api_state;
\r
348 var progress = (int)entry.api_progress_flag;
\r
349 var cat = (int)entry.api_category;
\r
350 var name = (string)entry.api_title;
\r
351 var detail = ((string)entry.api_detail).Replace("<br>", "\r\n");
\r
367 if (_quests.Remove(id))
\r
374 AddQuest(id, cat, name, detail, progress, true);
\r
378 if (_quests.Count <= AcceptMax)
\r
381 * ほかのPCで任務を達成した場合、任務が消えずに受領した任務の数が_questCountを超えることがある。
\r
382 * その場合はいったん任務をクリアして、現在のページの任務だけを登録し直す。
\r
388 private void AddQuest(int id, int category, string name, string detail, int progress, bool adjustCount)
\r
390 var count = _countList.GetCount(id);
\r
393 count.AdjustCount(progress);
\r
396 _quests[id] = new QuestStatus
\r
399 Category = category,
\r
403 Progress = progress,
\r
404 Color = category <= _color.Length ? _color[category - 1] : Control.DefaultBackColor
\r
408 public void ClearQuests()
\r
413 private void ResetQuests()
\r
415 var now = _nowFunc();
\r
416 var daily = now.Date.AddHours(5);
\r
417 if (!(_lastReset < daily && daily <= now))
\r
419 _quests.Clear(); // 前日に未消化のデイリーを消す。
\r
420 _countList.Remove(QuestInterval.Daily);
\r
421 var weekly = now.Date.AddDays(-((6 + (int)now.DayOfWeek) % 7)).AddHours(5);
\r
422 if (_lastReset < weekly && weekly <= now)
\r
423 _countList.Remove(QuestInterval.Weekly);
\r
424 var monthly = new DateTime(now.Year, now.Month, 1, 5, 0, 0);
\r
425 if (_lastReset < monthly && monthly <= now)
\r
426 _countList.Remove(QuestInterval.Monthly);
\r
427 var season = now.Month / 3;
\r
428 var quarterly = new DateTime(now.Year - (season == 0 ? 1 : 0), (season == 0 ? 12 : season * 3), 1, 5, 0, 0);
\r
429 if (_lastReset < quarterly && quarterly <= now)
\r
430 _countList.Remove(QuestInterval.Quarterly);
\r
435 private bool _boss;
\r
438 public void InspectMapStart(dynamic json)
\r
440 if (_quests.TryGetValue(214, out var ago)) // あ号
\r
441 ago.Count.NowArray[0]++;
\r
442 InspectMapNext(json);
\r
445 public void InspectMapNext(dynamic json)
\r
447 _map = (int)json.api_maparea_id * 10 + (int)json.api_mapinfo_no;
\r
448 _boss = (int)json.api_event_id == 5;
\r
451 public void InspectBattleResult(dynamic json)
\r
453 var rank = json.api_win_rank;
\r
454 foreach (var quest in _quests.Values)
\r
456 var count = quest.Count;
\r
457 switch (count.Spec)
\r
459 case QuestSortie sortie:
\r
460 if (count.Id == 216 && !_boss || sortie.Check(rank, _map, _boss))
\r
461 IncrementCount(count);
\r
463 case QuestEnemyType enemyType:
\r
464 var num = enemyType.CountResult(
\r
465 _battleInfo.Result.Enemy.Main.Concat(_battleInfo.Result.Enemy.Guard));
\r
467 AddCount(count, num);
\r
471 if (_quests.TryGetValue(214, out var ago))
\r
473 var array = ago.Count.NowArray;
\r
477 if (QuestSortie.CompareRank(rank, "B") <= 0)
\r
487 if (_quests.TryGetValue(854, out var opz) && _boss)
\r
489 var array = opz.Count.NowArray;
\r
492 case 24 when QuestSortie.CompareRank(rank, "A") <= 0:
\r
496 case 61 when QuestSortie.CompareRank(rank, "A") <= 0:
\r
500 case 63 when QuestSortie.CompareRank(rank, "A") <= 0:
\r
504 case 64 when QuestSortie.CompareRank(rank, "S") <= 0:
\r
512 public void InspectPracticeResult(dynamic json)
\r
514 foreach (var quest in _quests.Values)
\r
516 var count = quest.Count;
\r
517 if (!(count.Spec is QuestPractice practice))
\r
519 if (practice.Check(json.api_win_rank))
\r
520 IncrementCount(count);
\r
524 private readonly int[] _missionId = new int[ShipInfo.FleetCount];
\r
526 public void InspectDeck(dynamic json)
\r
528 foreach (var entry in json)
\r
529 _missionId[(int)entry.api_id - 1] = (int)entry.api_mission[1];
\r
532 public void InspectMissionResult(string request, dynamic json)
\r
534 var values = HttpUtility.ParseQueryString(request);
\r
535 var deck = int.Parse(values["api_deck_id"]);
\r
536 if ((int)json.api_clear_result == 0)
\r
538 foreach (var quest in _quests.Values)
\r
540 var count = quest.Count;
\r
541 if (!(count.Spec is QuestMission mission))
\r
543 if (mission.Check(_missionId[deck - 1]))
\r
544 IncrementCount(count);
\r
548 private void IncrementCount(QuestCount count)
\r
554 private void AddCount(QuestCount count, int value)
\r
556 count.Now += value;
\r
560 private void IncrementCount(int id)
\r
565 private void AddCount(int id, int value)
\r
567 if (_quests.TryGetValue(id, out var quest))
\r
569 quest.Count.Now += value;
\r
574 public void CountNyukyo() => IncrementCount(503);
\r
576 public void CountCharge() => IncrementCount(504);
\r
578 public void CountCreateItem()
\r
580 IncrementCount(605);
\r
581 IncrementCount(607);
\r
584 public void CountCreateShip()
\r
586 IncrementCount(606);
\r
587 IncrementCount(608);
\r
590 public void InspectDestroyShip(string request)
\r
592 AddCount(609, HttpUtility.ParseQueryString(request)["api_ship_id"].Split(',').Length);
\r
595 public void CountRemodelSlot() => IncrementCount(619);
\r
597 public void InspectDestroyItem(string request, dynamic json)
\r
599 var values = HttpUtility.ParseQueryString(request);
\r
600 var items = values["api_slotitem_ids"].Split(',')
\r
601 .Select(id => _itemInfo.GetStatus(int.Parse(id)).Spec.Type).ToArray();
\r
602 IncrementCount(613); // 613: 資源の再利用
\r
603 foreach (var quest in _quests.Values)
\r
605 var count = quest.Count;
\r
606 if (!(count.Spec is QuestDestroyItem destroy))
\r
608 AddCount(count, items.Count(destroy.Check));
\r
610 if (_quests.TryGetValue(675, out var q675))
\r
612 q675.Count.NowArray[0] += items.Count(id => id == 6);
\r
613 q675.Count.NowArray[1] += items.Count(id => id == 21);
\r
616 if (_quests.TryGetValue(676, out var q676))
\r
618 q676.Count.NowArray[0] += items.Count(id => id == 2);
\r
619 q676.Count.NowArray[1] += items.Count(id => id == 4);
\r
620 q676.Count.NowArray[2] += items.Count(id => id == 30);
\r
623 if (_quests.TryGetValue(677, out var q677))
\r
625 q677.Count.NowArray[0] += items.Count(id => id == 3);
\r
626 q677.Count.NowArray[1] += items.Count(id => id == 10);
\r
627 q677.Count.NowArray[2] += items.Count(id => id == 5);
\r
632 public void InspectPowerup(dynamic json)
\r
634 if ((int)json.api_powerup_flag == 0)
\r
636 foreach (var quest in _quests.Values)
\r
638 var count = quest.Count;
\r
639 if (!(count.Spec is QuestPowerup))
\r
641 IncrementCount(count);
\r
645 public void InspectStop(string request)
\r
647 var values = HttpUtility.ParseQueryString(request);
\r
648 _quests.Remove(int.Parse(values["api_quest_id"]));
\r
652 public void InspectClearItemGet(string request)
\r
654 var values = HttpUtility.ParseQueryString(request);
\r
655 var id = int.Parse(values["api_quest_id"]);
\r
656 _countList.Remove(id);
\r
657 _quests.Remove(id);
\r
661 public bool NeedSave { get; private set; }
\r
663 public void SaveState(Status status)
\r
666 status.QuestLastReset = _lastReset;
\r
667 if (_quests != null)
\r
668 status.QuestList = _quests.Values.ToArray();
\r
669 if (_countList != null)
\r
670 status.QuestCountList = _countList.CountList.ToArray();
\r
673 public void LoadState(Status status)
\r
675 _lastReset = status.QuestLastReset;
\r
676 if (status.QuestCountList != null)
\r
677 _countList.CountList = status.QuestCountList;
\r
678 if (status.QuestList != null)
\r
681 foreach (var q in status.QuestList)
\r
682 AddQuest(q.Id, q.Category, q.Name, q.Detail, q.Progress, false);
\r