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 int[] Formation { get; private set; }
\r
64 public int[] FighterPower { 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 BattleResult Result { get; set; }
\r
70 public bool EnemyIsCombined => _enemyGuard.Length > 0;
\r
71 public List<AirBattleResult> AirBattleResults { get; } = new List<AirBattleResult>();
\r
73 public class RankPair
\r
75 public char Assumed { get; set; }
\r
76 public char Actual { get; set; }
\r
77 public bool IsError => Assumed != Actual;
\r
80 public class BattleResult
\r
82 public class Combined
\r
84 public ShipStatus[] Main { get; set; }
\r
85 public ShipStatus[] Guard { get; set; }
\r
88 public Combined Friend { get; set; }
\r
89 public Combined Enemy { get; set; }
\r
92 public BattleInfo(ShipInfo shipInfo, ItemInfo itemInfo)
\r
94 _shipInfo = shipInfo;
\r
95 _itemInfo = itemInfo;
\r
98 public void InspectBattle(string url, string request, dynamic json)
\r
100 if (json.api_formation())
\r
101 Formation = ((dynamic[])json.api_formation).Select(f => f is string ? (int)int.Parse(f) : (int)f)
\r
103 AirControlLevel = CheckAirControlLevel(json);
\r
104 ShowResult(false); // 昼戦の結果を夜戦のときに表示する
\r
105 SetupResult(request, json, url.Contains("practice"));
\r
106 FighterPower = CalcFighterPower();
\r
107 EnemyFighterPower = CalcEnemyFighterPower(json);
\r
108 BattleState = IsNightBattle(json) ? BattleState.Night : BattleState.Day;
\r
110 ResultRank = url.EndsWith("ld_airbattle") ? CalcLdAirBattleRank() : CalcResultRank();
\r
114 private bool IsNightBattle(dynamic json) => json.api_hougeki();
\r
116 public static int DeckId(dynamic json)
\r
118 if (json.api_dock_id()) // 昼戦はtypoしている
\r
119 return (int)json.api_dock_id - 1;
\r
120 if (json.api_deck_id is string) // 通常の夜戦と連合艦隊(味方のみ)では文字列
\r
121 return int.Parse(json.api_deck_id) - 1;
\r
122 return (int)json.api_deck_id - 1;
\r
125 private void SetupResult(string request, dynamic json, bool practice)
\r
127 if (_friend != null)
\r
129 _shipInfo.SaveBattleStartStatus();
\r
130 _fleet = DeckId(json);
\r
131 var fstats = _shipInfo.GetShipStatuses(_fleet);
\r
132 FlagshipRecovery(request, fstats[0]);
\r
133 _friend = Record.Setup(fstats, practice);
\r
134 _guard = json.api_f_nowhps_combined()
\r
135 ? Record.Setup(_shipInfo.GetShipStatuses(1), practice)
\r
137 _enemy = Record.Setup((int[])json.api_e_nowhps,
\r
138 ((int[])json.api_ship_ke).Select(_shipInfo.GetSpec).ToArray(),
\r
139 ((int[][])json.api_eSlot).Select(slot => slot.Select(_itemInfo.GetSpecByItemId).ToArray()).ToArray(),
\r
141 _enemyGuard = json.api_ship_ke_combined()
\r
142 ? Record.Setup((int[])json.api_e_nowhps_combined,
\r
143 ((int[])json.api_ship_ke_combined).Select(_shipInfo.GetSpec).ToArray(),
\r
144 ((int[][])json.api_eSlot).Select(slot => slot.Select(_itemInfo.GetSpecByItemId).ToArray())
\r
145 .ToArray(), practice)
\r
149 private void SetResult()
\r
151 Result = new BattleResult
\r
153 Friend = new BattleResult.Combined
\r
155 Main = _friend.Select(r => r.SnapShot).ToArray(),
\r
156 Guard = _guard.Select(r => r.SnapShot).ToArray()
\r
158 Enemy = new BattleResult.Combined
\r
160 Main = _enemy.Select(r => r.SnapShot).ToArray(),
\r
161 Guard = _enemyGuard.Select(r => r.SnapShot).ToArray()
\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 int[] CalcFighterPower()
\r
223 if (_guard.Length > 0 && _enemyGuard.Length > 0)
\r
224 return _shipInfo.GetFighterPower(0).Zip(_shipInfo.GetFighterPower(1), (a, b) => a + b).ToArray();
\r
225 return _shipInfo.GetFighterPower(_fleet);
\r
228 private EnemyFighterPower CalcEnemyFighterPower(dynamic json)
\r
230 var result = new EnemyFighterPower();
\r
231 var ships = (int[])json.api_ship_ke;
\r
232 if (json.api_ship_ke_combined() && _guard.Length > 0)
\r
233 ships = ships.Concat((int[])json.api_ship_ke_combined).ToArray();
\r
234 var maxEq = ships.SelectMany(id =>
\r
236 var r = _shipInfo.GetSpec(id).MaxEq;
\r
239 result.HasUnknown = true;
\r
242 var equips = ((int[][])json.api_eSlot).SelectMany(x => x);
\r
243 if (json.api_eSlot_combined() && _guard.Length > 0)
\r
244 equips = equips.Concat(((int[][])json.api_eSlot_combined).SelectMany(x => x));
\r
245 foreach (var entry in from slot in equips.Zip(maxEq, (id, max) => new {id, max})
\r
246 let spec = _itemInfo.GetSpecByItemId(slot.id)
\r
247 let perSlot = (int)Floor(spec.AntiAir * Sqrt(slot.max))
\r
248 select new {spec, perSlot})
\r
250 if (entry.spec.CanAirCombat)
\r
251 result.AirCombat += entry.perSlot;
\r
252 if (entry.spec.IsAircraft)
\r
253 result.Interception += entry.perSlot;
\r
258 private enum CombatType
\r
267 private class Phase
\r
269 public string Api { get; }
\r
270 public CombatType Type { get; }
\r
271 public string Name { get; }
\r
273 public Phase(string api, CombatType type, string name = "")
\r
281 private void CalcDamage(dynamic json)
\r
283 AirBattleResults.Clear();
\r
286 new Phase("air_base_injection", CombatType.Aircraft, "AB噴式"),
\r
287 new Phase("injection_kouku", CombatType.Aircraft, "噴式"),
\r
288 new Phase("air_base_attack", CombatType.AirBase),
\r
289 new Phase("n_support_info", CombatType.Support),
\r
290 new Phase("n_hougeki1", CombatType.ByTurn),
\r
291 new Phase("n_hougeki2", CombatType.ByTurn),
\r
292 new Phase("kouku", CombatType.Aircraft, "航空戦"),
\r
293 new Phase("kouku2", CombatType.Aircraft, "航空戦2"),
\r
294 new Phase("support_info", CombatType.Support),
\r
295 new Phase("opening_taisen", CombatType.ByTurn),
\r
296 new Phase("opening_atack", CombatType.AtOnce),
\r
297 new Phase("hougeki", CombatType.ByTurn),
\r
298 new Phase("hougeki1", CombatType.ByTurn),
\r
299 new Phase("hougeki2", CombatType.ByTurn),
\r
300 new Phase("hougeki3", CombatType.ByTurn),
\r
301 new Phase("raigeki", CombatType.AtOnce)
\r
303 foreach (var phase in phases)
\r
304 CalcDamageByType(json, phase);
\r
307 private void CalcDamageByType(dynamic json, Phase phase)
\r
309 var api = "api_" + phase.Api;
\r
310 if (!json.IsDefined(api) || json[api] == null)
\r
312 switch (phase.Type)
\r
314 case CombatType.AtOnce:
\r
315 CalcDamageAtOnce(json[api]);
\r
317 case CombatType.ByTurn:
\r
318 CalcDamageByTurn(json[api]);
\r
320 case CombatType.Support:
\r
321 CalcSupportDamage(json[api]);
\r
323 case CombatType.Aircraft:
\r
324 AddAirBattleResult(json[api], phase.Name);
\r
325 CalcKoukuDamage(json[api]);
\r
327 case CombatType.AirBase:
\r
328 CalcAirBaseAttackDamage(json[api]);
\r
333 private void CalcSupportDamage(dynamic json)
\r
335 if (json.api_support_hourai != null)
\r
337 CalcRawDamageAtOnce(json.api_support_hourai.api_damage, _enemy, _enemyGuard);
\r
339 else if (json.api_support_airatack != null)
\r
341 CalcRawDamageAtOnce(json.api_support_airatack.api_stage3.api_edam, _enemy, _enemyGuard);
\r
345 private void CalcAirBaseAttackDamage(dynamic json)
\r
348 foreach (var entry in json)
\r
350 AddAirBattleResult(entry, "基地" + i++);
\r
351 CalcKoukuDamage(entry);
\r
355 private void AddAirBattleResult(dynamic json, string phaseName)
\r
357 var stage1 = json.api_stage1;
\r
358 if (stage1 == null || (stage1.api_f_count == 0 && stage1.api_e_count == 0))
\r
360 var result = new AirBattleResult
\r
362 PhaseName = phaseName,
\r
363 AirControlLevel = json.api_stage1.api_disp_seiku() ? (int)json.api_stage1.api_disp_seiku : 0,
\r
364 Stage1 = new AirBattleResult.StageResult
\r
366 FriendCount = (int)json.api_stage1.api_f_count,
\r
367 FriendLost = (int)json.api_stage1.api_f_lostcount,
\r
368 EnemyCount = (int)json.api_stage1.api_e_count,
\r
369 EnemyLost = (int)json.api_stage1.api_e_lostcount
\r
371 Stage2 = json.api_stage2 == null
\r
372 ? new AirBattleResult.StageResult
\r
379 : new AirBattleResult.StageResult
\r
381 FriendCount = (int)json.api_stage2.api_f_count,
\r
382 FriendLost = (int)json.api_stage2.api_f_lostcount,
\r
383 EnemyCount = (int)json.api_stage2.api_e_count,
\r
384 EnemyLost = (int)json.api_stage2.api_e_lostcount
\r
387 if (json.api_stage2 != null && json.api_stage2.api_air_fire())
\r
389 var airfire = json.api_stage2.api_air_fire;
\r
390 var idx = (int)airfire.api_idx;
\r
391 result.AirFire = new AirBattleResult.AirFireResult
\r
393 ShipName = idx < _friend.Length ? _friend[idx].Name : _guard[idx - 6].Name,
\r
394 Kind = (int)airfire.api_kind,
\r
395 Items = ((int[])airfire.api_use_items).Select(id => _itemInfo.GetSpecByItemId(id).Name).ToArray()
\r
398 AirBattleResults.Add(result);
\r
401 private void CalcKoukuDamage(dynamic json)
\r
403 if (json.api_stage3() && json.api_stage3 != null)
\r
404 CalcDamageAtOnce(json.api_stage3, _friend, _enemy);
\r
405 if (json.api_stage3_combined() && json.api_stage3_combined != null)
\r
406 CalcDamageAtOnce(json.api_stage3_combined, _guard, _enemyGuard);
\r
409 private void CalcDamageAtOnce(dynamic json)
\r
411 CalcDamageAtOnce(json, _friend, _guard, _enemy, _enemyGuard);
\r
414 private void CalcDamageAtOnce(dynamic json, Record[] friend, Record[] enemy)
\r
416 CalcDamageAtOnce(json, friend, null, enemy, null);
\r
419 private void CalcDamageAtOnce(dynamic json,
\r
420 Record[] friend, Record[] guard, Record[] enemy, Record[] enemyGuard)
\r
422 if (json.api_fdam() && json.api_fdam != null)
\r
423 CalcRawDamageAtOnce(json.api_fdam, friend, guard);
\r
424 if (json.api_edam() && json.api_edam != null)
\r
425 CalcRawDamageAtOnce(json.api_edam, enemy, enemyGuard);
\r
428 private void CalcRawDamageAtOnce(dynamic rawDamage, Record[] friend, Record[] guard = null)
\r
430 var damage = (int[])rawDamage;
\r
431 for (var i = 0; i < friend.Length; i++)
\r
432 friend[i].ApplyDamage(damage[i]);
\r
435 for (var i = 0; i < guard.Length; i++)
\r
436 guard[i].ApplyDamage(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 = (int[][])json.api_df_list;
\r
447 var damages = (int[][])json.api_damage;
\r
448 var eflags = (int[])json.api_at_eflag;
\r
449 var records = new[] {new Record[12], new Record[12]};
\r
450 Array.Copy(_friend, records[1], _friend.Length);
\r
451 Array.Copy(_guard, 0, records[1], 6, _guard.Length);
\r
452 Array.Copy(_enemy, records[0], _enemy.Length);
\r
453 Array.Copy(_enemyGuard, 0, records[0], 6, _enemyGuard.Length);
\r
454 for (var i = 0; i < eflags.Length; i++)
\r
456 // 一度に複数の目標を狙う攻撃はないものと仮定する
\r
457 var hit = new {t = targets[i][0], d = damages[i].Sum(d => d >= 0 ? d : 0)};
\r
460 records[eflags[i]][hit.t].ApplyDamage(hit.d);
\r
464 public void InspectMapStart(dynamic json)
\r
466 InspectMapNext(json);
\r
469 public void InspectMapNext(dynamic json)
\r
471 _lastCell = (int)json.api_next == 0;
\r
474 public void InspectBattleResult(dynamic json)
\r
476 BattleState = BattleState.Result;
\r
477 ShowResult(!_lastCell);
\r
478 _shipInfo.SaveBattleResult();
\r
479 VerifyResultRank(json);
\r
481 SetEscapeShips(json);
\r
484 private void VerifyResultRank(dynamic json)
\r
486 if (_friend == null)
\r
488 if (!json.api_win_rank())
\r
490 var assumed = "PSABCDE"[(int)ResultRank];
\r
491 if (assumed == 'P')
\r
493 var actual = ((string)json.api_win_rank)[0];
\r
494 DisplayedResultRank.Assumed = assumed;
\r
495 DisplayedResultRank.Actual = actual;
\r
498 public void InspectPracticeResult(dynamic json)
\r
500 BattleState = BattleState.Result;
\r
502 VerifyResultRank(json);
\r
506 private void ShowResult(bool warnDamagedShip = true)
\r
508 if (_friend == null)
\r
510 var ships = _guard.Length > 0
\r
511 ? _shipInfo.GetShipStatuses(0).Concat(_shipInfo.GetShipStatuses(1)).ToArray()
\r
512 : _shipInfo.GetShipStatuses(_fleet);
\r
513 foreach (var entry in ships.Zip(_friend.Concat(_guard), (ship, now) => new {ship, now}))
\r
514 entry.now.UpdateShipStatus(entry.ship);
\r
515 if (warnDamagedShip)
\r
516 _shipInfo.SetBadlyDamagedShips();
\r
518 _shipInfo.ClearBadlyDamagedShips();
\r
521 public void SetEscapeShips(dynamic json)
\r
523 _escapingShips.Clear();
\r
524 if (!json.api_escape_flag() || (int)json.api_escape_flag == 0)
\r
526 var damaged = (int)json.api_escape.api_escape_idx[0] - 1;
\r
527 if (json.api_escape.api_tow_idx())
\r
529 _escapingShips.Add(_shipInfo.GetDeck(damaged / 6)[damaged % 6]);
\r
530 var escort = (int)json.api_escape.api_tow_idx[0] - 1;
\r
531 _escapingShips.Add(_shipInfo.GetDeck(escort / 6)[escort % 6]);
\r
535 _escapingShips.Add(_shipInfo.GetDeck(2)[damaged]);
\r
539 public void CauseEscape()
\r
541 _shipInfo.SetEscapedShips(_escapingShips);
\r
542 _shipInfo.SetBadlyDamagedShips();
\r
545 private class Record
\r
547 private ShipStatus _status;
\r
548 private bool _practice;
\r
549 public ShipStatus SnapShot => (ShipStatus)_status.Clone();
\r
550 public int NowHp => _status.NowHp;
\r
551 public bool Escaped => _status.Escaped;
\r
552 public ShipStatus.Damage DamageLevel => _status.DamageLevel;
\r
553 public string Name => _status.Name;
\r
554 public int StartHp { get; private set; }
\r
556 public static Record[] Setup(ShipStatus[] ships, bool practice) =>
\r
558 select new Record {_status = (ShipStatus)s.Clone(), _practice = practice, StartHp = s.NowHp}).ToArray();
\r
560 public static Record[] Setup(int[] nowhps, ShipSpec[] ships, ItemSpec[][] slots, bool practice)
\r
562 return Enumerable.Range(0, nowhps.Length).Select(i =>
\r
565 StartHp = nowhps[i],
\r
566 _status = new ShipStatus
\r
572 Slot = slots[i].Select(spec => new ItemStatus {Id = spec.Id, Spec = spec}).ToArray(),
\r
573 SlotEx = new ItemStatus(0)
\r
575 _practice = practice
\r
579 public void ApplyDamage(int damage)
\r
581 if (_status.NowHp > damage)
\r
583 _status.NowHp -= damage;
\r
589 foreach (var item in new[] {_status.SlotEx}.Concat(_status.Slot))
\r
591 if (item.Spec.Id == 42)
\r
593 _status.NowHp = (int)(_status.MaxHp * 0.2);
\r
594 ConsumeSlotItem(_status, 42);
\r
597 if (item.Spec.Id == 43)
\r
599 _status.NowHp = _status.MaxHp;
\r
600 ConsumeSlotItem(_status, 43);
\r
606 public void UpdateShipStatus(ShipStatus ship)
\r
608 ship.NowHp = NowHp;
\r
609 ship.Slot = _status.Slot;
\r
610 ship.SlotEx = _status.SlotEx;
\r
614 private BattleResultRank CalcLdAirBattleRank()
\r
616 var combined = _friend.Concat(_guard).ToArray();
\r
617 var friendNowShips = combined.Count(r => r.NowHp > 0);
\r
618 var friendGauge = combined.Sum(r => r.StartHp - r.NowHp);
\r
619 var friendSunk = combined.Count(r => r.NowHp == 0);
\r
620 var friendGaugeRate = Floor((double)friendGauge / combined.Sum(r => r.StartHp) * 100);
\r
622 if (friendSunk == 0)
\r
624 if (friendGauge == 0)
\r
625 return BattleResultRank.P;
\r
626 if (friendGaugeRate < 10)
\r
627 return BattleResultRank.A;
\r
628 if (friendGaugeRate < 20)
\r
629 return BattleResultRank.B;
\r
630 if (friendGaugeRate < 50)
\r
631 return BattleResultRank.C;
\r
632 return BattleResultRank.D;
\r
634 if (friendSunk < friendNowShips)
\r
635 return BattleResultRank.D;
\r
636 return BattleResultRank.E;
\r
639 private BattleResultRank CalcResultRank()
\r
641 var friend = _friend.Concat(_guard).ToArray();
\r
642 var enemy = _enemy.Concat(_enemyGuard).ToArray();
\r
644 var friendCount = friend.Length;
\r
645 var friendStartHpTotal = 0;
\r
646 var friendNowHpTotal = 0;
\r
647 var friendSunk = 0;
\r
648 foreach (var ship in friend)
\r
652 friendStartHpTotal += ship.StartHp;
\r
653 friendNowHpTotal += ship.NowHp;
\r
654 if (ship.NowHp == 0)
\r
657 var friendGaugeRate = (int)((double)(friendStartHpTotal - friendNowHpTotal) / friendStartHpTotal * 100);
\r
659 var enemyCount = enemy.Length;
\r
660 var enemyStartHpTotal = enemy.Sum(r => r.StartHp);
\r
661 var enemyNowHpTotal = enemy.Sum(r => r.NowHp);
\r
662 var enemySunk = enemy.Count(r => r.NowHp == 0);
\r
663 var enemyGaugeRate = (int)((double)(enemyStartHpTotal - enemyNowHpTotal) / enemyStartHpTotal * 100);
\r
665 if (friendSunk == 0 && enemySunk == enemyCount)
\r
667 if (friendNowHpTotal >= friendStartHpTotal)
\r
668 return BattleResultRank.P;
\r
669 return BattleResultRank.S;
\r
671 if (friendSunk == 0 && enemySunk >= (int)(enemyCount * 0.7) && enemyCount > 1)
\r
672 return BattleResultRank.A;
\r
673 if (friendSunk < enemySunk && enemy[0].NowHp == 0)
\r
674 return BattleResultRank.B;
\r
675 if (friendCount == 1 && friend[0].DamageLevel == ShipStatus.Damage.Badly)
\r
676 return BattleResultRank.D;
\r
677 if (enemyGaugeRate > friendGaugeRate * 2.5)
\r
678 return BattleResultRank.B;
\r
679 if (enemyGaugeRate > friendGaugeRate * 0.9)
\r
680 return BattleResultRank.C;
\r
681 if (friendCount > 1 && friendCount - 1 == friendSunk)
\r
682 return BattleResultRank.E;
\r
683 return BattleResultRank.D;
\r
689 public void InjectEnemyResultStatus(ShipStatus[] enemy, ShipStatus[] guard)
\r
691 Result = new BattleResult {Enemy = new BattleResult.Combined {Main = enemy, Guard = guard}};
\r