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
40 public class EnemyFighterPower
\r
42 public bool HasUnknown { get; set; }
\r
43 public string UnknownMark => HasUnknown ? "+" : "";
\r
44 public int AirCombat { get; set; }
\r
45 public int Interception { get; set; }
\r
48 public class BattleInfo
\r
50 private readonly ShipInfo _shipInfo;
\r
51 private readonly ItemInfo _itemInfo;
\r
53 private Record[] _friend;
\r
54 private Record[] _guard;
\r
55 private int[] _enemyHp;
\r
56 private int[] _enemyGuardHp;
\r
57 private int[] _enemyStartHp;
\r
58 private int[] _enemyGuardStartHp;
\r
59 private readonly List<int> _escapingShips = new List<int>();
\r
60 private int _flagshipRecoveryType;
\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 List<char> WrongResultRank { get; set; } = new List<char>(2);
\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 BattleInfo(ShipInfo shipInfo, ItemInfo itemInfo)
\r
76 _shipInfo = shipInfo;
\r
77 _itemInfo = itemInfo;
\r
80 public void InspectBattle(dynamic json, string url)
\r
82 Formation = FormationName(json);
\r
83 AirControlLevel = CheckAirControlLevel(json);
\r
84 ShowResult(false); // 昼戦の結果を夜戦のときに表示する
\r
86 EnemyFighterPower = CalcEnemyFighterPower(json);
\r
87 BattleState = IsNightBattle(json) ? BattleState.Night : BattleState.Day;
\r
89 ClearEnemyOverKill();
\r
90 ResultRank = url.EndsWith("ld_airbattle") ? CalcLdAirBattleRank() : CalcResultRank();
\r
93 private void ClearEnemyOverKill()
\r
95 _enemyHp = _enemyHp.Select(hp => hp < 0 ? 0 : hp).ToArray();
\r
96 _enemyGuardHp = _enemyGuardHp.Select(hp => hp < 0 ? 0 : hp).ToArray();
\r
99 public void InspectMapNext(string request)
\r
101 var type = HttpUtility.ParseQueryString(request)["api_recovery_type"];
\r
104 _flagshipRecoveryType = int.Parse(type);
\r
107 private bool IsNightBattle(dynamic json) => json.api_hougeki();
\r
109 public static int DeckId(dynamic json)
\r
111 if (json.api_dock_id()) // 昼戦はtypoしている
\r
112 return (int)json.api_dock_id - 1;
\r
113 if (json.api_deck_id is string) // 通常の夜戦と連合艦隊(味方のみ)では文字列
\r
114 return int.Parse(json.api_deck_id) - 1;
\r
115 return (int)json.api_deck_id - 1;
\r
118 private string FormationName(dynamic json)
\r
120 if (!json.api_formation()) // 演習の夜戦
\r
122 switch ((int)json.api_formation[2])
\r
136 private void SetupResult(dynamic json)
\r
138 if (_friend != null)
\r
140 _fleet = DeckId(json);
\r
141 var fstats = _shipInfo.GetShipStatuses(_fleet);
\r
142 FlagshipRecovery(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(ShipStatus flagship)
\r
168 switch (_flagshipRecoveryType)
\r
173 flagship.NowHp = flagship.MaxHp / 2;
\r
174 ConsumeSlotItem(flagship, 42); // ダメコン
\r
177 flagship.NowHp = flagship.MaxHp;
\r
178 ConsumeSlotItem(flagship, 43); // 女神
\r
181 if (_flagshipRecoveryType != 0)
\r
182 _shipInfo.SetBadlyDamagedShips();
\r
183 _flagshipRecoveryType = 0;
\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 void CalcDamage(dynamic json)
\r
253 AirBattleResults.Clear();
\r
254 if (json.api_air_base_injection())
\r
256 AddAirBattleResult(json.api_air_base_injection, "AB噴式");
\r
257 CalcKoukuDamage(json.api_air_base_injection);
\r
259 if (json.api_injection_kouku())
\r
261 AddAirBattleResult(json.api_injection_kouku, "噴式");
\r
262 CalcKoukuDamage(json.api_injection_kouku);
\r
264 if (json.api_air_base_attack())
\r
265 CalcAirBaseAttackDamage(json.api_air_base_attack);
\r
266 if (json.api_kouku())
\r
268 AddAirBattleResult(json.api_kouku, "航空戦");
\r
269 CalcKoukuDamage(json.api_kouku);
\r
271 if (json.api_kouku2()) // 航空戦2回目
\r
273 AddAirBattleResult(json.api_kouku2, "航空戦2");
\r
274 CalcKoukuDamage(json.api_kouku2);
\r
276 CalcSurfaceBattleDamage(json);
\r
279 private enum CombatType
\r
286 private class Phase
\r
288 public string Api { get; }
\r
289 public CombatType Type { get; }
\r
291 public Phase(string api, CombatType type)
\r
298 private void CalcSurfaceBattleDamage(dynamic json)
\r
302 new Phase("support_info", CombatType.Support),
\r
303 new Phase("n_support_info", CombatType.Support),
\r
304 new Phase("opening_taisen", CombatType.ByTurn),
\r
305 new Phase("opening_atack", CombatType.AtOnce),
\r
306 new Phase("hougeki", CombatType.ByTurn),
\r
307 new Phase("hougeki1", CombatType.ByTurn),
\r
308 new Phase("hougeki2", CombatType.ByTurn),
\r
309 new Phase("hougeki3", CombatType.ByTurn),
\r
310 new Phase("raigeki", CombatType.AtOnce)
\r
312 foreach (var phase in phases)
\r
313 CalcDamageByType(json, "api_" + phase.Api, phase.Type);
\r
316 private void CalcDamageByType(dynamic json, string api, CombatType type)
\r
318 if (!json.IsDefined(api) || json[api] == null)
\r
322 case CombatType.AtOnce:
\r
323 CalcDamageAtOnce(json[api]);
\r
325 case CombatType.ByTurn:
\r
326 CalcDamageByTurn(json[api]);
\r
328 case CombatType.Support:
\r
329 CalcSupportDamage(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 = ((dynamic[])json.api_df_list).Select(x => (int[])x);
\r
446 var damages = ((dynamic[])json.api_damage).Select(x => (int[])x);
\r
447 var eflags = (int[])json.api_at_eflag;
\r
448 foreach (var turn in
\r
449 targets.Zip(damages, (t, d) => new {t, d}).Zip(eflags, (td, e) => new {e, td.t, td.d}))
\r
451 foreach (var hit in turn.t.Zip(turn.d, (t, d) => new {t, d}))
\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
481 public void InspectMapStart(dynamic json)
\r
483 InspectMapNext(json);
\r
486 public void InspectMapNext(dynamic json)
\r
488 _lastCell = (int)json.api_next == 0;
\r
491 public void InspectBattleResult(dynamic json)
\r
493 BattleState = BattleState.Result;
\r
494 ShowResult(!_lastCell);
\r
495 VerifyResultRank(json);
\r
497 SetEscapeShips(json);
\r
500 private void VerifyResultRank(dynamic json)
\r
502 if (_friend == null)
\r
504 WrongResultRank.Clear();
\r
505 if (!json.api_win_rank())
\r
507 var assumed = "PSABCDE"[(int)ResultRank];
\r
508 if (assumed == 'P')
\r
510 var actual = ((string)json.api_win_rank)[0];
\r
511 if (assumed == actual)
\r
513 WrongResultRank.AddRange(new[] {assumed, 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 CauseCombinedBattleEscape()
\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