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 int _flagshipRecoveryType;
\r
62 private bool _lastCell;
\r
64 public BattleState BattleState { get; set; }
\r
65 public string Formation { get; private set; }
\r
66 public EnemyFighterPower EnemyFighterPower { get; private set; }
\r
67 public int AirControlLevel { get; private set; }
\r
68 public BattleResultRank ResultRank { get; private set; }
\r
69 public RankPair DisplayedResultRank { get; } = new RankPair();
\r
70 public ShipStatus[] EnemyResultStatus { get; private set; }
\r
71 public ShipStatus[] EnemyGuardResultStatus { get; private set; }
\r
72 public bool EnemyIsCombined => EnemyGuardResultStatus.Length > 0;
\r
73 public List<AirBattleResult> AirBattleResults { get; } = new List<AirBattleResult>();
\r
75 public class RankPair
\r
77 public char Assumed { get; set; }
\r
78 public char Actual { get; set; }
\r
79 public bool IsError => Assumed != Actual;
\r
82 public BattleInfo(ShipInfo shipInfo, ItemInfo itemInfo)
\r
84 _shipInfo = shipInfo;
\r
85 _itemInfo = itemInfo;
\r
88 public void InspectBattle(dynamic json, string url)
\r
90 Formation = FormationName(json);
\r
91 AirControlLevel = CheckAirControlLevel(json);
\r
92 ShowResult(false); // 昼戦の結果を夜戦のときに表示する
\r
94 EnemyFighterPower = CalcEnemyFighterPower(json);
\r
95 BattleState = IsNightBattle(json) ? BattleState.Night : BattleState.Day;
\r
97 ClearEnemyOverKill();
\r
98 ResultRank = url.EndsWith("ld_airbattle") ? CalcLdAirBattleRank() : CalcResultRank();
\r
101 private void ClearEnemyOverKill()
\r
103 _enemyHp = _enemyHp.Select(hp => hp < 0 ? 0 : hp).ToArray();
\r
104 _enemyGuardHp = _enemyGuardHp.Select(hp => hp < 0 ? 0 : hp).ToArray();
\r
107 public void InspectMapNext(string request)
\r
109 var type = HttpUtility.ParseQueryString(request)["api_recovery_type"];
\r
112 _flagshipRecoveryType = int.Parse(type);
\r
115 private bool IsNightBattle(dynamic json) => json.api_hougeki();
\r
117 public static int DeckId(dynamic json)
\r
119 if (json.api_dock_id()) // 昼戦はtypoしている
\r
120 return (int)json.api_dock_id - 1;
\r
121 if (json.api_deck_id is string) // 通常の夜戦と連合艦隊(味方のみ)では文字列
\r
122 return int.Parse(json.api_deck_id) - 1;
\r
123 return (int)json.api_deck_id - 1;
\r
126 private string FormationName(dynamic json)
\r
128 if (!json.api_formation()) // 演習の夜戦
\r
130 switch ((int)json.api_formation[2])
\r
144 private void SetupResult(dynamic json)
\r
146 if (_friend != null)
\r
148 _fleet = DeckId(json);
\r
149 var fstats = _shipInfo.GetShipStatuses(_fleet);
\r
150 FlagshipRecovery(fstats[0]);
\r
151 _friend = Record.Setup(fstats);
\r
152 _enemyHp = (int[])json.api_e_nowhps;
\r
153 _enemyStartHp = (int[])_enemyHp.Clone();
\r
154 EnemyResultStatus = ((int[])json.api_ship_ke)
\r
155 .Select(id => new ShipStatus {Id = id, Spec = _shipInfo.GetSpec(id)}).ToArray();
\r
156 EnemyGuardResultStatus = new ShipStatus[0];
\r
157 if (json.api_ship_ke_combined())
\r
159 EnemyGuardResultStatus = ((int[])json.api_ship_ke_combined)
\r
160 .Select(id => new ShipStatus {Id = id, Spec = _shipInfo.GetSpec(id)}).ToArray();
\r
162 _guard = new Record[0];
\r
163 _enemyGuardHp = new int[0];
\r
164 _enemyGuardStartHp = new int[0];
\r
165 if (json.api_f_nowhps_combined())
\r
166 _guard = Record.Setup(_shipInfo.GetShipStatuses(1));
\r
167 if (json.api_e_nowhps_combined()) // 敵が連合艦隊
\r
169 _enemyGuardHp = (int[])json.api_e_nowhps_combined;
\r
170 _enemyGuardStartHp = (int[])_enemyGuardHp.Clone();
\r
174 private void FlagshipRecovery(ShipStatus flagship)
\r
176 switch (_flagshipRecoveryType)
\r
181 flagship.NowHp = flagship.MaxHp / 2;
\r
182 ConsumeSlotItem(flagship, 42); // ダメコン
\r
185 flagship.NowHp = flagship.MaxHp;
\r
186 ConsumeSlotItem(flagship, 43); // 女神
\r
189 if (_flagshipRecoveryType != 0)
\r
190 _shipInfo.SetBadlyDamagedShips();
\r
191 _flagshipRecoveryType = 0;
\r
194 private static void ConsumeSlotItem(ShipStatus ship, int id)
\r
196 if (ship.SlotEx.Spec.Id == id)
\r
198 ship.SlotEx = new ItemStatus();
\r
201 for (var i = 0; i < ship.Slot.Length; i++)
\r
203 if (ship.Slot[i].Spec.Id == id)
\r
205 ship.Slot[i] = new ItemStatus();
\r
211 public void CleanupResult()
\r
217 private int CheckAirControlLevel(dynamic json)
\r
219 if (!json.api_kouku())
\r
221 var stage1 = json.api_kouku.api_stage1;
\r
222 if (stage1 == null)
\r
224 if (stage1.api_f_count == 0 && stage1.api_e_count == 0)
\r
226 return (int)stage1.api_disp_seiku;
\r
229 private EnemyFighterPower CalcEnemyFighterPower(dynamic json)
\r
231 var result = new EnemyFighterPower();
\r
232 var ships = (int[])json.api_ship_ke;
\r
233 if (json.api_ship_ke_combined() && _guard.Length > 0)
\r
234 ships = ships.Concat((int[])json.api_ship_ke_combined).ToArray();
\r
235 var maxEq = ships.SelectMany(id =>
\r
237 var r = _shipInfo.GetSpec(id).MaxEq;
\r
240 result.HasUnknown = true;
\r
243 var equips = ((int[][])json.api_eSlot).SelectMany(x => x);
\r
244 if (json.api_eSlot_combined() && _guard.Length > 0)
\r
245 equips = equips.Concat(((int[][])json.api_eSlot_combined).SelectMany(x => x));
\r
246 foreach (var entry in from slot in equips.Zip(maxEq, (id, max) => new {id, max})
\r
247 let spec = _itemInfo.GetSpecByItemId(slot.id)
\r
248 let perSlot = (int)Floor(spec.AntiAir * Sqrt(slot.max))
\r
249 select new {spec, perSlot})
\r
251 if (entry.spec.CanAirCombat)
\r
252 result.AirCombat += entry.perSlot;
\r
253 if (entry.spec.IsAircraft)
\r
254 result.Interception += entry.perSlot;
\r
259 private enum CombatType
\r
268 private class Phase
\r
270 public string Api { get; }
\r
271 public CombatType Type { get; }
\r
272 public string Name { get; }
\r
274 public Phase(string api, CombatType type, string name = "")
\r
282 private void CalcDamage(dynamic json)
\r
284 AirBattleResults.Clear();
\r
287 new Phase("air_base_injection", CombatType.Aircraft, "AB噴式"),
\r
288 new Phase("injection_kouku", CombatType.Aircraft, "噴式"),
\r
289 new Phase("air_base_attack", CombatType.AirBase),
\r
290 new Phase("n_support_info", CombatType.Support),
\r
291 new Phase("n_hougeki1", CombatType.ByTurn),
\r
292 new Phase("n_hougeki2", CombatType.ByTurn),
\r
293 new Phase("kouku", CombatType.Aircraft, "航空戦"),
\r
294 new Phase("kouku2", CombatType.Aircraft, "航空戦2"),
\r
295 new Phase("support_info", CombatType.Support),
\r
296 new Phase("opening_taisen", CombatType.ByTurn),
\r
297 new Phase("opening_atack", CombatType.AtOnce),
\r
298 new Phase("hougeki", CombatType.ByTurn),
\r
299 new Phase("hougeki1", CombatType.ByTurn),
\r
300 new Phase("hougeki2", CombatType.ByTurn),
\r
301 new Phase("hougeki3", CombatType.ByTurn),
\r
302 new Phase("raigeki", CombatType.AtOnce)
\r
304 foreach (var phase in phases)
\r
305 CalcDamageByType(json, phase);
\r
308 private void CalcDamageByType(dynamic json, Phase phase)
\r
310 var api = "api_" + phase.Api;
\r
311 if (!json.IsDefined(api) || json[api] == null)
\r
313 switch (phase.Type)
\r
315 case CombatType.AtOnce:
\r
316 CalcDamageAtOnce(json[api]);
\r
318 case CombatType.ByTurn:
\r
319 CalcDamageByTurn(json[api]);
\r
321 case CombatType.Support:
\r
322 CalcSupportDamage(json[api]);
\r
324 case CombatType.Aircraft:
\r
325 AddAirBattleResult(json[api], phase.Name);
\r
326 CalcKoukuDamage(json[api]);
\r
328 case CombatType.AirBase:
\r
329 CalcAirBaseAttackDamage(json[api]);
\r
334 private void CalcSupportDamage(dynamic json)
\r
336 if (json.api_support_hourai != null)
\r
338 CalcDamageAtOnce(json.api_support_hourai.api_damage, _enemyHp, _enemyGuardHp);
\r
340 else if (json.api_support_airatack != null)
\r
342 CalcDamageAtOnce(json.api_support_airatack.api_stage3.api_edam, _enemyHp, _enemyGuardHp);
\r
346 private void CalcAirBaseAttackDamage(dynamic json)
\r
349 foreach (var entry in json)
\r
351 AddAirBattleResult(entry, "基地" + i++);
\r
352 CalcKoukuDamage(entry);
\r
356 private void AddAirBattleResult(dynamic json, string phaseName)
\r
358 var stage1 = json.api_stage1;
\r
359 if (stage1 == null || (stage1.api_f_count == 0 && stage1.api_e_count == 0))
\r
361 AirBattleResults.Add(new AirBattleResult
\r
363 PhaseName = phaseName,
\r
364 AirControlLevel = json.api_stage1.api_disp_seiku() ? (int)json.api_stage1.api_disp_seiku : 0,
\r
365 Stage1 = new AirBattleResult.StageResult
\r
367 FriendCount = (int)json.api_stage1.api_f_count,
\r
368 FriendLost = (int)json.api_stage1.api_f_lostcount,
\r
369 EnemyCount = (int)json.api_stage1.api_e_count,
\r
370 EnemyLost = (int)json.api_stage1.api_e_lostcount
\r
372 Stage2 = json.api_stage2 == null
\r
373 ? new AirBattleResult.StageResult
\r
380 : new AirBattleResult.StageResult
\r
382 FriendCount = (int)json.api_stage2.api_f_count,
\r
383 FriendLost = (int)json.api_stage2.api_f_lostcount,
\r
384 EnemyCount = (int)json.api_stage2.api_e_count,
\r
385 EnemyLost = (int)json.api_stage2.api_e_lostcount
\r
390 private void CalcKoukuDamage(dynamic json)
\r
392 if (json.api_stage3() && json.api_stage3 != null)
\r
393 CalcDamageAtOnce(json.api_stage3, _friend, _enemyHp);
\r
394 if (json.api_stage3_combined() && json.api_stage3_combined != null)
\r
395 CalcDamageAtOnce(json.api_stage3_combined, _guard, _enemyGuardHp);
\r
398 private void CalcDamageAtOnce(dynamic json)
\r
400 CalcDamageAtOnce(json, _friend, _guard, _enemyHp, _enemyGuardHp);
\r
403 private void CalcDamageAtOnce(dynamic json, Record[] friend, int[] enemy)
\r
405 CalcDamageAtOnce(json, friend, null, enemy, null);
\r
408 private void CalcDamageAtOnce(dynamic json, Record[] friend, Record[] guard, int[] enemy, int[] enemyGuard)
\r
410 if (json.api_fdam() && json.api_fdam != null)
\r
411 CalcDamageAtOnce(json.api_fdam, friend, guard);
\r
412 if (json.api_edam() && json.api_edam != null)
\r
413 CalcDamageAtOnce(json.api_edam, enemy, enemyGuard);
\r
416 private void CalcDamageAtOnce(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 CalcDamageAtOnce(dynamic rawDamage, int[] enemy, int[] enemyGuard = null)
\r
429 var damage = (int[])rawDamage;
\r
430 for (var i = 0; i < enemy.Length; i++)
\r
431 enemy[i] -= damage[i];
\r
432 if (enemyGuard == null)
\r
434 for (var i = 0; i < enemyGuard.Length; i++)
\r
435 enemyGuard[i] -= damage[i + 6];
\r
438 private void CalcDamageByTurn(dynamic json)
\r
440 if (!(json.api_df_list() && json.api_df_list != null &&
\r
441 json.api_damage() && json.api_damage != null &&
\r
442 json.api_at_eflag() && json.api_at_eflag != null))
\r
445 var targets = (int[][])json.api_df_list;
\r
446 var damages = (int[][])json.api_damage;
\r
447 var eflags = (int[])json.api_at_eflag;
\r
449 for (var i = 0; i < eflags.Length; i++)
\r
451 // 一度に複数の目標を狙う攻撃はないものと仮定する
\r
452 var hit = new {t = targets[i][0], d = damages[i].Sum()};
\r
455 if (eflags[i] == 1)
\r
457 if (hit.t < _friend.Length)
\r
459 _friend[hit.t].ApplyDamage(hit.d);
\r
463 _guard[hit.t - 6].ApplyDamage(hit.d);
\r
468 if (hit.t < _enemyHp.Length)
\r
470 _enemyHp[hit.t] -= hit.d;
\r
474 _enemyGuardHp[hit.t - 6] -= hit.d;
\r
480 public void InspectMapStart(dynamic json)
\r
482 InspectMapNext(json);
\r
485 public void InspectMapNext(dynamic json)
\r
487 _lastCell = (int)json.api_next == 0;
\r
490 public void InspectBattleResult(dynamic json)
\r
492 BattleState = BattleState.Result;
\r
493 ShowResult(!_lastCell);
\r
494 _shipInfo.SaveBattleResult();
\r
495 VerifyResultRank(json);
\r
497 SetEscapeShips(json);
\r
502 private void VerifyResultRank(dynamic json)
\r
504 if (_friend == null)
\r
506 if (!json.api_win_rank())
\r
508 var assumed = "PSABCDE"[(int)ResultRank];
\r
509 if (assumed == 'P')
\r
511 var actual = ((string)json.api_win_rank)[0];
\r
512 DisplayedResultRank.Assumed = assumed;
\r
513 DisplayedResultRank.Actual = actual;
\r
516 public void InspectPracticeResult(dynamic json)
\r
518 BattleState = BattleState.Result;
\r
523 private void ShowResult(bool warnDamagedShip = true)
\r
525 if (_friend == null)
\r
527 var ships = _guard.Length > 0
\r
528 ? _shipInfo.GetShipStatuses(0).Concat(_shipInfo.GetShipStatuses(1)).ToArray()
\r
529 : _shipInfo.GetShipStatuses(_fleet);
\r
530 foreach (var entry in ships.Zip(_friend.Concat(_guard), (ship, now) => new {ship, now}))
\r
531 entry.now.UpdateShipStatus(entry.ship);
\r
532 if (warnDamagedShip)
\r
533 _shipInfo.SetBadlyDamagedShips();
\r
535 _shipInfo.ClearBadlyDamagedShips();
\r
536 SetEnemyResultStatus();
\r
539 private void SetEnemyResultStatus()
\r
541 for (var i = 0; i < _enemyHp.Length; i++)
\r
543 EnemyResultStatus[i].MaxHp = _enemyStartHp[i];
\r
544 EnemyResultStatus[i].NowHp = _enemyHp[i];
\r
546 for (var i = 0; i < _enemyGuardHp.Length; i++)
\r
548 EnemyGuardResultStatus[i].MaxHp = _enemyGuardStartHp[i];
\r
549 EnemyGuardResultStatus[i].NowHp = _enemyGuardHp[i];
\r
553 public void SetEscapeShips(dynamic json)
\r
555 _escapingShips.Clear();
\r
556 if (!json.api_escape_flag() || (int)json.api_escape_flag == 0)
\r
558 var damaged = (int)json.api_escape.api_escape_idx[0] - 1;
\r
559 if (json.api_escape.api_tow_idx())
\r
561 _escapingShips.Add(_shipInfo.GetDeck(damaged / 6)[damaged % 6]);
\r
562 var escort = (int)json.api_escape.api_tow_idx[0] - 1;
\r
563 _escapingShips.Add(_shipInfo.GetDeck(escort / 6)[escort % 6]);
\r
567 _escapingShips.Add(_shipInfo.GetDeck(2)[damaged]);
\r
571 public void CauseEscape()
\r
573 _shipInfo.SetEscapedShips(_escapingShips);
\r
574 _shipInfo.SetBadlyDamagedShips();
\r
577 private class Record
\r
579 private ShipStatus _status;
\r
580 public int NowHp => _status.NowHp;
\r
581 public bool Escaped => _status.Escaped;
\r
582 public ShipStatus.Damage DamageLevel => _status.DamageLevel;
\r
583 public int StartHp;
\r
585 public static Record[] Setup(ShipStatus[] ships) =>
\r
586 (from s in ships select new Record {_status = (ShipStatus)s.Clone(), StartHp = s.NowHp}).ToArray();
\r
588 public void ApplyDamage(int damage)
\r
590 if (_status.NowHp > damage)
\r
592 _status.NowHp -= damage;
\r
596 foreach (var item in new[] {_status.SlotEx}.Concat(_status.Slot))
\r
598 if (item.Spec.Id == 42)
\r
600 _status.NowHp = (int)(_status.MaxHp * 0.2);
\r
601 ConsumeSlotItem(_status, 42);
\r
604 if (item.Spec.Id == 43)
\r
606 _status.NowHp = _status.MaxHp;
\r
607 ConsumeSlotItem(_status, 43);
\r
613 public void UpdateShipStatus(ShipStatus ship)
\r
615 ship.NowHp = NowHp;
\r
616 ship.Slot = _status.Slot;
\r
617 ship.SlotEx = _status.SlotEx;
\r
621 private BattleResultRank CalcLdAirBattleRank()
\r
623 var combined = _friend.Concat(_guard).ToArray();
\r
624 var friendNowShips = combined.Count(r => r.NowHp > 0);
\r
625 var friendGauge = combined.Sum(r => r.StartHp - r.NowHp);
\r
626 var friendSunk = combined.Count(r => r.NowHp == 0);
\r
627 var friendGaugeRate = Floor((double)friendGauge / combined.Sum(r => r.StartHp) * 100);
\r
629 if (friendSunk == 0)
\r
631 if (friendGauge == 0)
\r
632 return BattleResultRank.P;
\r
633 if (friendGaugeRate < 10)
\r
634 return BattleResultRank.A;
\r
635 if (friendGaugeRate < 20)
\r
636 return BattleResultRank.B;
\r
637 if (friendGaugeRate < 50)
\r
638 return BattleResultRank.C;
\r
639 return BattleResultRank.D;
\r
641 if (friendSunk < friendNowShips)
\r
642 return BattleResultRank.D;
\r
643 return BattleResultRank.E;
\r
646 private BattleResultRank CalcResultRank()
\r
648 var friend = _friend.Concat(_guard).ToArray();
\r
649 var enemyHp = _enemyHp.Concat(_enemyGuardHp).ToArray();
\r
650 var enemyStartHp = _enemyStartHp.Concat(_enemyGuardStartHp).ToArray();
\r
652 var friendCount = friend.Length;
\r
653 var friendStartHpTotal = 0;
\r
654 var friendNowHpTotal = 0;
\r
655 var friendSunk = 0;
\r
656 foreach (var ship in friend)
\r
660 friendStartHpTotal += ship.StartHp;
\r
661 friendNowHpTotal += ship.NowHp;
\r
662 if (ship.NowHp == 0)
\r
665 var friendGaugeRate = (int)((double)(friendStartHpTotal - friendNowHpTotal) / friendStartHpTotal * 100);
\r
667 var enemyCount = enemyHp.Length;
\r
668 var enemyStartHpTotal = enemyStartHp.Sum();
\r
669 var enemyNowHpTotal = enemyHp.Sum();
\r
670 var enemySunk = enemyHp.Count(hp => hp == 0);
\r
671 var enemyGaugeRate = (int)((double)(enemyStartHpTotal - enemyNowHpTotal) / enemyStartHpTotal * 100);
\r
673 if (friendSunk == 0 && enemySunk == enemyCount)
\r
675 if (friendNowHpTotal >= friendStartHpTotal)
\r
676 return BattleResultRank.P;
\r
677 return BattleResultRank.S;
\r
679 if (friendSunk == 0 && enemySunk >= (int)(enemyCount * 0.7) && enemyCount > 1)
\r
680 return BattleResultRank.A;
\r
681 if (friendSunk < enemySunk && enemyHp[0] == 0)
\r
682 return BattleResultRank.B;
\r
683 if (friendCount == 1 && friend[0].DamageLevel == ShipStatus.Damage.Badly)
\r
684 return BattleResultRank.D;
\r
685 if (enemyGaugeRate > friendGaugeRate * 2.5)
\r
686 return BattleResultRank.B;
\r
687 if (enemyGaugeRate > friendGaugeRate * 0.9)
\r
688 return BattleResultRank.C;
\r
689 if (friendCount > 1 && friendCount - 1 == friendSunk)
\r
690 return BattleResultRank.E;
\r
691 return BattleResultRank.D;
\r