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 string Formation { get; private set; }
\r
64 public EnemyFighterPower EnemyFighterPower { get; private set; }
\r
65 public int AirControlLevel { get; private set; }
\r
66 public BattleResultRank ResultRank { get; private set; }
\r
67 public RankPair DisplayedResultRank { get; } = new RankPair();
\r
68 public BattleResult Result { get; set; }
\r
69 public bool EnemyIsCombined => _enemyGuard.Length > 0;
\r
70 public List<AirBattleResult> AirBattleResults { get; } = new List<AirBattleResult>();
\r
72 public class RankPair
\r
74 public char Assumed { get; set; }
\r
75 public char Actual { get; set; }
\r
76 public bool IsError => Assumed != Actual;
\r
79 public class BattleResult
\r
81 public class Combined
\r
83 public ShipStatus[] Main { get; set; }
\r
84 public ShipStatus[] Guard { get; set; }
\r
87 public Combined Friend { get; set; }
\r
88 public Combined Enemy { get; set; }
\r
91 public BattleInfo(ShipInfo shipInfo, ItemInfo itemInfo)
\r
93 _shipInfo = shipInfo;
\r
94 _itemInfo = itemInfo;
\r
97 public void InspectBattle(string url, string request, dynamic json)
\r
99 Formation = FormationName(json);
\r
100 AirControlLevel = CheckAirControlLevel(json);
\r
101 ShowResult(false); // 昼戦の結果を夜戦のときに表示する
\r
102 SetupResult(request, json);
\r
103 EnemyFighterPower = CalcEnemyFighterPower(json);
\r
104 BattleState = IsNightBattle(json) ? BattleState.Night : BattleState.Day;
\r
106 ResultRank = url.EndsWith("ld_airbattle") ? CalcLdAirBattleRank() : CalcResultRank();
\r
110 private bool IsNightBattle(dynamic json) => json.api_hougeki();
\r
112 public static int DeckId(dynamic json)
\r
114 if (json.api_dock_id()) // 昼戦はtypoしている
\r
115 return (int)json.api_dock_id - 1;
\r
116 if (json.api_deck_id is string) // 通常の夜戦と連合艦隊(味方のみ)では文字列
\r
117 return int.Parse(json.api_deck_id) - 1;
\r
118 return (int)json.api_deck_id - 1;
\r
121 private string FormationName(dynamic json)
\r
123 if (!json.api_formation()) // 演習の夜戦
\r
125 switch ((int)json.api_formation[2])
\r
139 private void SetupResult(string request, dynamic json)
\r
141 if (_friend != null)
\r
143 _shipInfo.SaveBattleStartStatus();
\r
144 _fleet = DeckId(json);
\r
145 var fstats = _shipInfo.GetShipStatuses(_fleet);
\r
146 FlagshipRecovery(request, fstats[0]);
\r
147 _friend = Record.Setup(fstats);
\r
148 _guard = json.api_f_nowhps_combined() ? Record.Setup(_shipInfo.GetShipStatuses(1)) : new Record[0];
\r
149 _enemy = Record.Setup((int[])json.api_e_nowhps,
\r
150 ((int[])json.api_ship_ke).Select(_shipInfo.GetSpec).ToArray(),
\r
151 ((int[][])json.api_eSlot).Select(slot => slot.Select(_itemInfo.GetSpecByItemId).ToArray()).ToArray());
\r
152 _enemyGuard = json.api_ship_ke_combined()
\r
153 ? Record.Setup((int[])json.api_e_nowhps_combined,
\r
154 ((int[])json.api_ship_ke_combined).Select(_shipInfo.GetSpec).ToArray(),
\r
155 ((int[][])json.api_eSlot).Select(slot => slot.Select(_itemInfo.GetSpecByItemId).ToArray())
\r
160 private void FlagshipRecovery(string request, ShipStatus flagship)
\r
162 var type = int.Parse(HttpUtility.ParseQueryString(request)["api_recovery_type"] ?? "0");
\r
168 flagship.NowHp = flagship.MaxHp / 2;
\r
169 ConsumeSlotItem(flagship, 42); // ダメコン
\r
172 flagship.NowHp = flagship.MaxHp;
\r
173 ConsumeSlotItem(flagship, 43); // 女神
\r
177 _shipInfo.SetBadlyDamagedShips();
\r
180 private static void ConsumeSlotItem(ShipStatus ship, int id)
\r
182 if (ship.SlotEx.Spec.Id == id)
\r
184 ship.SlotEx = new ItemStatus();
\r
187 for (var i = 0; i < ship.Slot.Length; i++)
\r
189 if (ship.Slot[i].Spec.Id == id)
\r
191 ship.Slot[i] = new ItemStatus();
\r
197 public void CleanupResult()
\r
203 private int CheckAirControlLevel(dynamic json)
\r
205 if (!json.api_kouku())
\r
207 var stage1 = json.api_kouku.api_stage1;
\r
208 if (stage1 == null)
\r
210 if (stage1.api_f_count == 0 && stage1.api_e_count == 0)
\r
212 return (int)stage1.api_disp_seiku;
\r
215 private EnemyFighterPower CalcEnemyFighterPower(dynamic json)
\r
217 var result = new EnemyFighterPower();
\r
218 var ships = (int[])json.api_ship_ke;
\r
219 if (json.api_ship_ke_combined() && _guard.Length > 0)
\r
220 ships = ships.Concat((int[])json.api_ship_ke_combined).ToArray();
\r
221 var maxEq = ships.SelectMany(id =>
\r
223 var r = _shipInfo.GetSpec(id).MaxEq;
\r
226 result.HasUnknown = true;
\r
229 var equips = ((int[][])json.api_eSlot).SelectMany(x => x);
\r
230 if (json.api_eSlot_combined() && _guard.Length > 0)
\r
231 equips = equips.Concat(((int[][])json.api_eSlot_combined).SelectMany(x => x));
\r
232 foreach (var entry in from slot in equips.Zip(maxEq, (id, max) => new {id, max})
\r
233 let spec = _itemInfo.GetSpecByItemId(slot.id)
\r
234 let perSlot = (int)Floor(spec.AntiAir * Sqrt(slot.max))
\r
235 select new {spec, perSlot})
\r
237 if (entry.spec.CanAirCombat)
\r
238 result.AirCombat += entry.perSlot;
\r
239 if (entry.spec.IsAircraft)
\r
240 result.Interception += entry.perSlot;
\r
245 private enum CombatType
\r
254 private class Phase
\r
256 public string Api { get; }
\r
257 public CombatType Type { get; }
\r
258 public string Name { get; }
\r
260 public Phase(string api, CombatType type, string name = "")
\r
268 private void CalcDamage(dynamic json)
\r
270 AirBattleResults.Clear();
\r
273 new Phase("air_base_injection", CombatType.Aircraft, "AB噴式"),
\r
274 new Phase("injection_kouku", CombatType.Aircraft, "噴式"),
\r
275 new Phase("air_base_attack", CombatType.AirBase),
\r
276 new Phase("n_support_info", CombatType.Support),
\r
277 new Phase("n_hougeki1", CombatType.ByTurn),
\r
278 new Phase("n_hougeki2", CombatType.ByTurn),
\r
279 new Phase("kouku", CombatType.Aircraft, "航空戦"),
\r
280 new Phase("kouku2", CombatType.Aircraft, "航空戦2"),
\r
281 new Phase("support_info", CombatType.Support),
\r
282 new Phase("opening_taisen", CombatType.ByTurn),
\r
283 new Phase("opening_atack", CombatType.AtOnce),
\r
284 new Phase("hougeki", CombatType.ByTurn),
\r
285 new Phase("hougeki1", CombatType.ByTurn),
\r
286 new Phase("hougeki2", CombatType.ByTurn),
\r
287 new Phase("hougeki3", CombatType.ByTurn),
\r
288 new Phase("raigeki", CombatType.AtOnce)
\r
290 foreach (var phase in phases)
\r
291 CalcDamageByType(json, phase);
\r
294 private void CalcDamageByType(dynamic json, Phase phase)
\r
296 var api = "api_" + phase.Api;
\r
297 if (!json.IsDefined(api) || json[api] == null)
\r
299 switch (phase.Type)
\r
301 case CombatType.AtOnce:
\r
302 CalcDamageAtOnce(json[api]);
\r
304 case CombatType.ByTurn:
\r
305 CalcDamageByTurn(json[api]);
\r
307 case CombatType.Support:
\r
308 CalcSupportDamage(json[api]);
\r
310 case CombatType.Aircraft:
\r
311 AddAirBattleResult(json[api], phase.Name);
\r
312 CalcKoukuDamage(json[api]);
\r
314 case CombatType.AirBase:
\r
315 CalcAirBaseAttackDamage(json[api]);
\r
320 private void CalcSupportDamage(dynamic json)
\r
322 if (json.api_support_hourai != null)
\r
324 CalcRawDamageAtOnce(json.api_support_hourai.api_damage, _enemy, _enemyGuard);
\r
326 else if (json.api_support_airatack != null)
\r
328 CalcRawDamageAtOnce(json.api_support_airatack.api_stage3.api_edam, _enemy, _enemyGuard);
\r
332 private void CalcAirBaseAttackDamage(dynamic json)
\r
335 foreach (var entry in json)
\r
337 AddAirBattleResult(entry, "基地" + i++);
\r
338 CalcKoukuDamage(entry);
\r
342 private void AddAirBattleResult(dynamic json, string phaseName)
\r
344 var stage1 = json.api_stage1;
\r
345 if (stage1 == null || (stage1.api_f_count == 0 && stage1.api_e_count == 0))
\r
347 AirBattleResults.Add(new AirBattleResult
\r
349 PhaseName = phaseName,
\r
350 AirControlLevel = json.api_stage1.api_disp_seiku() ? (int)json.api_stage1.api_disp_seiku : 0,
\r
351 Stage1 = new AirBattleResult.StageResult
\r
353 FriendCount = (int)json.api_stage1.api_f_count,
\r
354 FriendLost = (int)json.api_stage1.api_f_lostcount,
\r
355 EnemyCount = (int)json.api_stage1.api_e_count,
\r
356 EnemyLost = (int)json.api_stage1.api_e_lostcount
\r
358 Stage2 = json.api_stage2 == null
\r
359 ? new AirBattleResult.StageResult
\r
366 : new AirBattleResult.StageResult
\r
368 FriendCount = (int)json.api_stage2.api_f_count,
\r
369 FriendLost = (int)json.api_stage2.api_f_lostcount,
\r
370 EnemyCount = (int)json.api_stage2.api_e_count,
\r
371 EnemyLost = (int)json.api_stage2.api_e_lostcount
\r
376 private void CalcKoukuDamage(dynamic json)
\r
378 if (json.api_stage3() && json.api_stage3 != null)
\r
379 CalcDamageAtOnce(json.api_stage3, _friend, _enemy);
\r
380 if (json.api_stage3_combined() && json.api_stage3_combined != null)
\r
381 CalcDamageAtOnce(json.api_stage3_combined, _guard, _enemyGuard);
\r
384 private void CalcDamageAtOnce(dynamic json)
\r
386 CalcDamageAtOnce(json, _friend, _guard, _enemy, _enemyGuard);
\r
389 private void CalcDamageAtOnce(dynamic json, Record[] friend, Record[] enemy)
\r
391 CalcDamageAtOnce(json, friend, null, enemy, null);
\r
394 private void CalcDamageAtOnce(dynamic json, Record[] friend, Record[] guard, Record[] enemy, Record[] enemyGuard)
\r
396 if (json.api_fdam() && json.api_fdam != null)
\r
397 CalcRawDamageAtOnce(json.api_fdam, friend, guard);
\r
398 if (json.api_edam() && json.api_edam != null)
\r
399 CalcRawDamageAtOnce(json.api_edam, enemy, enemyGuard);
\r
402 private void CalcRawDamageAtOnce(dynamic rawDamage, Record[] friend, Record[] guard = null)
\r
404 var damage = (int[])rawDamage;
\r
405 for (var i = 0; i < friend.Length; i++)
\r
406 friend[i].ApplyDamage(damage[i]);
\r
409 for (var i = 0; i < guard.Length; i++)
\r
410 guard[i].ApplyDamage(damage[i + 6]);
\r
413 private void CalcDamageByTurn(dynamic json)
\r
415 if (!(json.api_df_list() && json.api_df_list != null &&
\r
416 json.api_damage() && json.api_damage != null &&
\r
417 json.api_at_eflag() && json.api_at_eflag != null))
\r
420 var targets = (int[][])json.api_df_list;
\r
421 var damages = (int[][])json.api_damage;
\r
422 var eflags = (int[])json.api_at_eflag;
\r
423 var records = new[] {new Record[12], new Record[12]};
\r
424 Array.Copy(_friend, records[1], _friend.Length);
\r
425 Array.Copy(_guard, 0, records[1], 6, _guard.Length);
\r
426 Array.Copy(_enemy, records[0], _enemy.Length);
\r
427 Array.Copy(_enemyGuard, 0, records[0], 6, _enemyGuard.Length);
\r
428 for (var i = 0; i < eflags.Length; i++)
\r
430 // 一度に複数の目標を狙う攻撃はないものと仮定する
\r
431 var hit = new {t = targets[i][0], d = damages[i].Sum(d => d >= 0 ? d : 0)};
\r
434 records[eflags[i]][hit.t].ApplyDamage(hit.d);
\r
438 public void InspectMapStart(dynamic json)
\r
440 InspectMapNext(json);
\r
443 public void InspectMapNext(dynamic json)
\r
445 _lastCell = (int)json.api_next == 0;
\r
448 public void InspectBattleResult(dynamic json)
\r
450 BattleState = BattleState.Result;
\r
451 ShowResult(!_lastCell);
\r
452 _shipInfo.SaveBattleResult();
\r
453 VerifyResultRank(json);
\r
455 SetEscapeShips(json);
\r
459 private void VerifyResultRank(dynamic json)
\r
461 if (_friend == null)
\r
463 if (!json.api_win_rank())
\r
465 var assumed = "PSABCDE"[(int)ResultRank];
\r
466 if (assumed == 'P')
\r
468 var actual = ((string)json.api_win_rank)[0];
\r
469 DisplayedResultRank.Assumed = assumed;
\r
470 DisplayedResultRank.Actual = actual;
\r
473 public void InspectPracticeResult(dynamic json)
\r
475 BattleState = BattleState.Result;
\r
480 private void ShowResult(bool warnDamagedShip = true)
\r
482 if (_friend == null)
\r
484 var ships = _guard.Length > 0
\r
485 ? _shipInfo.GetShipStatuses(0).Concat(_shipInfo.GetShipStatuses(1)).ToArray()
\r
486 : _shipInfo.GetShipStatuses(_fleet);
\r
487 foreach (var entry in ships.Zip(_friend.Concat(_guard), (ship, now) => new {ship, now}))
\r
488 entry.now.UpdateShipStatus(entry.ship);
\r
489 if (warnDamagedShip)
\r
490 _shipInfo.SetBadlyDamagedShips();
\r
492 _shipInfo.ClearBadlyDamagedShips();
\r
495 private void SetResult()
\r
497 Result = new BattleResult
\r
499 Friend = new BattleResult.Combined
\r
501 Main = _friend.Select(r => r.SnapShot).ToArray(),
\r
502 Guard = _guard.Select(r => r.SnapShot).ToArray()
\r
504 Enemy = new BattleResult.Combined
\r
506 Main = _enemy.Select(r => r.SnapShot).ToArray(),
\r
507 Guard = _enemyGuard.Select(r => r.SnapShot).ToArray()
\r
512 public void SetEscapeShips(dynamic json)
\r
514 _escapingShips.Clear();
\r
515 if (!json.api_escape_flag() || (int)json.api_escape_flag == 0)
\r
517 var damaged = (int)json.api_escape.api_escape_idx[0] - 1;
\r
518 if (json.api_escape.api_tow_idx())
\r
520 _escapingShips.Add(_shipInfo.GetDeck(damaged / 6)[damaged % 6]);
\r
521 var escort = (int)json.api_escape.api_tow_idx[0] - 1;
\r
522 _escapingShips.Add(_shipInfo.GetDeck(escort / 6)[escort % 6]);
\r
526 _escapingShips.Add(_shipInfo.GetDeck(2)[damaged]);
\r
530 public void CauseEscape()
\r
532 _shipInfo.SetEscapedShips(_escapingShips);
\r
533 _shipInfo.SetBadlyDamagedShips();
\r
536 private class Record
\r
538 private ShipStatus _status;
\r
539 public ShipStatus SnapShot => (ShipStatus)_status.Clone();
\r
540 public int NowHp => _status.NowHp;
\r
541 public bool Escaped => _status.Escaped;
\r
542 public ShipStatus.Damage DamageLevel => _status.DamageLevel;
\r
543 public int StartHp;
\r
545 public static Record[] Setup(ShipStatus[] ships) =>
\r
546 (from s in ships select new Record {_status = (ShipStatus)s.Clone(), StartHp = s.NowHp}).ToArray();
\r
548 public static Record[] Setup(int[] nowhps, ShipSpec[] ships, ItemSpec[][] slots)
\r
550 return Enumerable.Range(0, nowhps.Length).Select(i =>
\r
553 StartHp = nowhps[i],
\r
554 _status = new ShipStatus
\r
560 Slot = slots[i].Select(spec => new ItemStatus{Id = spec.Id, Spec = spec}).ToArray(),
\r
561 SlotEx = new ItemStatus(0)
\r
566 public void ApplyDamage(int damage)
\r
568 if (_status.NowHp > damage)
\r
570 _status.NowHp -= damage;
\r
574 foreach (var item in _status.AllSlot)
\r
576 if (item.Spec.Id == 42)
\r
578 _status.NowHp = (int)(_status.MaxHp * 0.2);
\r
579 ConsumeSlotItem(_status, 42);
\r
582 if (item.Spec.Id == 43)
\r
584 _status.NowHp = _status.MaxHp;
\r
585 ConsumeSlotItem(_status, 43);
\r
591 public void UpdateShipStatus(ShipStatus ship)
\r
593 ship.NowHp = NowHp;
\r
594 ship.Slot = _status.Slot;
\r
595 ship.SlotEx = _status.SlotEx;
\r
599 private BattleResultRank CalcLdAirBattleRank()
\r
601 var combined = _friend.Concat(_guard).ToArray();
\r
602 var friendNowShips = combined.Count(r => r.NowHp > 0);
\r
603 var friendGauge = combined.Sum(r => r.StartHp - r.NowHp);
\r
604 var friendSunk = combined.Count(r => r.NowHp == 0);
\r
605 var friendGaugeRate = Floor((double)friendGauge / combined.Sum(r => r.StartHp) * 100);
\r
607 if (friendSunk == 0)
\r
609 if (friendGauge == 0)
\r
610 return BattleResultRank.P;
\r
611 if (friendGaugeRate < 10)
\r
612 return BattleResultRank.A;
\r
613 if (friendGaugeRate < 20)
\r
614 return BattleResultRank.B;
\r
615 if (friendGaugeRate < 50)
\r
616 return BattleResultRank.C;
\r
617 return BattleResultRank.D;
\r
619 if (friendSunk < friendNowShips)
\r
620 return BattleResultRank.D;
\r
621 return BattleResultRank.E;
\r
624 private BattleResultRank CalcResultRank()
\r
626 var friend = _friend.Concat(_guard).ToArray();
\r
627 var enemy = _enemy.Concat(_enemyGuard).ToArray();
\r
629 var friendCount = friend.Length;
\r
630 var friendStartHpTotal = 0;
\r
631 var friendNowHpTotal = 0;
\r
632 var friendSunk = 0;
\r
633 foreach (var ship in friend)
\r
637 friendStartHpTotal += ship.StartHp;
\r
638 friendNowHpTotal += ship.NowHp;
\r
639 if (ship.NowHp == 0)
\r
642 var friendGaugeRate = (int)((double)(friendStartHpTotal - friendNowHpTotal) / friendStartHpTotal * 100);
\r
644 var enemyCount = enemy.Length;
\r
645 var enemyStartHpTotal = enemy.Sum(r => r.StartHp);
\r
646 var enemyNowHpTotal = enemy.Sum(r => r.NowHp);
\r
647 var enemySunk = enemy.Count(r => r.NowHp == 0);
\r
648 var enemyGaugeRate = (int)((double)(enemyStartHpTotal - enemyNowHpTotal) / enemyStartHpTotal * 100);
\r
650 if (friendSunk == 0 && enemySunk == enemyCount)
\r
652 if (friendNowHpTotal >= friendStartHpTotal)
\r
653 return BattleResultRank.P;
\r
654 return BattleResultRank.S;
\r
656 if (friendSunk == 0 && enemySunk >= (int)(enemyCount * 0.7) && enemyCount > 1)
\r
657 return BattleResultRank.A;
\r
658 if (friendSunk < enemySunk && enemy[0].NowHp == 0)
\r
659 return BattleResultRank.B;
\r
660 if (friendCount == 1 && friend[0].DamageLevel == ShipStatus.Damage.Badly)
\r
661 return BattleResultRank.D;
\r
662 if (enemyGaugeRate > friendGaugeRate * 2.5)
\r
663 return BattleResultRank.B;
\r
664 if (enemyGaugeRate > friendGaugeRate * 0.9)
\r
665 return BattleResultRank.C;
\r
666 if (friendCount > 1 && friendCount - 1 == friendSunk)
\r
667 return BattleResultRank.E;
\r
668 return BattleResultRank.D;
\r
674 public void InjectEnemyResultStatus(ShipStatus[] enemy, ShipStatus[] guard)
\r
676 Result = new BattleResult {Enemy = new BattleResult.Combined {Main = enemy, Guard = guard}};
\r