OSDN Git Service

ApplicationDirecroriesの中のアプリケーションに依存する部分を分離した
[kcd/KCD.git] / KCD / FleetViewController.swift
1 //
2 //  FleetViewController.swift
3 //  KCD
4 //
5 //  Created by Hori,Masaki on 2016/12/27.
6 //  Copyright © 2016年 Hori,Masaki. All rights reserved.
7 //
8
9 import Cocoa
10
11 enum FleetViewType: Int {
12     
13     case detailViewType = 0
14     
15     case minimumViewType = 1
16     
17     case miniVierticalType = 2
18 }
19
20 private var shipKeysContext: Int = 0
21 private var shipsContext: Int = 0
22
23 protocol FleetViewControllerDelegate: class {
24     
25     func changeShowsExtShip(_ fleetViewController: FleetViewController, showsExtShip: Bool)
26 }
27
28 final class FleetViewController: NSViewController {
29     
30     enum ShipOrder: Int {
31         
32         case doubleLine = 0
33         
34         case leftToRight = 1
35     }
36     
37     enum SakutekiType: Int {
38         
39         case adding = 0
40         
41         ///
42         case formula33 = 100
43         
44         case formula33Parameter1 = 101
45         
46         case formula33Parameter3 = 103
47         
48         case formula33Parameter4 = 104
49     }
50     
51     enum SakutekiCalclationSterategy: Int {
52         
53         case total
54         
55         case formula33
56     }
57     
58     static let oldStyleFleetViewHeight: CGFloat = 128.0
59     static let detailViewHeight: CGFloat = 320.0
60     static let heightDifference: CGFloat = detailViewHeight - oldStyleFleetViewHeight
61     
62     private static let maxFleetNumber: Int = 4
63     
64     private let details: [ShipDetailViewController]
65     private let shipKeys = [#keyPath(Deck.ship_0), #keyPath(Deck.ship_1), #keyPath(Deck.ship_2), #keyPath(Deck.ship_3),
66                             #keyPath(Deck.ship_4), #keyPath(Deck.ship_5), #keyPath(Deck.ship_6)]
67     private let type: FleetViewType
68     private let fleetController = NSObjectController()
69     private let shipObserveKeys = [#keyPath(Ship.seiku), #keyPath(Ship.lv), #keyPath(Ship.equippedItem)]
70     
71     private var notificationObserver = NotificationObserver()
72     
73     init?(viewType: FleetViewType) {
74         
75         type = viewType
76         
77         let shipiewType: ShipDetailViewType = {
78             
79             switch viewType {
80                 
81             case .detailViewType: return .full
82                 
83             case .minimumViewType: return .medium
84                 
85             case .miniVierticalType: return .minimum
86                 
87             }
88         }()
89         details = (1...6).map {
90             
91             guard let res = ShipDetailViewController(type: shipiewType) else {
92                 
93                 fatalError("Can not create ShipDetailViewController")
94             }
95             
96             res.title = "\($0)"
97             
98             return res
99         }
100         
101         let nibName: NSNib.Name = {
102             
103             switch viewType {
104                 
105             case .detailViewType: return FleetViewController.nibName
106                 
107             case .minimumViewType: return NSNib.Name("FleetMinimumViewController")
108                 
109             case .miniVierticalType: return NSNib.Name("VerticalFleetViewController")
110                 
111             }
112         }()
113         
114         super.init(nibName: nibName, bundle: nil)
115     }
116     
117     required init?(coder: NSCoder) {
118         
119         fatalError("init(coder:) has not been implemented")
120     }
121     
122     @IBOutlet private weak var placeholder01: NSView!
123     @IBOutlet private weak var placeholder02: NSView!
124     @IBOutlet private weak var placeholder03: NSView!
125     @IBOutlet private weak var placeholder04: NSView!
126     @IBOutlet private weak var placeholder05: NSView!
127     @IBOutlet private weak var placeholder06: NSView!
128     
129     @objc dynamic var fleetNumber: Int = 1 {
130         
131         didSet {
132             
133             ServerDataStore.default
134                 .deck(by: fleetNumber)
135                 .map { fleet = $0 }
136         }
137     }
138     
139     @objc dynamic var fleet: Deck? {
140         
141         get { return representedObject as? Deck }
142         set {
143             
144             representedObject = newValue
145             title = newValue?.name
146             checkExtShip()
147             setupShips()
148         }
149     }
150     
151     private var extDetail: ShipDetailViewController?
152     private var extShipAnimating: Bool = false
153     weak var delegate: FleetViewControllerDelegate?
154     
155     var enableAnimation: Bool = false
156     
157     var shipOrder: FleetViewController.ShipOrder = .doubleLine {
158         
159         willSet {
160             
161             if shipOrder == newValue {
162                 
163                 return
164             }
165             
166             switch newValue {
167                 
168             case .doubleLine: reorderShipToDoubleLine()
169                 
170             case .leftToRight: reorderShipToLeftToRight()
171                 
172             }
173         }
174     }
175     
176     var canDivide: Bool { return type == .detailViewType }
177     
178     var normalHeight: CGFloat {
179         
180         switch type {
181             
182         case .detailViewType: return FleetViewController.detailViewHeight
183             
184         case .minimumViewType: return FleetViewController.oldStyleFleetViewHeight
185             
186         case .miniVierticalType: return 0.0
187             
188         }
189     }
190     
191     var upsideHeight: CGFloat {
192         
193         switch type {
194             
195         case .detailViewType: return 175.0
196             
197         case .minimumViewType: return FleetViewController.oldStyleFleetViewHeight
198             
199         case .miniVierticalType: return 0.0
200             
201         }
202     }
203     
204     var sakutekiCalculator: SakutekiCalculator = SimpleCalculator() {
205         
206         didSet {
207             
208             switch sakutekiCalculator {
209                 
210             case _ as SimpleCalculator:
211                 UserDefaults.standard[.sakutekiCalclationSterategy] = .total
212                 
213             case let f as Formula33:
214                 UserDefaults.standard[.sakutekiCalclationSterategy] = .formula33
215                 UserDefaults.standard[.formula33Factor] = Double(f.condition)
216                 
217             default: ()
218             }
219             
220         }
221     }
222     
223     @objc private var totalSakuteki: Double { return sakutekiCalculator.calculate(ships) }
224     @objc var totalSeiku: Int { return ships.reduce(0) { $0 + $1.seiku } }
225     @objc var totalCalclatedSeiku: Int { return ships.reduce(0) { $0 + totalSeiku(of: $1) } }
226     @objc private var totalLevel: Int { return ships.reduce(0) { $0 + $1.lv } }
227     @objc private var totalDrums: Int { return ships.reduce(0) { $0 + totalDrums(of: $1) } }
228     @objc var totalTPValue: Int {
229         
230         return ships
231             .map { ShipTPValueCalculator($0).value }
232             .reduce(0, +)
233     }
234     @objc private var totalBRankTPValue: Int { return Int(floor(Double(totalTPValue) * 0.7)) }
235     
236     private func totalSeiku(of ship: Ship) -> Int {
237         
238         return SeikuCalclator(ship: ship).totalSeiku
239     }
240     private func totalDrums(of ship: Ship) -> Int {
241         
242         return (0...4).compactMap(ship.slotItem).filter { $0.slotitem_id == 75 }.count
243     }
244     
245     private var ships: [Ship] = [] {
246         
247         willSet {
248             
249             ships.forEach { ship in
250                 
251                 shipObserveKeys.forEach { ship.removeObserver(self, forKeyPath: $0) }
252             }
253         }
254         
255         didSet {
256             
257             ships.forEach { ship in
258                 
259                 shipObserveKeys.forEach {
260                     
261                     ship.addObserver(self, forKeyPath: $0, context: &shipsContext)
262                 }
263             }
264         }
265     }
266     
267     private(set) var anchorageRepair = AnchorageRepairManager.default
268     
269     @objc dynamic private(set) var repairTime: NSNumber?
270     
271     override func viewDidLoad() {
272         
273         super.viewDidLoad()
274         
275         switch UserDefaults.standard[.sakutekiCalclationSterategy] {
276             
277         case .total:
278             sakutekiCalculator = SimpleCalculator()
279             
280         case .formula33:
281             let factor = UserDefaults.standard[.formula33Factor]
282             sakutekiCalculator = Formula33(Int(factor))
283             
284         }
285         
286         fleetController.bind(NSBindingName(#keyPath(NSArrayController.content)), to: self, withKeyPath: #keyPath(fleet))
287         fleetController.addObserver(self, forKeyPath: "selection.name", context: nil)
288         shipKeys.forEach {
289             
290             let keyPath = "selection.\($0)"
291             fleetController.addObserver(self, forKeyPath: keyPath, context: &shipKeysContext)
292         }
293         
294         buildAnchorageRepairHolder()
295         
296         zip([placeholder01, placeholder02, placeholder03, placeholder04, placeholder05, placeholder06], details)
297             .forEach { view, detail in
298                 
299                 guard let view = view else {
300                     
301                     return
302                 }
303                 
304                 detail.view.frame = view.frame
305                 detail.view.autoresizingMask = view.autoresizingMask
306                 self.view.replaceSubview(view, with: detail.view)
307                 
308         }
309         fleetNumber = 1
310         
311         // 初回起動時などデータがまだない時はportAPIを受信後設定する
312         NotificationCenter.default
313             .addObserverOnce(forName: .PortAPIReceived, object: nil, queue: .main) { [weak self] _ in
314                 
315                 if let current = self?.fleetNumber {
316                     
317                     self?.fleetNumber = current
318                     
319                 } else {
320                     
321                     self?.fleetNumber = 1
322                 }
323         }
324         
325         notificationObserver
326             .addObserver(forName: .DidUpdateGuardEscape, object: nil, queue: .main) { [weak self] _ in
327                 
328                 self?.notifyChangeValue(forKey: #keyPath(totalSeiku))
329                 self?.notifyChangeValue(forKey: #keyPath(totalCalclatedSeiku))
330                 self?.notifyChangeValue(forKey: #keyPath(totalSakuteki))
331                 self?.notifyChangeValue(forKey: #keyPath(totalDrums))
332                 self?.notifyChangeValue(forKey: #keyPath(totalTPValue))
333                 self?.notifyChangeValue(forKey: #keyPath(totalBRankTPValue))
334         }
335     }
336     
337     override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
338         
339         
340         if keyPath == "selection.name" {
341             
342             title = fleet?.name
343             
344             return
345         }
346         
347         if context == &shipKeysContext {
348             
349             checkExtShip()
350             setupShips()
351             
352             return
353         }
354         
355         if let keyPath = keyPath {
356             
357             if context == &shipsContext {
358                 
359                 switch keyPath {
360                     
361                 case #keyPath(Ship.equippedItem):
362                     notifyChangeValue(forKey: #keyPath(totalSakuteki))
363                     notifyChangeValue(forKey: #keyPath(totalDrums))
364                     notifyChangeValue(forKey: #keyPath(totalCalclatedSeiku))
365                     notifyChangeValue(forKey: #keyPath(totalTPValue))
366                     notifyChangeValue(forKey: #keyPath(totalBRankTPValue))
367
368                 case #keyPath(Ship.seiku):
369                     notifyChangeValue(forKey: #keyPath(totalSeiku))
370                     notifyChangeValue(forKey: #keyPath(totalCalclatedSeiku))
371                     
372                 case #keyPath(Ship.lv):
373                     notifyChangeValue(forKey: #keyPath(totalLevel))
374                     
375                 default: break
376                     
377                 }
378                 
379                 return
380             }
381         }
382         
383         super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
384     }
385     
386     @IBAction func selectNextFleet(_ sender: AnyObject?) {
387         
388         let next = fleetNumber + 1
389         fleetNumber = (next <= FleetViewController.maxFleetNumber ? next : 1)
390     }
391     
392     @IBAction func selectPreviousFleet(_ sender: AnyObject?) {
393         
394         let prev = fleetNumber - 1
395         fleetNumber = (prev > 0 ? prev : 4)
396     }
397     
398     @IBAction func changeSakutekiCalculator(_ sender: Any?) {
399         
400         guard let menuItem = sender as? NSMenuItem else {
401             
402             return
403         }
404         
405         switch menuItem.tag {
406             
407         case 0:
408             sakutekiCalculator = SimpleCalculator()
409             
410         case 101...199:
411             sakutekiCalculator = Formula33(menuItem.tag - 100)
412             
413         case 100:
414             askCalcutaionTurnPoint()
415             
416         default: return
417             
418         }
419         
420         notifyChangeValue(forKey: #keyPath(totalSakuteki))
421     }
422     
423     private func askCalcutaionTurnPoint() {
424         
425         guard let window = self.view.window else {
426             
427             return
428         }
429         
430         let current = (sakutekiCalculator as? Formula33)?.condition ?? 1
431         
432         let wc = CalculateConditionPanelController()
433         wc.condition = Double(current)
434         wc.beginModal(for: window) {
435             
436             self.sakutekiCalculator = Formula33(Int($0))
437             
438             self.notifyChangeValue(forKey: #keyPath(totalSakuteki))
439         }
440     }
441     
442     private func setupShips() {
443         
444         let array: [Ship?] = (0..<6).map { fleet?[$0] }
445         zip(details, array).forEach { $0.0.ship = $0.1 }
446         
447         let extShip = fleet?[6]
448         extShip.map { extDetail?.ship = $0 }
449         ships = array.compactMap { $0 } + [extShip].compactMap { $0 }
450         
451         [#keyPath(totalSakuteki), #keyPath(totalSeiku), #keyPath(totalCalclatedSeiku),
452          #keyPath(totalLevel), #keyPath(totalDrums), #keyPath(repairable),
453          #keyPath(totalTPValue), #keyPath(totalBRankTPValue)]
454             .forEach(notifyChangeValue(forKey:))
455     }
456 }
457
458 extension FleetViewController {
459     
460     override func validateMenuItem(_ menuItem: NSMenuItem) -> Bool {
461         
462         guard let action = menuItem.action else {
463             
464             return false
465         }
466         
467         switch action {
468             
469         case #selector(changeSakutekiCalculator(_:)):
470             
471             if let _ = sakutekiCalculator as? SimpleCalculator {
472                 
473                 menuItem.state = (menuItem.tag == 0 ? .on : .off)
474                 
475                 return true
476                 
477             } else if let sakuObj = sakutekiCalculator as? Formula33 {
478                 
479                 let cond = 100 + sakuObj.condition
480                 
481                 menuItem.state = (menuItem.tag == cond ? .on : .off)
482                 
483                 return true
484             }
485             
486         default: ()
487             
488         }
489         
490         return false
491     }
492 }
493
494 extension FleetViewController {
495     
496     private func reorder(order: [Int]) {
497         
498         guard order.count == 6 else {
499             
500             Logger.shared.log("FleetViewController: order count is not 6.")
501             
502             return
503         }
504         
505         let views = details.map { $0.view }
506         let options = views.map { $0.autoresizingMask }
507         let reorderedOptions = order.map { options[$0] }
508         zip(views, reorderedOptions).forEach { $0.0.autoresizingMask = $0.1 }
509         
510         let frames = views.map { $0.frame }
511         let reorderedFrames = order.map { frames[$0] }
512         zip(views, reorderedFrames).forEach { $0.0.setFrame($0.1, animate: enableAnimation) }
513     }
514     
515     private func reorderShipToDoubleLine() {
516         
517         reorder(order: [0, 3, 1, 4, 2, 5])
518     }
519     
520     private func reorderShipToLeftToRight() {
521         
522         reorder(order: [0, 2, 4, 1, 3, 5])
523     }
524 }
525
526 extension FleetViewController {
527     
528     private func buildAnchorageRepairHolder() {
529         
530         AppDelegate.shared.addCounterUpdate { [weak self] in
531             
532             guard let `self` = self else {
533                 
534                 return
535             }
536             
537             self.repairTime = self.calcRepairTime()
538         }
539     }
540     
541     private func calcRepairTime() -> NSNumber? {
542         
543         let time = anchorageRepair.repairTime
544         let complete = time.timeIntervalSince1970
545         let now = Date(timeIntervalSinceNow: 0.0)
546         let diff = complete - now.timeIntervalSince1970
547         
548         return NSNumber(value: diff + 20.0 * 60)
549     }
550     
551     private var repairShipIds: [Int] { return [19] }
552     
553     @objc private dynamic var repairable: Bool {
554         
555         guard let flagShip = fleet?[0] else {
556             
557             return false
558         }
559         
560         return repairShipIds.contains(flagShip.master_ship.stype.id)
561     }
562 }
563
564 extension FleetViewController {
565     
566     var shipViewSize: NSSize { return details[0].view.frame.size }
567     
568     private func showExtShip() {
569         
570         guard type == .detailViewType else {
571             
572             return
573         }
574         guard extDetail == nil else {
575             
576             return
577         }
578         guard let _ = fleet?[6] else {
579             
580             return
581         }
582         
583         if extShipAnimating {
584             
585             DispatchQueue.main.async(execute: showExtShip)
586             
587             return
588         }
589         
590         extShipAnimating = true
591         
592         NSAnimationContext.runAnimationGroup({ _ in
593             
594             extDetail = ShipDetailViewController(type: .full)
595             guard let extDetail = extDetail else {
596                 
597                 return
598             }
599             
600             extDetail.title = "7"
601             extDetail.view.autoresizingMask = [.minXMargin]
602             
603             let width = extDetail.view.frame.width - 1
604             var frame = view.frame
605             frame.size.width += width
606             
607             extDetail.view.alphaValue = 0.0
608             extDetail.view.animator().alphaValue = 1.0
609             
610             extDetail.view.frame = details[5].view.frame
611             view.addSubview(extDetail.view, positioned: .below, relativeTo: details[5].view)
612             view.animator().frame = frame
613             
614         }, completionHandler: { [weak self] in
615             
616             self?.extShipAnimating = false
617         })
618         
619         delegate?.changeShowsExtShip(self, showsExtShip: true)
620     }
621     
622     private func hideExtShip() {
623         
624         guard type == .detailViewType else {
625             
626             return
627         }
628         
629         if extShipAnimating {
630             
631             DispatchQueue.main.async(execute: hideExtShip)
632             
633             return
634         }
635         
636         guard let extDetail = extDetail else {
637             
638             return
639         }
640         
641         extShipAnimating = true
642         
643         NSAnimationContext.runAnimationGroup({ _ in
644             
645             extDetail.view.animator().alphaValue = 0.0
646             
647             var frame = view.frame
648             frame.size.width -= extDetail.view.frame.width - 1
649             view.animator().frame = frame
650             
651             ships.removeLast()
652             
653         }, completionHandler: { [weak self] in
654             
655             extDetail.view.removeFromSuperview()
656             
657             self?.extShipAnimating = false
658             
659         })
660         
661         self.extDetail = nil
662         
663         delegate?.changeShowsExtShip(self, showsExtShip: false)
664     }
665     
666     private func checkExtShip() {
667                 
668         guard type == .detailViewType else {
669             
670             return
671         }
672         
673         if fleet?[6] != nil {
674             
675             showExtShip()
676             
677         } else {
678             
679             hideExtShip()
680         }
681     }
682 }
683
684 extension FleetViewController: NibLoadable {}