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 List<char> WrongResultRank { get; set; } = new List<char>(2);
\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 BattleInfo(ShipInfo shipInfo, ItemInfo itemInfo)
\r
77 _shipInfo = shipInfo;
\r
78 _itemInfo = itemInfo;
\r
81 public void InspectBattle(dynamic json, string url)
\r
83 Formation = FormationName(json);
\r
84 AirControlLevel = CheckAirControlLevel(json);
\r
85 ShowResult(false); // 昼戦の結果を夜戦のときに表示する
\r
87 EnemyFighterPower = CalcEnemyFighterPower(json);
\r
88 BattleState = IsNightBattle(json) ? BattleState.Night : BattleState.Day;
\r
90 ClearEnemyOverKill();
\r
91 ResultRank = url.EndsWith("ld_airbattle") ? CalcLdAirBattleRank() : CalcResultRank();
\r
94 private void ClearEnemyOverKill()
\r
96 _enemyHp = _enemyHp.Select(hp => hp < 0 ? 0 : hp).ToArray();
\r
97 _enemyGuardHp = _enemyGuardHp.Select(hp => hp < 0 ? 0 : hp).ToArray();
\r
100 public void InspectMapNext(string request)
\r
102 var type = HttpUtility.ParseQueryString(request)["api_recovery_type"];
\r
105 _flagshipRecoveryType = int.Parse(type);
\r
108 private bool IsNightBattle(dynamic json) => json.api_hougeki();
\r
110 public static int DeckId(dynamic json)
\r
112 if (json.api_dock_id()) // 昼戦はtypoしている
\r
113 return (int)json.api_dock_id - 1;
\r
114 if (json.api_deck_id is string) // 通常の夜戦と連合艦隊(味方のみ)では文字列
\r
115 return int.Parse(json.api_deck_id) - 1;
\r
116 return (int)json.api_deck_id - 1;
\r
119 private string FormationName(dynamic json)
\r
121 if (!json.api_formation()) // 演習の夜戦
\r
123 switch ((int)json.api_formation[2])
\r
137 private void SetupResult(dynamic json)
\r
139 if (_friend != null)
\r
141 _fleet = DeckId(json);
\r
142 var fstats = _shipInfo.GetShipStatuses(_fleet);
\r
143 FlagshipRecovery(fstats[0]);
\r
144 _friend = Record.Setup(fstats);
\r
145 _enemyHp = (int[])json.api_e_nowhps;
\r
146 _enemyStartHp = (int[])_enemyHp.Clone();
\r
147 EnemyResultStatus = ((int[])json.api_ship_ke)
\r
148 .Select(id => new ShipStatus {Id = id, Spec = _shipInfo.GetSpec(id)}).ToArray();
\r
149 EnemyGuardResultStatus = new ShipStatus[0];
\r
150 if (json.api_ship_ke_combined())
\r
152 EnemyGuardResultStatus = ((int[])json.api_ship_ke_combined)
\r
153 .Select(id => new ShipStatus {Id = id, Spec = _shipInfo.GetSpec(id)}).ToArray();
\r
155 _guard = new Record[0];
\r
156 _enemyGuardHp = new int[0];
\r
157 _enemyGuardStartHp = new int[0];
\r
158 if (json.api_f_nowhps_combined())
\r
159 _guard = Record.Setup(_shipInfo.GetShipStatuses(1));
\r
160 if (json.api_e_nowhps_combined()) // 敵が連合艦隊
\r
162 _enemyGuardHp = (int[])json.api_e_nowhps_combined;
\r
163 _enemyGuardStartHp = (int[])_enemyGuardHp.Clone();
\r
167 private void FlagshipRecovery(ShipStatus flagship)
\r
169 switch (_flagshipRecoveryType)
\r
174 flagship.NowHp = flagship.MaxHp / 2;
\r
175 ConsumeSlotItem(flagship, 42); // ダメコン
\r
178 flagship.NowHp = flagship.MaxHp;
\r
179 ConsumeSlotItem(flagship, 43); // 女神
\r
182 if (_flagshipRecoveryType != 0)
\r
183 _shipInfo.SetBadlyDamagedShips();
\r
184 _flagshipRecoveryType = 0;
\r
187 private static void ConsumeSlotItem(ShipStatus ship, int id)
\r
189 if (ship.SlotEx.Spec.Id == id)
\r
191 ship.SlotEx = new ItemStatus();
\r
194 for (var i = 0; i < ship.Slot.Length; i++)
\r
196 if (ship.Slot[i].Spec.Id == id)
\r
198 ship.Slot[i] = new ItemStatus();
\r
204 public void CleanupResult()
\r
210 private int CheckAirControlLevel(dynamic json)
\r
212 if (!json.api_kouku())
\r
214 var stage1 = json.api_kouku.api_stage1;
\r
215 if (stage1 == null)
\r
217 if (stage1.api_f_count == 0 && stage1.api_e_count == 0)
\r
219 return (int)stage1.api_disp_seiku;
\r
222 private EnemyFighterPower CalcEnemyFighterPower(dynamic json)
\r
224 var result = new EnemyFighterPower();
\r
225 var ships = (int[])json.api_ship_ke;
\r
226 if (json.api_ship_ke_combined() && _guard.Length > 0)
\r
227 ships = ships.Concat((int[])json.api_ship_ke_combined).ToArray();
\r
228 var maxEq = ships.SelectMany(id =>
\r
230 var r = _shipInfo.GetSpec(id).MaxEq;
\r
233 result.HasUnknown = true;
\r
236 var equips = ((int[][])json.api_eSlot).SelectMany(x => x);
\r
237 if (json.api_eSlot_combined() && _guard.Length > 0)
\r
238 equips = equips.Concat(((int[][])json.api_eSlot_combined).SelectMany(x => x));
\r
239 foreach (var entry in from slot in equips.Zip(maxEq, (id, max) => new {id, max})
\r
240 let spec = _itemInfo.GetSpecByItemId(slot.id)
\r
241 let perSlot = (int)Floor(spec.AntiAir * Sqrt(slot.max))
\r
242 select new {spec, perSlot})
\r
244 if (entry.spec.CanAirCombat)
\r
245 result.AirCombat += entry.perSlot;
\r
246 if (entry.spec.IsAircraft)
\r
247 result.Interception += entry.perSlot;
\r
252 private void CalcDamage(dynamic json)
\r
254 AirBattleResults.Clear();
\r
255 if (json.api_air_base_injection())
\r
257 AddAirBattleResult(json.api_air_base_injection, "AB噴式");
\r
258 CalcKoukuDamage(json.api_air_base_injection);
\r
260 if (json.api_injection_kouku())
\r
262 AddAirBattleResult(json.api_injection_kouku, "噴式");
\r
263 CalcKoukuDamage(json.api_injection_kouku);
\r
265 if (json.api_air_base_attack())
\r
266 CalcAirBaseAttackDamage(json.api_air_base_attack);
\r
267 if (json.api_kouku())
\r
269 AddAirBattleResult(json.api_kouku, "航空戦");
\r
270 CalcKoukuDamage(json.api_kouku);
\r
272 if (json.api_kouku2()) // 航空戦2回目
\r
274 AddAirBattleResult(json.api_kouku2, "航空戦2");
\r
275 CalcKoukuDamage(json.api_kouku2);
\r
277 CalcSurfaceBattleDamage(json);
\r
280 private enum CombatType
\r
287 private class Phase
\r
289 public string Api { get; }
\r
290 public CombatType Type { get; }
\r
292 public Phase(string api, CombatType type)
\r
299 private void CalcSurfaceBattleDamage(dynamic json)
\r
303 new Phase("support_info", CombatType.Support),
\r
304 new Phase("n_support_info", CombatType.Support),
\r
305 new Phase("opening_taisen", CombatType.ByTurn),
\r
306 new Phase("opening_atack", CombatType.AtOnce),
\r
307 new Phase("hougeki", CombatType.ByTurn),
\r
308 new Phase("hougeki1", CombatType.ByTurn),
\r
309 new Phase("hougeki2", CombatType.ByTurn),
\r
310 new Phase("hougeki3", CombatType.ByTurn),
\r
311 new Phase("raigeki", CombatType.AtOnce)
\r
313 foreach (var phase in phases)
\r
314 CalcDamageByType(json, "api_" + phase.Api, phase.Type);
\r
317 private void CalcDamageByType(dynamic json, string api, CombatType type)
\r
319 if (!json.IsDefined(api) || json[api] == null)
\r
323 case CombatType.AtOnce:
\r
324 CalcDamageAtOnce(json[api]);
\r
326 case CombatType.ByTurn:
\r
327 CalcDamageByTurn(json[api]);
\r
329 case CombatType.Support:
\r
330 CalcSupportDamage(json[api]);
\r
335 private void CalcSupportDamage(dynamic json)
\r
337 if (json.api_support_hourai != null)
\r
339 CalcDamageAtOnce(json.api_support_hourai.api_damage, _enemyHp, _enemyGuardHp);
\r
341 else if (json.api_support_airatack != null)
\r
343 CalcDamageAtOnce(json.api_support_airatack.api_stage3.api_edam, _enemyHp, _enemyGuardHp);
\r
347 private void CalcAirBaseAttackDamage(dynamic json)
\r
350 foreach (var entry in json)
\r
352 AddAirBattleResult(entry, "基地" + i++);
\r
353 CalcKoukuDamage(entry);
\r
357 private void AddAirBattleResult(dynamic json, string phaseName)
\r
359 var stage1 = json.api_stage1;
\r
360 if (stage1 == null || (stage1.api_f_count == 0 && stage1.api_e_count == 0))
\r
362 AirBattleResults.Add(new AirBattleResult
\r
364 PhaseName = phaseName,
\r
365 AirControlLevel = json.api_stage1.api_disp_seiku() ? (int)json.api_stage1.api_disp_seiku : 0,
\r
366 Stage1 = new AirBattleResult.StageResult
\r
368 FriendCount = (int)json.api_stage1.api_f_count,
\r
369 FriendLost = (int)json.api_stage1.api_f_lostcount,
\r
370 EnemyCount = (int)json.api_stage1.api_e_count,
\r
371 EnemyLost = (int)json.api_stage1.api_e_lostcount
\r
373 Stage2 = json.api_stage2 == null
\r
374 ? new AirBattleResult.StageResult
\r
381 : new AirBattleResult.StageResult
\r
383 FriendCount = (int)json.api_stage2.api_f_count,
\r
384 FriendLost = (int)json.api_stage2.api_f_lostcount,
\r
385 EnemyCount = (int)json.api_stage2.api_e_count,
\r
386 EnemyLost = (int)json.api_stage2.api_e_lostcount
\r
391 private void CalcKoukuDamage(dynamic json)
\r
393 if (json.api_stage3() && json.api_stage3 != null)
\r
394 CalcDamageAtOnce(json.api_stage3, _friend, _enemyHp);
\r
395 if (json.api_stage3_combined() && json.api_stage3_combined != null)
\r
396 CalcDamageAtOnce(json.api_stage3_combined, _guard, _enemyGuardHp);
\r
399 private void CalcDamageAtOnce(dynamic json)
\r
401 CalcDamageAtOnce(json, _friend, _guard, _enemyHp, _enemyGuardHp);
\r
404 private void CalcDamageAtOnce(dynamic json, Record[] friend, int[] enemy)
\r
406 CalcDamageAtOnce(json, friend, null, enemy, null);
\r
409 private void CalcDamageAtOnce(dynamic json, Record[] friend, Record[] guard, int[] enemy, int[] enemyGuard)
\r
411 if (json.api_fdam() && json.api_fdam != null)
\r
412 CalcDamageAtOnce(json.api_fdam, friend, guard);
\r
413 if (json.api_edam() && json.api_edam != null)
\r
414 CalcDamageAtOnce(json.api_edam, enemy, enemyGuard);
\r
417 private void CalcDamageAtOnce(dynamic rawDamage, Record[] friend, Record[] guard = null)
\r
419 var damage = (int[])rawDamage;
\r
420 for (var i = 0; i < friend.Length; i++)
\r
421 friend[i].ApplyDamage(damage[i]);
\r
424 for (var i = 0; i < guard.Length; i++)
\r
425 guard[i].ApplyDamage(damage[i + 6]);
\r
428 private void CalcDamageAtOnce(dynamic rawDamage, int[] enemy, int[] enemyGuard = null)
\r
430 var damage = (int[])rawDamage;
\r
431 for (var i = 0; i < enemy.Length; i++)
\r
432 enemy[i] -= damage[i];
\r
433 if (enemyGuard == null)
\r
435 for (var i = 0; i < enemyGuard.Length; i++)
\r
436 enemyGuard[i] -= damage[i + 6];
\r
439 private void CalcDamageByTurn(dynamic json)
\r
441 if (!(json.api_df_list() && json.api_df_list != null &&
\r
442 json.api_damage() && json.api_damage != null &&
\r
443 json.api_at_eflag() && json.api_at_eflag != null))
\r
446 var targets = ((dynamic[])json.api_df_list).Select(x => (int[])x);
\r
447 var damages = ((dynamic[])json.api_damage).Select(x => (int[])x);
\r
448 var eflags = (int[])json.api_at_eflag;
\r
449 foreach (var turn in
\r
450 targets.Zip(damages, (t, d) => new {t, d}).Zip(eflags, (td, e) => new {e, td.t, td.d}))
\r
452 foreach (var hit in turn.t.Zip(turn.d, (t, d) => new {t, d}))
\r
458 if (hit.t < _friend.Length)
\r
460 _friend[hit.t].ApplyDamage(hit.d);
\r
464 _guard[hit.t - 6].ApplyDamage(hit.d);
\r
469 if (hit.t < _enemyHp.Length)
\r
471 _enemyHp[hit.t] -= hit.d;
\r
475 _enemyGuardHp[hit.t - 6] -= hit.d;
\r
482 public void InspectMapStart(dynamic json)
\r
484 InspectMapNext(json);
\r
487 public void InspectMapNext(dynamic json)
\r
489 _lastCell = (int)json.api_next == 0;
\r
492 public void InspectBattleResult(dynamic json)
\r
494 BattleState = BattleState.Result;
\r
495 ShowResult(!_lastCell);
\r
496 VerifyResultRank(json);
\r
498 SetEscapeShips(json);
\r
501 private void VerifyResultRank(dynamic json)
\r
503 if (_friend == null)
\r
505 WrongResultRank.Clear();
\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 if (assumed == actual)
\r
514 WrongResultRank.AddRange(new[] {assumed, actual});
\r
517 public void InspectPracticeResult(dynamic json)
\r
519 BattleState = BattleState.Result;
\r
524 private void ShowResult(bool warnDamagedShip = true)
\r
526 if (_friend == null)
\r
528 var ships = _guard.Length > 0
\r
529 ? _shipInfo.GetShipStatuses(0).Concat(_shipInfo.GetShipStatuses(1)).ToArray()
\r
530 : _shipInfo.GetShipStatuses(_fleet);
\r
531 foreach (var entry in ships.Zip(_friend.Concat(_guard), (ship, now) => new {ship, now}))
\r
532 entry.now.UpdateShipStatus(entry.ship);
\r
533 if (warnDamagedShip)
\r
534 _shipInfo.SetBadlyDamagedShips();
\r
536 _shipInfo.ClearBadlyDamagedShips();
\r
537 SetEnemyResultStatus();
\r
540 private void SetEnemyResultStatus()
\r
542 for (var i = 0; i < _enemyHp.Length; i++)
\r
544 EnemyResultStatus[i].MaxHp = _enemyStartHp[i];
\r
545 EnemyResultStatus[i].NowHp = _enemyHp[i];
\r
547 for (var i = 0; i < _enemyGuardHp.Length; i++)
\r
549 EnemyGuardResultStatus[i].MaxHp = _enemyGuardStartHp[i];
\r
550 EnemyGuardResultStatus[i].NowHp = _enemyGuardHp[i];
\r
554 public void SetEscapeShips(dynamic json)
\r
556 _escapingShips.Clear();
\r
557 if (!json.api_escape_flag() || (int)json.api_escape_flag == 0)
\r
559 var damaged = (int)json.api_escape.api_escape_idx[0] - 1;
\r
560 if (json.api_escape.api_tow_idx())
\r
562 _escapingShips.Add(_shipInfo.GetDeck(damaged / 6)[damaged % 6]);
\r
563 var escort = (int)json.api_escape.api_tow_idx[0] - 1;
\r
564 _escapingShips.Add(_shipInfo.GetDeck(escort / 6)[escort % 6]);
\r
568 _escapingShips.Add(_shipInfo.GetDeck(2)[damaged]);
\r
572 public void CauseCombinedBattleEscape()
\r
574 _shipInfo.SetEscapedShips(_escapingShips);
\r
575 _shipInfo.SetBadlyDamagedShips();
\r
578 private class Record
\r
580 private ShipStatus _status;
\r
581 public int NowHp => _status.NowHp;
\r
582 public bool Escaped => _status.Escaped;
\r
583 public ShipStatus.Damage DamageLevel => _status.DamageLevel;
\r
584 public int StartHp;
\r
586 public static Record[] Setup(ShipStatus[] ships) =>
\r
587 (from s in ships select new Record {_status = (ShipStatus)s.Clone(), StartHp = s.NowHp}).ToArray();
\r
589 public void ApplyDamage(int damage)
\r
591 if (_status.NowHp > damage)
\r
593 _status.NowHp -= damage;
\r
597 foreach (var item in new[] {_status.SlotEx}.Concat(_status.Slot))
\r
599 if (item.Spec.Id == 42)
\r
601 _status.NowHp = (int)(_status.MaxHp * 0.2);
\r
602 ConsumeSlotItem(_status, 42);
\r
605 if (item.Spec.Id == 43)
\r
607 _status.NowHp = _status.MaxHp;
\r
608 ConsumeSlotItem(_status, 43);
\r
614 public void UpdateShipStatus(ShipStatus ship)
\r
616 ship.NowHp = NowHp;
\r
617 ship.Slot = _status.Slot;
\r
618 ship.SlotEx = _status.SlotEx;
\r
622 private BattleResultRank CalcLdAirBattleRank()
\r
624 var combined = _friend.Concat(_guard).ToArray();
\r
625 var friendNowShips = combined.Count(r => r.NowHp > 0);
\r
626 var friendGauge = combined.Sum(r => r.StartHp - r.NowHp);
\r
627 var friendSunk = combined.Count(r => r.NowHp == 0);
\r
628 var friendGaugeRate = Floor((double)friendGauge / combined.Sum(r => r.StartHp) * 100);
\r
630 if (friendSunk == 0)
\r
632 if (friendGauge == 0)
\r
633 return BattleResultRank.P;
\r
634 if (friendGaugeRate < 10)
\r
635 return BattleResultRank.A;
\r
636 if (friendGaugeRate < 20)
\r
637 return BattleResultRank.B;
\r
638 if (friendGaugeRate < 50)
\r
639 return BattleResultRank.C;
\r
640 return BattleResultRank.D;
\r
642 if (friendSunk < friendNowShips)
\r
643 return BattleResultRank.D;
\r
644 return BattleResultRank.E;
\r
647 private BattleResultRank CalcResultRank()
\r
649 var friend = _friend.Concat(_guard).ToArray();
\r
650 var enemyHp = _enemyHp.Concat(_enemyGuardHp).ToArray();
\r
651 var enemyStartHp = _enemyStartHp.Concat(_enemyGuardStartHp).ToArray();
\r
653 var friendCount = friend.Length;
\r
654 var friendStartHpTotal = 0;
\r
655 var friendNowHpTotal = 0;
\r
656 var friendSunk = 0;
\r
657 foreach (var ship in friend)
\r
661 friendStartHpTotal += ship.StartHp;
\r
662 friendNowHpTotal += ship.NowHp;
\r
663 if (ship.NowHp == 0)
\r
666 var friendGaugeRate = (int)((double)(friendStartHpTotal - friendNowHpTotal) / friendStartHpTotal * 100);
\r
668 var enemyCount = enemyHp.Length;
\r
669 var enemyStartHpTotal = enemyStartHp.Sum();
\r
670 var enemyNowHpTotal = enemyHp.Sum();
\r
671 var enemySunk = enemyHp.Count(hp => hp == 0);
\r
672 var enemyGaugeRate = (int)((double)(enemyStartHpTotal - enemyNowHpTotal) / enemyStartHpTotal * 100);
\r
674 if (friendSunk == 0 && enemySunk == enemyCount)
\r
676 if (friendNowHpTotal >= friendStartHpTotal)
\r
677 return BattleResultRank.P;
\r
678 return BattleResultRank.S;
\r
680 if (friendSunk == 0 && enemySunk >= (int)(enemyCount * 0.7) && enemyCount > 1)
\r
681 return BattleResultRank.A;
\r
682 if (friendSunk < enemySunk && enemyHp[0] == 0)
\r
683 return BattleResultRank.B;
\r
684 if (friendCount == 1 && friend[0].DamageLevel == ShipStatus.Damage.Badly)
\r
685 return BattleResultRank.D;
\r
686 if (enemyGaugeRate > friendGaugeRate * 2.5)
\r
687 return BattleResultRank.B;
\r
688 if (enemyGaugeRate > friendGaugeRate * 0.9)
\r
689 return BattleResultRank.C;
\r
690 if (friendCount > 1 && friendCount - 1 == friendSunk)
\r
691 return BattleResultRank.E;
\r
692 return BattleResultRank.D;
\r