1 // Copyright (C) 2014, 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
18 using static System.Math;
\r
20 namespace KancolleSniffer
\r
22 public enum BattleResultRank
\r
33 public enum BattleState
\r
42 public class EnemyFighterPower
\r
44 public bool HasUnknown { get; set; }
\r
45 public string UnknownMark => HasUnknown ? "+" : "";
\r
46 public int AirCombat { get; set; }
\r
47 public int Interception { get; set; }
\r
50 public class BattleInfo
\r
52 private readonly ShipInfo _shipInfo;
\r
53 private readonly ItemInfo _itemInfo;
\r
55 private Record[] _friend;
\r
56 private Record[] _guard;
\r
57 private Record[] _enemy;
\r
58 private Record[] _enemyGuard;
\r
59 private readonly List<int> _escapingShips = new List<int>();
\r
60 private bool _lastCell;
\r
62 public BattleState BattleState { get; set; }
\r
63 public int[] Formation { get; private set; }
\r
64 public int[] FighterPower { get; private set; }
\r
65 public EnemyFighterPower EnemyFighterPower { get; private set; }
\r
66 public int AirControlLevel { get; private set; }
\r
67 public BattleResultRank ResultRank { get; private set; }
\r
68 public RankPair DisplayedResultRank { get; } = new RankPair();
\r
69 public BattleResult Result { get; set; }
\r
70 public bool EnemyIsCombined => _enemyGuard.Length > 0;
\r
71 public List<AirBattleResult> AirBattleResults { get; } = new List<AirBattleResult>();
\r
73 public class RankPair
\r
75 public char Assumed { get; set; }
\r
76 public char Actual { get; set; }
\r
77 public bool IsError => Assumed != Actual;
\r
80 public class BattleResult
\r
82 public class Combined
\r
84 public ShipStatus[] Main { get; set; }
\r
85 public ShipStatus[] Guard { get; set; }
\r
88 public Combined Friend { get; set; }
\r
89 public Combined Enemy { get; set; }
\r
92 public BattleInfo(ShipInfo shipInfo, ItemInfo itemInfo)
\r
94 _shipInfo = shipInfo;
\r
95 _itemInfo = itemInfo;
\r
98 public void InspectBattle(string url, string request, dynamic json)
\r
100 if (json.api_formation())
\r
101 Formation = ((dynamic[])json.api_formation).Select(f => f is string ? (int)int.Parse(f) : (int)f)
\r
103 AirControlLevel = CheckAirControlLevel(json);
\r
104 ShowResult(false); // 昼戦の結果を夜戦のときに表示する
\r
105 SetupResult(request, json, url.Contains("practice"));
\r
106 FighterPower = CalcFighterPower();
\r
107 EnemyFighterPower = CalcEnemyFighterPower(json);
\r
108 BattleState = IsNightBattle(json) ? BattleState.Night : BattleState.Day;
\r
110 ResultRank = url.EndsWith("ld_airbattle") ? CalcLdAirBattleRank() : CalcResultRank();
\r
114 private bool IsNightBattle(dynamic json) => json.api_hougeki();
\r
116 public static int DeckId(dynamic json)
\r
118 if (json.api_dock_id()) // 昼戦はtypoしている
\r
119 return (int)json.api_dock_id - 1;
\r
120 if (json.api_deck_id is string) // 通常の夜戦と連合艦隊(味方のみ)では文字列
\r
121 return int.Parse(json.api_deck_id) - 1;
\r
122 return (int)json.api_deck_id - 1;
\r
125 private void SetupResult(string request, dynamic json, bool practice)
\r
127 if (_friend != null)
\r
129 _shipInfo.SaveBattleStartStatus();
\r
130 _fleet = DeckId(json);
\r
131 var fstats = _shipInfo.GetShipStatuses(_fleet);
\r
132 FlagshipRecovery(request, fstats[0]);
\r
133 _friend = Record.Setup(fstats, practice);
\r
134 _guard = json.api_f_nowhps_combined()
\r
135 ? Record.Setup(_shipInfo.GetShipStatuses(1), practice)
\r
137 _enemy = Record.Setup((int[])json.api_e_nowhps,
\r
138 ((int[])json.api_ship_ke).Select(_shipInfo.GetSpec).ToArray(),
\r
139 ((int[][])json.api_eSlot).Select(slot => slot.Select(_itemInfo.GetSpecByItemId).ToArray()).ToArray(),
\r
141 _enemyGuard = json.api_ship_ke_combined()
\r
142 ? Record.Setup((int[])json.api_e_nowhps_combined,
\r
143 ((int[])json.api_ship_ke_combined).Select(_shipInfo.GetSpec).ToArray(),
\r
144 ((int[][])json.api_eSlot).Select(slot => slot.Select(_itemInfo.GetSpecByItemId).ToArray())
\r
145 .ToArray(), practice)
\r
149 private void SetResult()
\r
151 Result = new BattleResult
\r
153 Friend = new BattleResult.Combined
\r
155 Main = _friend.Select(r => r.SnapShot).ToArray(),
\r
156 Guard = _guard.Select(r => r.SnapShot).ToArray()
\r
158 Enemy = new BattleResult.Combined
\r
160 Main = _enemy.Select(r => r.SnapShot).ToArray(),
\r
161 Guard = _enemyGuard.Select(r => r.SnapShot).ToArray()
\r
166 private void FlagshipRecovery(string request, ShipStatus flagship)
\r
168 var type = int.Parse(HttpUtility.ParseQueryString(request)["api_recovery_type"] ?? "0");
\r
174 flagship.NowHp = flagship.MaxHp / 2;
\r
175 ConsumeSlotItem(flagship, 42); // ダメコン
\r
178 flagship.NowHp = flagship.MaxHp;
\r
179 ConsumeSlotItem(flagship, 43); // 女神
\r
183 _shipInfo.SetBadlyDamagedShips();
\r
186 private static void ConsumeSlotItem(ShipStatus ship, int id)
\r
188 if (ship.SlotEx.Spec.Id == id)
\r
190 ship.SlotEx = new ItemStatus();
\r
193 for (var i = 0; i < ship.Slot.Length; i++)
\r
195 if (ship.Slot[i].Spec.Id == id)
\r
197 ship.Slot[i] = new ItemStatus();
\r
203 public void CleanupResult()
\r
209 private int CheckAirControlLevel(dynamic json)
\r
211 if (!json.api_kouku())
\r
213 var stage1 = json.api_kouku.api_stage1;
\r
214 if (stage1 == null)
\r
216 if (stage1.api_f_count == 0 && stage1.api_e_count == 0)
\r
218 return (int)stage1.api_disp_seiku;
\r
221 private int[] CalcFighterPower()
\r
223 if (_guard.Length > 0 && _enemyGuard.Length > 0)
\r
224 return _shipInfo.GetFighterPower(0).Zip(_shipInfo.GetFighterPower(1), (a, b) => a + b).ToArray();
\r
225 return _shipInfo.GetFighterPower(_fleet);
\r
228 private EnemyFighterPower CalcEnemyFighterPower(dynamic json)
\r
230 var result = new EnemyFighterPower();
\r
231 var ships = (int[])json.api_ship_ke;
\r
232 if (json.api_ship_ke_combined() && _guard.Length > 0)
\r
233 ships = ships.Concat((int[])json.api_ship_ke_combined).ToArray();
\r
234 var maxEq = ships.SelectMany(id =>
\r
236 var r = _shipInfo.GetSpec(id).MaxEq;
\r
239 result.HasUnknown = true;
\r
242 var equips = ((int[][])json.api_eSlot).SelectMany(x => x);
\r
243 if (json.api_eSlot_combined() && _guard.Length > 0)
\r
244 equips = equips.Concat(((int[][])json.api_eSlot_combined).SelectMany(x => x));
\r
245 foreach (var entry in from slot in equips.Zip(maxEq, (id, max) => new {id, max})
\r
246 let spec = _itemInfo.GetSpecByItemId(slot.id)
\r
247 let perSlot = (int)Floor(spec.AntiAir * Sqrt(slot.max))
\r
248 select new {spec, perSlot})
\r
250 if (entry.spec.CanAirCombat)
\r
251 result.AirCombat += entry.perSlot;
\r
252 if (entry.spec.IsAircraft)
\r
253 result.Interception += entry.perSlot;
\r
258 private enum CombatType
\r
267 private class Phase
\r
269 public string Api { get; }
\r
270 public CombatType Type { get; }
\r
271 public string Name { get; }
\r
273 public Phase(string api, CombatType type, string name = "")
\r
281 private void CalcDamage(dynamic json)
\r
283 AirBattleResults.Clear();
\r
286 new Phase("air_base_injection", CombatType.Aircraft, "AB噴式"),
\r
287 new Phase("injection_kouku", CombatType.Aircraft, "噴式"),
\r
288 new Phase("air_base_attack", CombatType.AirBase),
\r
289 new Phase("n_support_info", CombatType.Support),
\r
290 new Phase("n_hougeki1", CombatType.ByTurn),
\r
291 new Phase("n_hougeki2", CombatType.ByTurn),
\r
292 new Phase("kouku", CombatType.Aircraft, "航空戦"),
\r
293 new Phase("kouku2", CombatType.Aircraft, "航空戦2"),
\r
294 new Phase("support_info", CombatType.Support),
\r
295 new Phase("opening_taisen", CombatType.ByTurn),
\r
296 new Phase("opening_atack", CombatType.AtOnce),
\r
297 new Phase("hougeki", CombatType.ByTurn),
\r
298 new Phase("hougeki1", CombatType.ByTurn),
\r
299 new Phase("hougeki2", CombatType.ByTurn),
\r
300 new Phase("hougeki3", CombatType.ByTurn),
\r
301 new Phase("raigeki", CombatType.AtOnce)
\r
303 foreach (var phase in phases)
\r
304 CalcDamageByType(json, phase);
\r
307 private void CalcDamageByType(dynamic json, Phase phase)
\r
309 var api = "api_" + phase.Api;
\r
310 if (!json.IsDefined(api) || json[api] == null)
\r
312 switch (phase.Type)
\r
314 case CombatType.AtOnce:
\r
315 CalcDamageAtOnce(json[api]);
\r
317 case CombatType.ByTurn:
\r
318 CalcDamageByTurn(json[api]);
\r
320 case CombatType.Support:
\r
321 CalcSupportDamage(json[api]);
\r
323 case CombatType.Aircraft:
\r
324 AddAirBattleResult(json[api], phase.Name);
\r
325 CalcKoukuDamage(json[api]);
\r
327 case CombatType.AirBase:
\r
328 CalcAirBaseAttackDamage(json[api]);
\r
333 private void CalcSupportDamage(dynamic json)
\r
335 if (json.api_support_hourai != null)
\r
337 CalcRawDamageAtOnce(json.api_support_hourai.api_damage, _enemy, _enemyGuard);
\r
339 else if (json.api_support_airatack != null)
\r
341 CalcRawDamageAtOnce(json.api_support_airatack.api_stage3.api_edam, _enemy, _enemyGuard);
\r
345 private void CalcAirBaseAttackDamage(dynamic json)
\r
348 foreach (var entry in json)
\r
350 AddAirBattleResult(entry, "基地" + i++);
\r
351 CalcKoukuDamage(entry);
\r
355 private void AddAirBattleResult(dynamic json, string phaseName)
\r
357 var stage1 = json.api_stage1;
\r
358 if (stage1 == null || (stage1.api_f_count == 0 && stage1.api_e_count == 0))
\r
360 AirBattleResults.Add(new AirBattleResult
\r
362 PhaseName = phaseName,
\r
363 AirControlLevel = json.api_stage1.api_disp_seiku() ? (int)json.api_stage1.api_disp_seiku : 0,
\r
364 Stage1 = new AirBattleResult.StageResult
\r
366 FriendCount = (int)json.api_stage1.api_f_count,
\r
367 FriendLost = (int)json.api_stage1.api_f_lostcount,
\r
368 EnemyCount = (int)json.api_stage1.api_e_count,
\r
369 EnemyLost = (int)json.api_stage1.api_e_lostcount
\r
371 Stage2 = json.api_stage2 == null
\r
372 ? new AirBattleResult.StageResult
\r
379 : new AirBattleResult.StageResult
\r
381 FriendCount = (int)json.api_stage2.api_f_count,
\r
382 FriendLost = (int)json.api_stage2.api_f_lostcount,
\r
383 EnemyCount = (int)json.api_stage2.api_e_count,
\r
384 EnemyLost = (int)json.api_stage2.api_e_lostcount
\r
389 private void CalcKoukuDamage(dynamic json)
\r
391 if (json.api_stage3() && json.api_stage3 != null)
\r
392 CalcDamageAtOnce(json.api_stage3, _friend, _enemy);
\r
393 if (json.api_stage3_combined() && json.api_stage3_combined != null)
\r
394 CalcDamageAtOnce(json.api_stage3_combined, _guard, _enemyGuard);
\r
397 private void CalcDamageAtOnce(dynamic json)
\r
399 CalcDamageAtOnce(json, _friend, _guard, _enemy, _enemyGuard);
\r
402 private void CalcDamageAtOnce(dynamic json, Record[] friend, Record[] enemy)
\r
404 CalcDamageAtOnce(json, friend, null, enemy, null);
\r
407 private void CalcDamageAtOnce(dynamic json,
\r
408 Record[] friend, Record[] guard, Record[] enemy, Record[] enemyGuard)
\r
410 if (json.api_fdam() && json.api_fdam != null)
\r
411 CalcRawDamageAtOnce(json.api_fdam, friend, guard);
\r
412 if (json.api_edam() && json.api_edam != null)
\r
413 CalcRawDamageAtOnce(json.api_edam, enemy, enemyGuard);
\r
416 private void CalcRawDamageAtOnce(dynamic rawDamage, Record[] friend, Record[] guard = null)
\r
418 var damage = (int[])rawDamage;
\r
419 for (var i = 0; i < friend.Length; i++)
\r
420 friend[i].ApplyDamage(damage[i]);
\r
423 for (var i = 0; i < guard.Length; i++)
\r
424 guard[i].ApplyDamage(damage[i + 6]);
\r
427 private void CalcDamageByTurn(dynamic json)
\r
429 if (!(json.api_df_list() && json.api_df_list != null &&
\r
430 json.api_damage() && json.api_damage != null &&
\r
431 json.api_at_eflag() && json.api_at_eflag != null))
\r
434 var targets = (int[][])json.api_df_list;
\r
435 var damages = (int[][])json.api_damage;
\r
436 var eflags = (int[])json.api_at_eflag;
\r
437 var records = new[] {new Record[12], new Record[12]};
\r
438 Array.Copy(_friend, records[1], _friend.Length);
\r
439 Array.Copy(_guard, 0, records[1], 6, _guard.Length);
\r
440 Array.Copy(_enemy, records[0], _enemy.Length);
\r
441 Array.Copy(_enemyGuard, 0, records[0], 6, _enemyGuard.Length);
\r
442 for (var i = 0; i < eflags.Length; i++)
\r
444 // 一度に複数の目標を狙う攻撃はないものと仮定する
\r
445 var hit = new {t = targets[i][0], d = damages[i].Sum(d => d >= 0 ? d : 0)};
\r
448 records[eflags[i]][hit.t].ApplyDamage(hit.d);
\r
452 public void InspectMapStart(dynamic json)
\r
454 InspectMapNext(json);
\r
457 public void InspectMapNext(dynamic json)
\r
459 _lastCell = (int)json.api_next == 0;
\r
462 public void InspectBattleResult(dynamic json)
\r
464 BattleState = BattleState.Result;
\r
465 ShowResult(!_lastCell);
\r
466 _shipInfo.SaveBattleResult();
\r
467 VerifyResultRank(json);
\r
469 SetEscapeShips(json);
\r
472 private void VerifyResultRank(dynamic json)
\r
474 if (_friend == null)
\r
476 if (!json.api_win_rank())
\r
478 var assumed = "PSABCDE"[(int)ResultRank];
\r
479 if (assumed == 'P')
\r
481 var actual = ((string)json.api_win_rank)[0];
\r
482 DisplayedResultRank.Assumed = assumed;
\r
483 DisplayedResultRank.Actual = actual;
\r
486 public void InspectPracticeResult(dynamic json)
\r
488 BattleState = BattleState.Result;
\r
490 VerifyResultRank(json);
\r
494 private void ShowResult(bool warnDamagedShip = true)
\r
496 if (_friend == null)
\r
498 var ships = _guard.Length > 0
\r
499 ? _shipInfo.GetShipStatuses(0).Concat(_shipInfo.GetShipStatuses(1)).ToArray()
\r
500 : _shipInfo.GetShipStatuses(_fleet);
\r
501 foreach (var entry in ships.Zip(_friend.Concat(_guard), (ship, now) => new {ship, now}))
\r
502 entry.now.UpdateShipStatus(entry.ship);
\r
503 if (warnDamagedShip)
\r
504 _shipInfo.SetBadlyDamagedShips();
\r
506 _shipInfo.ClearBadlyDamagedShips();
\r
509 public void SetEscapeShips(dynamic json)
\r
511 _escapingShips.Clear();
\r
512 if (!json.api_escape_flag() || (int)json.api_escape_flag == 0)
\r
514 var damaged = (int)json.api_escape.api_escape_idx[0] - 1;
\r
515 if (json.api_escape.api_tow_idx())
\r
517 _escapingShips.Add(_shipInfo.GetDeck(damaged / 6)[damaged % 6]);
\r
518 var escort = (int)json.api_escape.api_tow_idx[0] - 1;
\r
519 _escapingShips.Add(_shipInfo.GetDeck(escort / 6)[escort % 6]);
\r
523 _escapingShips.Add(_shipInfo.GetDeck(2)[damaged]);
\r
527 public void CauseEscape()
\r
529 _shipInfo.SetEscapedShips(_escapingShips);
\r
530 _shipInfo.SetBadlyDamagedShips();
\r
533 private class Record
\r
535 private ShipStatus _status;
\r
536 private bool _practice;
\r
537 public ShipStatus SnapShot => (ShipStatus)_status.Clone();
\r
538 public int NowHp => _status.NowHp;
\r
539 public bool Escaped => _status.Escaped;
\r
540 public ShipStatus.Damage DamageLevel => _status.DamageLevel;
\r
541 public int StartHp;
\r
543 public static Record[] Setup(ShipStatus[] ships, bool practice) =>
\r
545 select new Record {_status = (ShipStatus)s.Clone(), _practice = practice, StartHp = s.NowHp}).ToArray();
\r
547 public static Record[] Setup(int[] nowhps, ShipSpec[] ships, ItemSpec[][] slots, bool practice)
\r
549 return Enumerable.Range(0, nowhps.Length).Select(i =>
\r
552 StartHp = nowhps[i],
\r
553 _status = new ShipStatus
\r
559 Slot = slots[i].Select(spec => new ItemStatus {Id = spec.Id, Spec = spec}).ToArray(),
\r
560 SlotEx = new ItemStatus(0)
\r
562 _practice = practice
\r
566 public void ApplyDamage(int damage)
\r
568 if (_status.NowHp > damage)
\r
570 _status.NowHp -= damage;
\r
576 foreach (var item in _status.AllSlot)
\r
578 if (item.Spec.Id == 42)
\r
580 _status.NowHp = (int)(_status.MaxHp * 0.2);
\r
581 ConsumeSlotItem(_status, 42);
\r
584 if (item.Spec.Id == 43)
\r
586 _status.NowHp = _status.MaxHp;
\r
587 ConsumeSlotItem(_status, 43);
\r
593 public void UpdateShipStatus(ShipStatus ship)
\r
595 ship.NowHp = NowHp;
\r
596 ship.Slot = _status.Slot;
\r
597 ship.SlotEx = _status.SlotEx;
\r
601 private BattleResultRank CalcLdAirBattleRank()
\r
603 var combined = _friend.Concat(_guard).ToArray();
\r
604 var friendNowShips = combined.Count(r => r.NowHp > 0);
\r
605 var friendGauge = combined.Sum(r => r.StartHp - r.NowHp);
\r
606 var friendSunk = combined.Count(r => r.NowHp == 0);
\r
607 var friendGaugeRate = Floor((double)friendGauge / combined.Sum(r => r.StartHp) * 100);
\r
609 if (friendSunk == 0)
\r
611 if (friendGauge == 0)
\r
612 return BattleResultRank.P;
\r
613 if (friendGaugeRate < 10)
\r
614 return BattleResultRank.A;
\r
615 if (friendGaugeRate < 20)
\r
616 return BattleResultRank.B;
\r
617 if (friendGaugeRate < 50)
\r
618 return BattleResultRank.C;
\r
619 return BattleResultRank.D;
\r
621 if (friendSunk < friendNowShips)
\r
622 return BattleResultRank.D;
\r
623 return BattleResultRank.E;
\r
626 private BattleResultRank CalcResultRank()
\r
628 var friend = _friend.Concat(_guard).ToArray();
\r
629 var enemy = _enemy.Concat(_enemyGuard).ToArray();
\r
631 var friendCount = friend.Length;
\r
632 var friendStartHpTotal = 0;
\r
633 var friendNowHpTotal = 0;
\r
634 var friendSunk = 0;
\r
635 foreach (var ship in friend)
\r
639 friendStartHpTotal += ship.StartHp;
\r
640 friendNowHpTotal += ship.NowHp;
\r
641 if (ship.NowHp == 0)
\r
644 var friendGaugeRate = (int)((double)(friendStartHpTotal - friendNowHpTotal) / friendStartHpTotal * 100);
\r
646 var enemyCount = enemy.Length;
\r
647 var enemyStartHpTotal = enemy.Sum(r => r.StartHp);
\r
648 var enemyNowHpTotal = enemy.Sum(r => r.NowHp);
\r
649 var enemySunk = enemy.Count(r => r.NowHp == 0);
\r
650 var enemyGaugeRate = (int)((double)(enemyStartHpTotal - enemyNowHpTotal) / enemyStartHpTotal * 100);
\r
652 if (friendSunk == 0 && enemySunk == enemyCount)
\r
654 if (friendNowHpTotal >= friendStartHpTotal)
\r
655 return BattleResultRank.P;
\r
656 return BattleResultRank.S;
\r
658 if (friendSunk == 0 && enemySunk >= (int)(enemyCount * 0.7) && enemyCount > 1)
\r
659 return BattleResultRank.A;
\r
660 if (friendSunk < enemySunk && enemy[0].NowHp == 0)
\r
661 return BattleResultRank.B;
\r
662 if (friendCount == 1 && friend[0].DamageLevel == ShipStatus.Damage.Badly)
\r
663 return BattleResultRank.D;
\r
664 if (enemyGaugeRate > friendGaugeRate * 2.5)
\r
665 return BattleResultRank.B;
\r
666 if (enemyGaugeRate > friendGaugeRate * 0.9)
\r
667 return BattleResultRank.C;
\r
668 if (friendCount > 1 && friendCount - 1 == friendSunk)
\r
669 return BattleResultRank.E;
\r
670 return BattleResultRank.D;
\r
676 public void InjectEnemyResultStatus(ShipStatus[] enemy, ShipStatus[] guard)
\r
678 Result = new BattleResult {Enemy = new BattleResult.Combined {Main = enemy, Guard = guard}};
\r