OSDN Git Service

staticプロパティをインスタンスプロパティに変更
[kcd/KCD.git] / KCD / DamageCalculator.swift
1 //
2 //  DamageCalculator.swift
3 //  KCD
4 //
5 //  Created by Hori,Masaki on 2017/07/23.
6 //  Copyright © 2017年 Hori,Masaki. All rights reserved.
7 //
8
9 import Cocoa
10 import SwiftyJSON
11
12 enum BattleFleet {
13     
14     case normal
15     
16     case secondOnly
17 }
18
19 private struct PositionedDamage {
20     
21     let position: Int
22     let damage: Int
23     
24     static let zero = PositionedDamage(position: 0, damage: 0)
25 }
26
27 extension PositionedDamage: Equatable {
28     
29     static func == (lhs: PositionedDamage, rhs: PositionedDamage) -> Bool {
30         
31         return lhs.position == rhs.position && lhs.damage == rhs.damage
32     }
33 }
34
35 private struct HogekiBattleData {
36     
37     let targetPositionList: [Int]
38     let damageList: [Int]
39     let enemyFlags: Bool
40 }
41
42 private func friendDamage(_ data: HogekiBattleData) -> PositionedDamage {
43     
44     guard !data.enemyFlags else { return .zero }
45     
46     guard let pos = data.targetPositionList.first else { return .zero }
47     
48     return PositionedDamage(position: pos,
49                             damage: data.damageList.filter({ $0 > 0 }).reduce(0, +))
50 }
51
52 final class DamageCalculator {
53     
54     private let store = TemporaryDataStore.oneTimeEditor()
55     
56     private let json: JSON
57     private let battleType: BattleType
58     
59     init(_ json: JSON, _ battleType: BattleType = .normal) {
60         
61         self.battleType = battleType
62         self.json = json
63     }
64 }
65
66 // MARK: - Battle type
67 extension DamageCalculator {
68     
69     func calculateBattle() {
70         
71         store.sync {
72             
73             self.calcKouku()
74             self.calcOpeningTaisen()
75             self.calcOpeningAttack()
76             self.calcHougeki1()
77             self.calcHougeki2()
78             self.calcHougeki3()
79             self.calcRaigeki()
80         }
81     }
82     
83     func calcCombinedBattleAir() {
84         
85         store.sync {
86             
87             self.calcKouku()
88             self.calcOpeningTaisen()
89             self.calcOpeningAttack()
90             self.calcHougeki1()
91             self.calcRaigeki()
92             self.calcHougeki2()
93             self.calcHougeki3()
94         }
95     }
96     
97     func calcEachBattleAir() {
98         
99         store.sync {
100             
101             self.calcKouku()
102             self.calcOpeningTaisen()
103             self.calcOpeningAttack()
104             self.calcHougeki1()
105             self.calcHougeki2()
106             self.calcRaigeki()
107             self.calcHougeki3()
108         }
109     }
110     
111     func calcEachNightToDay() {
112         
113         store.sync {
114             
115             self.calcNightHogeki1()
116             self.calcNightHogeki2()
117             self.calcKouku()
118             self.calcOpeningTaisen()
119             self.calcOpeningAttack()
120             self.calcHougeki1()
121             self.calcHougeki2()
122             self.calcRaigeki()
123             self.calcHougeki3()
124         }
125     }
126     
127     func calcEnemyCombinedBattle() {
128         
129         // same phase as combined air
130         calcCombinedBattleAir()
131     }
132     
133     func calcMidnight() {
134         
135         store.sync {
136             
137             self.calculateMidnightBattle()
138         }
139     }
140 }
141
142 // MARK: - Battle phase
143 extension DamageCalculator {
144     
145     private func calcKouku() {
146         
147         calculateFDam(baseKeyPath: "api_data.api_kouku.api_stage3")
148         calculateFDam(baseKeyPath: "api_data.api_kouku2.api_stage3")
149         
150         let bf: () -> BattleFleet = {
151             
152             switch self.battleType {
153                 
154             case .combinedWater,
155                  .combinedAir,
156                  .eachCombinedWater,
157                  .eachCombinedAir:
158                 
159                 return .secondOnly
160                 
161             default:
162                 
163                 return .normal
164                 
165             }
166         }
167         calculateFDam(baseKeyPath: "api_data.api_kouku.api_stage3_combined", bf)
168         calculateFDam(baseKeyPath: "api_data.api_kouku2.api_stage3_combined", bf)
169         
170     }
171     
172     private func calcOpeningAttack() {
173         
174         calculateFDam(baseKeyPath: "api_data.api_opening_atack")
175     }
176     
177     private func calcOpeningTaisen() {
178         
179         calculateHogeki(baseKeyPath: "api_data.api_opening_taisen")
180     }
181     
182     private func calcHougeki1() {
183         
184         calculateHogeki(baseKeyPath: "api_data.api_hougeki1")
185     }
186     
187     private func calcHougeki2() {
188         
189         calculateHogeki(baseKeyPath: "api_data.api_hougeki2")
190     }
191     
192     private func calcHougeki3() {
193         
194         calculateHogeki(baseKeyPath: "api_data.api_hougeki3")
195     }
196     
197     private func calcNightHogeki1() {
198         
199         calculateHogeki(baseKeyPath: "api_data.api_n_hougeki1")
200     }
201     
202     private func calcNightHogeki2() {
203         
204         calculateHogeki(baseKeyPath: "api_data.api_n_hougeki2")
205     }
206     
207     private func calcRaigeki() {
208         
209         calculateFDam(baseKeyPath: "api_data.api_raigeki")
210     }
211     
212     private func calculateMidnightBattle() {
213         
214         calculateHogeki(baseKeyPath: "api_data.api_hougeki")
215     }
216 }
217
218 // MARK: - Properties
219 extension DamageCalculator {
220     
221     private var damages: [Damage] {
222         
223         let array = store.sortedDamagesById()
224         
225         if array.isEmpty {
226             
227             buildDamagedEntity()
228             
229             return store.sortedDamagesById()
230         }
231         
232         return array
233     }
234     
235     private var isCombinedBattle: Bool {
236         
237         switch battleType {
238             
239         case .combinedAir, .combinedWater, .eachCombinedAir, .eachCombinedWater:
240             
241             return true
242             
243         default:
244             
245             return false
246             
247         }
248     }
249     
250     private func makeDamage(num: Int) -> [Damage] {
251         
252         guard let battle = store.battle() else {
253             
254             Logger.shared.log("Battle is invalid.")
255             
256             return []
257         }
258         
259         return (0..<num).compactMap {
260             
261             guard let damage = store.createDamage() else {
262                 
263                 Logger.shared.log("Can not create Damage")
264                 
265                 return nil
266             }
267             
268             damage.battle = battle
269             damage.id = $0
270             
271             return damage
272         }
273     }
274     
275     private func buildDamages(first: [Ship], second: [Ship]?) {
276         
277         guard let battle = store.battle() else {
278             
279             Logger.shared.log("Battle is invalid.")
280             
281             return
282         }
283         
284         let damages = makeDamage(num: 12)
285         
286         func setShip(_ ship: Ship, into damage: Damage) {
287             
288             let sStore = ServerDataStore.default
289             
290             damage.shipID = sStore.sync { ship.id }
291             damage.hp = sStore.sync { ship.nowhp }
292             
293             Debug.excute(level: .debug) {
294                 
295                 let name = sStore.sync { ship.name }
296                 print("add Damage entity of \(name) at \(damage.id)")
297             }
298         }
299         
300         zip(first, damages).forEach(setShip)
301
302         if let second = second {
303
304             let secondsDamage = damages[6...]
305             zip(second, secondsDamage).forEach(setShip)
306         }
307         
308     }
309     
310     private func buildDamagedEntity() {
311         
312         guard let battle = store.battle() else {
313             
314             Logger.shared.log("Battle is invalid.")
315             
316             return
317         }
318         
319         let sStore = ServerDataStore.default
320         // 第一艦隊
321         let deckId = battle.deckId
322         let firstFleetShips = sStore.sync { sStore.ships(byDeckId: deckId) }
323         
324         // 第二艦隊
325         if isCombinedBattle {
326             
327             let secondFleetShips = sStore.sync { sStore.ships(byDeckId: 2) }
328             buildDamages(first: firstFleetShips, second: secondFleetShips)
329             
330         } else {
331             
332             buildDamages(first: firstFleetShips, second: nil)
333         }
334     }
335 }
336
337 // MARK: - Primitive Calclator
338 extension DamageCalculator {
339     
340     private func hogekiTargets(_ list: JSON) -> [[Int]]? {
341         
342         guard let targetArraysArray = list
343             .array?
344             .compactMap({ $0.array?.compactMap { $0.int } }) else {
345                 
346                 return nil
347         }
348         
349         guard list.count == targetArraysArray.count else {
350             
351             Logger.shared.log("api_df_list is wrong")
352             
353             return nil
354         }
355         
356         return targetArraysArray
357     }
358     
359     private func hogekiDamages(_ list: JSON) -> [[Int]]? {
360         
361         return list.array?.compactMap { $0.array?.compactMap { $0.int } }
362     }
363     
364     private func enemyFlags(_ list: JSON) -> [Int]? {
365         
366         return list.array?.compactMap { $0.int }
367     }
368     
369     private func validTargetPos(_ targetPos: Int, in battleFleet: BattleFleet) -> Bool {
370         
371         return 0..<damages.count ~= targetPos
372     }
373     
374     private func position(_ pos: Int, in fleet: BattleFleet) -> Int? {
375         
376         let damagePos = (fleet == .secondOnly ? pos + 6 : pos)
377         
378         guard case 0..<damages.count = damagePos else {
379             
380             return nil
381         }
382         
383         return damagePos
384     }
385     
386     private func calcHP(damage: Damage, receive: Int) {
387         
388         Debug.excute(level: .debug) {
389             
390             let store = ServerDataStore.default
391             if receive != 0, let shipName = store.sync(execute: { store.ship(by: damage.shipID)?.name }) {
392                 
393                 print("\(shipName) recieve Damage \(receive)")
394             }
395         }
396         
397         damage.hp -= receive
398         
399         if damage.hp > 0 {
400             
401             return
402         }
403         
404         let sStore = ServerDataStore.default
405         guard let ship = sStore.sync(execute: { sStore.ship(by: damage.shipID) }) else {
406             
407             return
408         }
409         
410         damage.hp = damageControlIfPossible(ship: ship)
411         damage.useDamageControl = (damage.hp != 0)
412         
413     }
414     
415     private func calculateHogeki(baseKeyPath: String, _ bf: () -> BattleFleet) {
416         
417         calculateHogeki(baseKeyPath: baseKeyPath, battleFleet: bf())
418     }
419     
420     private func buildBattleData(baseKeyPath: String) -> [HogekiBattleData] {
421         
422         let baseValue = json[baseKeyPath.components(separatedBy: ".")]
423         
424         guard let targetPosLists = hogekiTargets(baseValue["api_df_list"]),
425             let damageLists = hogekiDamages(baseValue["api_damage"]) else {
426                 
427                 Debug.print("Cound not find api_df_list or api_damage for \(baseKeyPath)", level: .full)
428                 
429                 return []
430         }
431         
432         guard targetPosLists.count == damageLists.count else {
433             
434             Logger.shared.log("api_damage is wrong.")
435             
436             return []
437         }
438         
439         guard let eFlags = enemyFlags(baseValue["api_at_eflag"]) else {
440             
441             return zip(targetPosLists, damageLists).map { HogekiBattleData(targetPositionList: $0.0, damageList: $0.1, enemyFlags: false) }
442         }
443         
444         return zip(zip(targetPosLists, damageLists), eFlags)
445             .map { arg -> HogekiBattleData in
446                 
447                 let ((targetPosList, damageList), eflag) = arg
448                 
449                 return HogekiBattleData(targetPositionList: targetPosList, damageList: damageList, enemyFlags: eflag != 1)
450         }
451     }
452     
453     private func calculateHogeki(baseKeyPath: String, battleFleet: BattleFleet = .normal) {
454         
455         Debug.print("Start Hougeki \(baseKeyPath)", level: .debug)
456         
457         buildBattleData(baseKeyPath: baseKeyPath)
458             .map(friendDamage)
459             .filter { $0 != .zero }
460             .forEach { posDamage in
461                 
462                 guard validTargetPos(posDamage.position, in: battleFleet) else {
463                     
464                     Logger.shared.log("invalid position \(posDamage.position)")
465                     
466                     return
467                 }
468                 
469                 guard let damagePos = position(posDamage.position, in: battleFleet) else {
470                     
471                     Logger.shared.log("damage pos is larger than damage count")
472                     
473                     return
474                 }
475                 
476                 calcHP(damage: damages[damagePos], receive: posDamage.damage)
477                 
478                 Debug.print("Hougeki \(posDamage.position) -> \(posDamage.damage)", level: .debug)
479         }
480     }
481     
482     private func calculateFDam(baseKeyPath: String, _ bf: () -> BattleFleet) {
483         
484         calculateFDam(baseKeyPath: baseKeyPath, battleFleet: bf())
485     }
486     
487     private func calculateFDam(baseKeyPath: String, battleFleet: BattleFleet = .normal) {
488         
489         let baseValue = json[baseKeyPath.components(separatedBy: ".")]
490         
491         guard let fdamArray = baseValue["api_fdam"].arrayObject else {
492
493             Debug.print("Could not find api_fdam of \(baseKeyPath)", level: .full)
494             
495             return
496         }
497         
498         guard let intFdamArray = fdamArray as? [IntConvertable] else {
499             
500             Debug.print("api_fdam value of \(baseKeyPath) is not [Int].", level: .debug)
501             Debug.print(baseValue, level: .debug)
502             
503             return
504         }
505         
506         let frendDamages = intFdamArray.map { $0.toInt() }
507         
508         Debug.print("Start FDam \(baseKeyPath)", level: .debug)
509         
510         frendDamages.enumerated().forEach { (idx, damage) in
511             
512             guard let damagePos = position(idx, in: battleFleet) else {
513                 
514                 return
515             }
516             
517             calcHP(damage: damages[damagePos], receive: damage)
518             
519             Debug.print("FDam \(idx) -> \(damage)", level: .debug)
520         }
521     }
522 }
523
524 // MARK: - Damage control
525 extension DamageCalculator {
526     
527     private func damageControlIfPossible(ship: Ship) -> Int {
528         
529         let store = ServerDataStore.default
530         
531         return store.sync {
532             
533             let damageControl = ship
534                 .equippedItem
535                 .array
536                 .lazy
537                 .compactMap { $0 as? SlotItem }
538                 .map { store.masterSlotItemID(by: $0.id) }
539                 .compactMap { DamageControlID(rawValue: $0) }
540                 .first
541             
542             if let validDamageControl = damageControl {
543                 
544                 switch validDamageControl {
545                     
546                 case .damageControl:
547                     Debug.print("Damage Control", level: .debug)
548                     
549                     return Int(Double(ship.maxhp) * 0.2)
550                     
551                 case .goddes:
552                     Debug.print("Goddes", level: .debug)
553                     
554                     return ship.maxhp
555                     
556                 }
557             }
558             
559             // check extra slot
560             let exItemId = store.masterSlotItemID(by: ship.slot_ex)
561             
562             guard let exType = DamageControlID(rawValue: exItemId) else {
563                 
564                 return 0
565             }
566             
567             switch exType {
568                 
569             case .damageControl:
570                 Debug.print("Damage Control", level: .debug)
571                 
572                 return Int(Double(ship.maxhp) * 0.2)
573                 
574             case .goddes:
575                 Debug.print("Goddes", level: .debug)
576                 
577                 return ship.maxhp
578                 
579             }
580         }
581     }
582 }