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
15 using System.Collections.Generic;
\r
17 using static System.Math;
\r
19 namespace KancolleSniffer
\r
21 public enum BattleResultRank
\r
32 public enum BattleState
\r
41 public class EnemyFighterPower
\r
43 public bool HasUnknown { get; set; }
\r
44 public string UnknownMark => HasUnknown ? "+" : "";
\r
45 public int AirCombat { get; set; }
\r
46 public int Interception { get; set; }
\r
49 public class BattleInfo
\r
51 private readonly ShipInfo _shipInfo;
\r
52 private readonly ItemInfo _itemInfo;
\r
54 private Record[] _friend;
\r
55 private Record[] _guard;
\r
56 private int[] _enemyHp;
\r
57 private int[] _enemyGuardHp;
\r
58 private int[] _enemyStartHp;
\r
59 private int[] _enemyGuardStartHp;
\r
60 private readonly List<int> _escapingShips = new List<int>();
\r
61 private bool _lastCell;
\r
63 public BattleState BattleState { get; set; }
\r
64 public string Formation { 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 ShipStatus[] EnemyResultStatus { get; private set; }
\r
70 public ShipStatus[] EnemyGuardResultStatus { get; private set; }
\r
71 public bool EnemyIsCombined => EnemyGuardResultStatus.Length > 0;
\r
72 public List<AirBattleResult> AirBattleResults { get; } = new List<AirBattleResult>();
\r
74 public class RankPair
\r
76 public char Assumed { get; set; }
\r
77 public char Actual { get; set; }
\r
78 public bool IsError => Assumed != Actual;
\r
81 public BattleInfo(ShipInfo shipInfo, ItemInfo itemInfo)
\r
83 _shipInfo = shipInfo;
\r
84 _itemInfo = itemInfo;
\r
87 public void InspectBattle(string url, string request, dynamic json)
\r
89 Formation = FormationName(json);
\r
90 AirControlLevel = CheckAirControlLevel(json);
\r
91 ShowResult(false); // 昼戦の結果を夜戦のときに表示する
\r
92 SetupResult(request, json);
\r
93 EnemyFighterPower = CalcEnemyFighterPower(json);
\r
94 BattleState = IsNightBattle(json) ? BattleState.Night : BattleState.Day;
\r
96 ClearEnemyOverKill();
\r
97 ResultRank = url.EndsWith("ld_airbattle") ? CalcLdAirBattleRank() : CalcResultRank();
\r
100 private void ClearEnemyOverKill()
\r
102 _enemyHp = _enemyHp.Select(hp => hp < 0 ? 0 : hp).ToArray();
\r
103 _enemyGuardHp = _enemyGuardHp.Select(hp => hp < 0 ? 0 : hp).ToArray();
\r
106 private bool IsNightBattle(dynamic json) => json.api_hougeki();
\r
108 public static int DeckId(dynamic json)
\r
110 if (json.api_dock_id()) // 昼戦はtypoしている
\r
111 return (int)json.api_dock_id - 1;
\r
112 if (json.api_deck_id is string) // 通常の夜戦と連合艦隊(味方のみ)では文字列
\r
113 return int.Parse(json.api_deck_id) - 1;
\r
114 return (int)json.api_deck_id - 1;
\r
117 private string FormationName(dynamic json)
\r
119 if (!json.api_formation()) // 演習の夜戦
\r
121 switch ((int)json.api_formation[2])
\r
135 private void SetupResult(string request, dynamic json)
\r
137 if (_friend != null)
\r
139 _shipInfo.SaveBattleStartStatus();
\r
140 _fleet = DeckId(json);
\r
141 var fstats = _shipInfo.GetShipStatuses(_fleet);
\r
142 FlagshipRecovery(request, fstats[0]);
\r
143 _friend = Record.Setup(fstats);
\r
144 _enemyHp = (int[])json.api_e_nowhps;
\r
145 _enemyStartHp = (int[])_enemyHp.Clone();
\r
146 EnemyResultStatus = ((int[])json.api_ship_ke)
\r
147 .Select(id => new ShipStatus {Id = id, Spec = _shipInfo.GetSpec(id)}).ToArray();
\r
148 EnemyGuardResultStatus = new ShipStatus[0];
\r
149 if (json.api_ship_ke_combined())
\r
151 EnemyGuardResultStatus = ((int[])json.api_ship_ke_combined)
\r
152 .Select(id => new ShipStatus {Id = id, Spec = _shipInfo.GetSpec(id)}).ToArray();
\r
154 _guard = new Record[0];
\r
155 _enemyGuardHp = new int[0];
\r
156 _enemyGuardStartHp = new int[0];
\r
157 if (json.api_f_nowhps_combined())
\r
158 _guard = Record.Setup(_shipInfo.GetShipStatuses(1));
\r
159 if (json.api_e_nowhps_combined()) // 敵が連合艦隊
\r
161 _enemyGuardHp = (int[])json.api_e_nowhps_combined;
\r
162 _enemyGuardStartHp = (int[])_enemyGuardHp.Clone();
\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 EnemyFighterPower CalcEnemyFighterPower(dynamic json)
\r
223 var result = new EnemyFighterPower();
\r
224 var ships = (int[])json.api_ship_ke;
\r
225 if (json.api_ship_ke_combined() && _guard.Length > 0)
\r
226 ships = ships.Concat((int[])json.api_ship_ke_combined).ToArray();
\r
227 var maxEq = ships.SelectMany(id =>
\r
229 var r = _shipInfo.GetSpec(id).MaxEq;
\r
232 result.HasUnknown = true;
\r
235 var equips = ((int[][])json.api_eSlot).SelectMany(x => x);
\r
236 if (json.api_eSlot_combined() && _guard.Length > 0)
\r
237 equips = equips.Concat(((int[][])json.api_eSlot_combined).SelectMany(x => x));
\r
238 foreach (var entry in from slot in equips.Zip(maxEq, (id, max) => new {id, max})
\r
239 let spec = _itemInfo.GetSpecByItemId(slot.id)
\r
240 let perSlot = (int)Floor(spec.AntiAir * Sqrt(slot.max))
\r
241 select new {spec, perSlot})
\r
243 if (entry.spec.CanAirCombat)
\r
244 result.AirCombat += entry.perSlot;
\r
245 if (entry.spec.IsAircraft)
\r
246 result.Interception += entry.perSlot;
\r
251 private enum CombatType
\r
260 private class Phase
\r
262 public string Api { get; }
\r
263 public CombatType Type { get; }
\r
264 public string Name { get; }
\r
266 public Phase(string api, CombatType type, string name = "")
\r
274 private void CalcDamage(dynamic json)
\r
276 AirBattleResults.Clear();
\r
279 new Phase("air_base_injection", CombatType.Aircraft, "AB噴式"),
\r
280 new Phase("injection_kouku", CombatType.Aircraft, "噴式"),
\r
281 new Phase("air_base_attack", CombatType.AirBase),
\r
282 new Phase("n_support_info", CombatType.Support),
\r
283 new Phase("n_hougeki1", CombatType.ByTurn),
\r
284 new Phase("n_hougeki2", CombatType.ByTurn),
\r
285 new Phase("kouku", CombatType.Aircraft, "航空戦"),
\r
286 new Phase("kouku2", CombatType.Aircraft, "航空戦2"),
\r
287 new Phase("support_info", CombatType.Support),
\r
288 new Phase("opening_taisen", CombatType.ByTurn),
\r
289 new Phase("opening_atack", CombatType.AtOnce),
\r
290 new Phase("hougeki", CombatType.ByTurn),
\r
291 new Phase("hougeki1", CombatType.ByTurn),
\r
292 new Phase("hougeki2", CombatType.ByTurn),
\r
293 new Phase("hougeki3", CombatType.ByTurn),
\r
294 new Phase("raigeki", CombatType.AtOnce)
\r
296 foreach (var phase in phases)
\r
297 CalcDamageByType(json, phase);
\r
300 private void CalcDamageByType(dynamic json, Phase phase)
\r
302 var api = "api_" + phase.Api;
\r
303 if (!json.IsDefined(api) || json[api] == null)
\r
305 switch (phase.Type)
\r
307 case CombatType.AtOnce:
\r
308 CalcDamageAtOnce(json[api]);
\r
310 case CombatType.ByTurn:
\r
311 CalcDamageByTurn(json[api]);
\r
313 case CombatType.Support:
\r
314 CalcSupportDamage(json[api]);
\r
316 case CombatType.Aircraft:
\r
317 AddAirBattleResult(json[api], phase.Name);
\r
318 CalcKoukuDamage(json[api]);
\r
320 case CombatType.AirBase:
\r
321 CalcAirBaseAttackDamage(json[api]);
\r
326 private void CalcSupportDamage(dynamic json)
\r
328 if (json.api_support_hourai != null)
\r
330 CalcDamageAtOnce(json.api_support_hourai.api_damage, _enemyHp, _enemyGuardHp);
\r
332 else if (json.api_support_airatack != null)
\r
334 CalcDamageAtOnce(json.api_support_airatack.api_stage3.api_edam, _enemyHp, _enemyGuardHp);
\r
338 private void CalcAirBaseAttackDamage(dynamic json)
\r
341 foreach (var entry in json)
\r
343 AddAirBattleResult(entry, "基地" + i++);
\r
344 CalcKoukuDamage(entry);
\r
348 private void AddAirBattleResult(dynamic json, string phaseName)
\r
350 var stage1 = json.api_stage1;
\r
351 if (stage1 == null || (stage1.api_f_count == 0 && stage1.api_e_count == 0))
\r
353 AirBattleResults.Add(new AirBattleResult
\r
355 PhaseName = phaseName,
\r
356 AirControlLevel = json.api_stage1.api_disp_seiku() ? (int)json.api_stage1.api_disp_seiku : 0,
\r
357 Stage1 = new AirBattleResult.StageResult
\r
359 FriendCount = (int)json.api_stage1.api_f_count,
\r
360 FriendLost = (int)json.api_stage1.api_f_lostcount,
\r
361 EnemyCount = (int)json.api_stage1.api_e_count,
\r
362 EnemyLost = (int)json.api_stage1.api_e_lostcount
\r
364 Stage2 = json.api_stage2 == null
\r
365 ? new AirBattleResult.StageResult
\r
372 : new AirBattleResult.StageResult
\r
374 FriendCount = (int)json.api_stage2.api_f_count,
\r
375 FriendLost = (int)json.api_stage2.api_f_lostcount,
\r
376 EnemyCount = (int)json.api_stage2.api_e_count,
\r
377 EnemyLost = (int)json.api_stage2.api_e_lostcount
\r
382 private void CalcKoukuDamage(dynamic json)
\r
384 if (json.api_stage3() && json.api_stage3 != null)
\r
385 CalcDamageAtOnce(json.api_stage3, _friend, _enemyHp);
\r
386 if (json.api_stage3_combined() && json.api_stage3_combined != null)
\r
387 CalcDamageAtOnce(json.api_stage3_combined, _guard, _enemyGuardHp);
\r
390 private void CalcDamageAtOnce(dynamic json)
\r
392 CalcDamageAtOnce(json, _friend, _guard, _enemyHp, _enemyGuardHp);
\r
395 private void CalcDamageAtOnce(dynamic json, Record[] friend, int[] enemy)
\r
397 CalcDamageAtOnce(json, friend, null, enemy, null);
\r
400 private void CalcDamageAtOnce(dynamic json, Record[] friend, Record[] guard, int[] enemy, int[] enemyGuard)
\r
402 if (json.api_fdam() && json.api_fdam != null)
\r
403 CalcDamageAtOnce(json.api_fdam, friend, guard);
\r
404 if (json.api_edam() && json.api_edam != null)
\r
405 CalcDamageAtOnce(json.api_edam, enemy, enemyGuard);
\r
408 private void CalcDamageAtOnce(dynamic rawDamage, Record[] friend, Record[] guard = null)
\r
410 var damage = (int[])rawDamage;
\r
411 for (var i = 0; i < friend.Length; i++)
\r
412 friend[i].ApplyDamage(damage[i]);
\r
415 for (var i = 0; i < guard.Length; i++)
\r
416 guard[i].ApplyDamage(damage[i + 6]);
\r
419 private void CalcDamageAtOnce(dynamic rawDamage, int[] enemy, int[] enemyGuard = null)
\r
421 var damage = (int[])rawDamage;
\r
422 for (var i = 0; i < enemy.Length; i++)
\r
423 enemy[i] -= damage[i];
\r
424 if (enemyGuard == null)
\r
426 for (var i = 0; i < enemyGuard.Length; i++)
\r
427 enemyGuard[i] -= damage[i + 6];
\r
430 private void CalcDamageByTurn(dynamic json)
\r
432 if (!(json.api_df_list() && json.api_df_list != null &&
\r
433 json.api_damage() && json.api_damage != null &&
\r
434 json.api_at_eflag() && json.api_at_eflag != null))
\r
437 var targets = (int[][])json.api_df_list;
\r
438 var damages = (int[][])json.api_damage;
\r
439 var eflags = (int[])json.api_at_eflag;
\r
441 for (var i = 0; i < eflags.Length; i++)
\r
443 // 一度に複数の目標を狙う攻撃はないものと仮定する
\r
444 var hit = new {t = targets[i][0], d = damages[i].Sum(d => d >= 0 ? d : 0)};
\r
447 if (eflags[i] == 1)
\r
449 if (hit.t < _friend.Length)
\r
451 _friend[hit.t].ApplyDamage(hit.d);
\r
455 _guard[hit.t - 6].ApplyDamage(hit.d);
\r
460 if (hit.t < _enemyHp.Length)
\r
462 _enemyHp[hit.t] -= hit.d;
\r
466 _enemyGuardHp[hit.t - 6] -= hit.d;
\r
472 public void InspectMapStart(dynamic json)
\r
474 InspectMapNext(json);
\r
477 public void InspectMapNext(dynamic json)
\r
479 _lastCell = (int)json.api_next == 0;
\r
482 public void InspectBattleResult(dynamic json)
\r
484 BattleState = BattleState.Result;
\r
485 ShowResult(!_lastCell);
\r
486 _shipInfo.SaveBattleResult();
\r
487 VerifyResultRank(json);
\r
489 SetEscapeShips(json);
\r
493 private void VerifyResultRank(dynamic json)
\r
495 if (_friend == null)
\r
497 if (!json.api_win_rank())
\r
499 var assumed = "PSABCDE"[(int)ResultRank];
\r
500 if (assumed == 'P')
\r
502 var actual = ((string)json.api_win_rank)[0];
\r
503 DisplayedResultRank.Assumed = assumed;
\r
504 DisplayedResultRank.Actual = actual;
\r
507 public void InspectPracticeResult(dynamic json)
\r
509 BattleState = BattleState.Result;
\r
514 private void ShowResult(bool warnDamagedShip = true)
\r
516 if (_friend == null)
\r
518 var ships = _guard.Length > 0
\r
519 ? _shipInfo.GetShipStatuses(0).Concat(_shipInfo.GetShipStatuses(1)).ToArray()
\r
520 : _shipInfo.GetShipStatuses(_fleet);
\r
521 foreach (var entry in ships.Zip(_friend.Concat(_guard), (ship, now) => new {ship, now}))
\r
522 entry.now.UpdateShipStatus(entry.ship);
\r
523 if (warnDamagedShip)
\r
524 _shipInfo.SetBadlyDamagedShips();
\r
526 _shipInfo.ClearBadlyDamagedShips();
\r
527 SetEnemyResultStatus();
\r
530 private void SetEnemyResultStatus()
\r
532 for (var i = 0; i < _enemyHp.Length; i++)
\r
534 EnemyResultStatus[i].MaxHp = _enemyStartHp[i];
\r
535 EnemyResultStatus[i].NowHp = _enemyHp[i];
\r
537 for (var i = 0; i < _enemyGuardHp.Length; i++)
\r
539 EnemyGuardResultStatus[i].MaxHp = _enemyGuardStartHp[i];
\r
540 EnemyGuardResultStatus[i].NowHp = _enemyGuardHp[i];
\r
544 public void SetEscapeShips(dynamic json)
\r
546 _escapingShips.Clear();
\r
547 if (!json.api_escape_flag() || (int)json.api_escape_flag == 0)
\r
549 var damaged = (int)json.api_escape.api_escape_idx[0] - 1;
\r
550 if (json.api_escape.api_tow_idx())
\r
552 _escapingShips.Add(_shipInfo.GetDeck(damaged / 6)[damaged % 6]);
\r
553 var escort = (int)json.api_escape.api_tow_idx[0] - 1;
\r
554 _escapingShips.Add(_shipInfo.GetDeck(escort / 6)[escort % 6]);
\r
558 _escapingShips.Add(_shipInfo.GetDeck(2)[damaged]);
\r
562 public void CauseEscape()
\r
564 _shipInfo.SetEscapedShips(_escapingShips);
\r
565 _shipInfo.SetBadlyDamagedShips();
\r
568 private class Record
\r
570 private ShipStatus _status;
\r
571 public int NowHp => _status.NowHp;
\r
572 public bool Escaped => _status.Escaped;
\r
573 public ShipStatus.Damage DamageLevel => _status.DamageLevel;
\r
574 public int StartHp;
\r
576 public static Record[] Setup(ShipStatus[] ships) =>
\r
577 (from s in ships select new Record {_status = (ShipStatus)s.Clone(), StartHp = s.NowHp}).ToArray();
\r
579 public void ApplyDamage(int damage)
\r
581 if (_status.NowHp > damage)
\r
583 _status.NowHp -= damage;
\r
587 foreach (var item in new[] {_status.SlotEx}.Concat(_status.Slot))
\r
589 if (item.Spec.Id == 42)
\r
591 _status.NowHp = (int)(_status.MaxHp * 0.2);
\r
592 ConsumeSlotItem(_status, 42);
\r
595 if (item.Spec.Id == 43)
\r
597 _status.NowHp = _status.MaxHp;
\r
598 ConsumeSlotItem(_status, 43);
\r
604 public void UpdateShipStatus(ShipStatus ship)
\r
606 ship.NowHp = NowHp;
\r
607 ship.Slot = _status.Slot;
\r
608 ship.SlotEx = _status.SlotEx;
\r
612 private BattleResultRank CalcLdAirBattleRank()
\r
614 var combined = _friend.Concat(_guard).ToArray();
\r
615 var friendNowShips = combined.Count(r => r.NowHp > 0);
\r
616 var friendGauge = combined.Sum(r => r.StartHp - r.NowHp);
\r
617 var friendSunk = combined.Count(r => r.NowHp == 0);
\r
618 var friendGaugeRate = Floor((double)friendGauge / combined.Sum(r => r.StartHp) * 100);
\r
620 if (friendSunk == 0)
\r
622 if (friendGauge == 0)
\r
623 return BattleResultRank.P;
\r
624 if (friendGaugeRate < 10)
\r
625 return BattleResultRank.A;
\r
626 if (friendGaugeRate < 20)
\r
627 return BattleResultRank.B;
\r
628 if (friendGaugeRate < 50)
\r
629 return BattleResultRank.C;
\r
630 return BattleResultRank.D;
\r
632 if (friendSunk < friendNowShips)
\r
633 return BattleResultRank.D;
\r
634 return BattleResultRank.E;
\r
637 private BattleResultRank CalcResultRank()
\r
639 var friend = _friend.Concat(_guard).ToArray();
\r
640 var enemyHp = _enemyHp.Concat(_enemyGuardHp).ToArray();
\r
641 var enemyStartHp = _enemyStartHp.Concat(_enemyGuardStartHp).ToArray();
\r
643 var friendCount = friend.Length;
\r
644 var friendStartHpTotal = 0;
\r
645 var friendNowHpTotal = 0;
\r
646 var friendSunk = 0;
\r
647 foreach (var ship in friend)
\r
651 friendStartHpTotal += ship.StartHp;
\r
652 friendNowHpTotal += ship.NowHp;
\r
653 if (ship.NowHp == 0)
\r
656 var friendGaugeRate = (int)((double)(friendStartHpTotal - friendNowHpTotal) / friendStartHpTotal * 100);
\r
658 var enemyCount = enemyHp.Length;
\r
659 var enemyStartHpTotal = enemyStartHp.Sum();
\r
660 var enemyNowHpTotal = enemyHp.Sum();
\r
661 var enemySunk = enemyHp.Count(hp => hp == 0);
\r
662 var enemyGaugeRate = (int)((double)(enemyStartHpTotal - enemyNowHpTotal) / enemyStartHpTotal * 100);
\r
664 if (friendSunk == 0 && enemySunk == enemyCount)
\r
666 if (friendNowHpTotal >= friendStartHpTotal)
\r
667 return BattleResultRank.P;
\r
668 return BattleResultRank.S;
\r
670 if (friendSunk == 0 && enemySunk >= (int)(enemyCount * 0.7) && enemyCount > 1)
\r
671 return BattleResultRank.A;
\r
672 if (friendSunk < enemySunk && enemyHp[0] == 0)
\r
673 return BattleResultRank.B;
\r
674 if (friendCount == 1 && friend[0].DamageLevel == ShipStatus.Damage.Badly)
\r
675 return BattleResultRank.D;
\r
676 if (enemyGaugeRate > friendGaugeRate * 2.5)
\r
677 return BattleResultRank.B;
\r
678 if (enemyGaugeRate > friendGaugeRate * 0.9)
\r
679 return BattleResultRank.C;
\r
680 if (friendCount > 1 && friendCount - 1 == friendSunk)
\r
681 return BattleResultRank.E;
\r
682 return BattleResultRank.D;
\r