OSDN Git Service

e9ce2675ebd20cea3a2a2f1c39c4c5bdafb28b91
[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     case minimumViewType = 1
15     case miniVierticalType = 2
16 }
17
18 private var shipKeysContext: Int = 0
19 private var shipsContext: Int = 0
20
21 final class FleetViewController: NSViewController {
22     
23     enum ShipOrder: Int {
24         
25         case doubleLine = 0
26         case leftToRight = 1
27     }
28     
29     enum SakutekiType: Int {
30         
31         case adding = 0
32         
33         case formula33 = 100
34         case formula33Parameter1 = 101
35         case formula33Parameter3 = 103
36         case formula33Parameter4 = 104
37     }
38     
39     enum SakutekiCalclationSterategy: Int {
40         
41         case total
42         case formula33
43     }
44     
45     static let oldStyleFleetViewHeight: CGFloat = 128.0
46     static let detailViewHeight: CGFloat = 288.0
47     static let heightDifference: CGFloat = detailViewHeight - oldStyleFleetViewHeight
48     
49     private static let maxFleetNumber: Int = 4
50     
51     private let details: [ShipDetailViewController]
52     private let shipKeys = [#keyPath(Deck.ship_0), #keyPath(Deck.ship_1), #keyPath(Deck.ship_2), #keyPath(Deck.ship_3), #keyPath(Deck.ship_4), #keyPath(Deck.ship_5)]
53     private let type: FleetViewType
54     private let fleetController = NSObjectController()
55     private let shipObserveKeys = [#keyPath(Ship.seiku), #keyPath(Ship.lv), #keyPath(Ship.equippedItem)]
56     
57     init?(viewType: FleetViewType) {
58         
59         type = viewType
60         
61         let shipiewType: ShipDetailViewType = {
62             
63             switch viewType {
64             case .detailViewType: return .full
65             case .minimumViewType: return .medium
66             case .miniVierticalType: return .minimum
67             }
68         }()
69         details = (1...6).map {
70             
71             guard let res = ShipDetailViewController(type: shipiewType) else { fatalError("Can not create ShipDetailViewController") }
72             
73             res.title = "\($0)"
74             
75             return res
76         }
77         
78         let nibName: NSNib.Name = {
79             switch viewType {
80             case .detailViewType: return FleetViewController.nibName
81             case .minimumViewType: return NSNib.Name("FleetMinimumViewController")
82             case .miniVierticalType: return NSNib.Name("VerticalFleetViewController")
83             }
84         }()
85         
86         super.init(nibName: nibName, bundle: nil)
87     }
88     
89     required init?(coder: NSCoder) {
90         
91         fatalError("init(coder:) has not been implemented")
92     }
93     
94     deinit {
95         
96         NotificationCenter.default.removeObserver(self)
97     }
98     
99     @IBOutlet weak var placeholder01: NSView!
100     @IBOutlet weak var placeholder02: NSView!
101     @IBOutlet weak var placeholder03: NSView!
102     @IBOutlet weak var placeholder04: NSView!
103     @IBOutlet weak var placeholder05: NSView!
104     @IBOutlet weak var placeholder06: NSView!
105     
106     @objc dynamic var fleetNumber: Int = 1 {
107         
108         didSet {
109             ServerDataStore.default
110                 .deck(by: fleetNumber)
111                 .map { fleet = $0 }
112         }
113     }
114     
115     @objc dynamic var fleet: Deck? {
116         
117         get { return representedObject as? Deck }
118         set {
119             representedObject = newValue
120             title = newValue?.name
121             setupShips()
122         }
123     }
124     
125     var enableAnimation: Bool = false
126     
127     var shipOrder: FleetViewController.ShipOrder = .doubleLine {
128         
129         willSet {
130             if shipOrder == newValue { return }
131             
132             switch newValue {
133             case .doubleLine: reorderShipToDoubleLine()
134             case .leftToRight: reorderShipToLeftToRight()
135             }
136         }
137     }
138     
139     var canDivide: Bool { return type == .detailViewType }
140     
141     var normalHeight: CGFloat {
142         
143         switch type {
144         case .detailViewType: return FleetViewController.detailViewHeight
145         case .minimumViewType: return FleetViewController.oldStyleFleetViewHeight
146         case .miniVierticalType: return 0.0
147         }
148     }
149     
150     var upsideHeight: CGFloat {
151         
152         switch type {
153         case .detailViewType: return 159.0
154         case .minimumViewType: return FleetViewController.oldStyleFleetViewHeight
155         case .miniVierticalType: return 0.0
156         }
157     }
158     
159     var sakutekiCalculator: SakutekiCalculator = SimpleCalculator() {
160         
161         didSet {
162             switch sakutekiCalculator {
163             case _ as SimpleCalculator:
164                 UserDefaults.standard[.sakutekiCalclationSterategy] = .total
165                 
166             case let f as Formula33:
167                 UserDefaults.standard[.sakutekiCalclationSterategy] = .formula33
168                 UserDefaults.standard[.formula33Factor] = Double(f.condition)
169                 
170             default: ()
171             }
172         }
173     }
174     
175     @objc var totalSakuteki: Double { return sakutekiCalculator.calculate(ships) }
176     @objc var totalSeiku: Int { return ships.reduce(0) { $0 + $1.seiku } }
177     @objc var totalCalclatedSeiku: Int { return ships.reduce(0) { $0 + totalSeiku(of: $1) } }
178     @objc var totalLevel: Int { return ships.reduce(0) { $0 + $1.lv } }
179     @objc var totalDrums: Int { return ships.reduce(0) { $0 + totalDrums(of: $1) } }
180     @objc var totalTPValue: Int {
181         
182         return ships
183             .map { ShipTPValueCalculator($0) }
184             .map { $0.value }
185             .reduce(0, +)
186         
187     }
188     
189     func totalSeiku(of ship: Ship) -> Int {
190         
191         return SeikuCalclator(ship: ship).totalSeiku
192     }
193     func totalDrums(of ship: Ship) -> Int {
194         
195         return (0...4).flatMap(ship.slotItem).filter { $0.slotitem_id == 75 }.count
196     }
197     
198     private var ships: [Ship] = [] {
199         
200         willSet {
201             ships.forEach { ship in
202                 
203                 shipObserveKeys.forEach { ship.removeObserver(self, forKeyPath: $0) }
204             }
205         }
206         didSet {
207             ships.forEach { ship in
208                 
209                 shipObserveKeys.forEach {
210                     
211                     ship.addObserver(self, forKeyPath: $0, context: &shipsContext)
212                 }
213             }
214         }
215     }
216     
217     private(set) var anchorageRepair = AnchorageRepairManager.default
218     
219     @objc dynamic private(set) var repairTime: NSNumber?
220     
221     override func viewDidLoad() {
222         
223         super.viewDidLoad()
224         
225         switch UserDefaults.standard[.sakutekiCalclationSterategy] {
226         case .total:
227             sakutekiCalculator = SimpleCalculator()
228             
229         case .formula33:
230             let factor = UserDefaults.standard[.formula33Factor]
231             sakutekiCalculator = Formula33(Int(factor))
232         }
233         
234         fleetController.bind(NSBindingName(#keyPath(NSArrayController.content)), to:self, withKeyPath:#keyPath(fleet), options:nil)
235         fleetController.addObserver(self, forKeyPath:"selection.name", context:nil)
236         shipKeys.forEach {
237             
238             let keyPath = "selection.\($0)"
239             fleetController.addObserver(self, forKeyPath:keyPath, context:&shipKeysContext)
240         }
241         
242         buildAnchorageRepairHolder()
243         
244         [placeholder01, placeholder02, placeholder03, placeholder04, placeholder05, placeholder06]
245             .enumerated()
246             .forEach {
247                 
248                 guard let view = $0.element else { return }
249                 
250                 let detail = details[$0.offset]
251                 detail.view.frame = view.frame
252                 detail.view.autoresizingMask = view.autoresizingMask
253                 self.view.replaceSubview(view, with: detail.view)
254         }
255         fleetNumber = 1
256         
257         // 初回起動時などデータがまだない時はportAPIを受信後設定する
258         weak var token: NSObjectProtocol?
259         token = NotificationCenter.default
260             .addObserver(forName: .PortAPIReceived, object: nil, queue: nil) { [weak self] _ in
261                 
262                 token.map(NotificationCenter.default.removeObserver)
263                 self?.fleetNumber = 1
264         }
265         
266         NotificationCenter
267             .default
268             .addObserver(forName: .DidUpdateGuardEscape, object: nil, queue: nil) { [weak self] _ in
269                 
270                 self?.notifyChangeValue(forKey: #keyPath(totalSeiku))
271                 self?.notifyChangeValue(forKey: #keyPath(totalCalclatedSeiku))
272                 self?.notifyChangeValue(forKey: #keyPath(totalSakuteki))
273                 self?.notifyChangeValue(forKey: #keyPath(totalDrums))
274                 self?.notifyChangeValue(forKey: #keyPath(totalTPValue))
275         }
276     }
277     
278     override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
279         
280         
281         if keyPath == "selection.name" {
282             
283             title = fleet?.name
284             
285             return
286         }
287         
288         if context == &shipKeysContext {
289             
290             setupShips()
291             
292             return
293         }
294         
295         if let keyPath = keyPath {
296             
297             if context == &shipsContext {
298                 
299                 switch keyPath {
300                 case #keyPath(Ship.equippedItem):
301                     notifyChangeValue(forKey: #keyPath(totalSakuteki))
302                     notifyChangeValue(forKey: #keyPath(totalDrums))
303                     notifyChangeValue(forKey: #keyPath(totalCalclatedSeiku))
304                     notifyChangeValue(forKey: #keyPath(totalTPValue))
305                     
306                 case #keyPath(Ship.seiku):
307                     notifyChangeValue(forKey: #keyPath(totalSeiku))
308                     notifyChangeValue(forKey: #keyPath(totalCalclatedSeiku))
309                     
310                 case #keyPath(Ship.lv):
311                     notifyChangeValue(forKey: #keyPath(totalLevel))
312                     
313                 default: break
314                 }
315                 
316                 return
317             }
318         }
319         
320         super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
321     }
322     
323     @IBAction func selectNextFleet(_ sender: AnyObject?) {
324         
325         let next = fleetNumber + 1
326         fleetNumber = (next <= FleetViewController.maxFleetNumber ? next : 1)
327     }
328     
329     @IBAction func selectPreviousFleet(_ sender: AnyObject?) {
330         
331         let prev = fleetNumber - 1
332         fleetNumber = (prev > 0 ? prev : 4)
333     }
334     
335     @IBAction func changeSakutekiCalculator(_ sender: Any?) {
336         
337         guard let menuItem = sender as? NSMenuItem else { return }
338         
339         switch menuItem.tag {
340         case 0:
341             sakutekiCalculator = SimpleCalculator()
342             
343         case 101...199:
344             sakutekiCalculator = Formula33(menuItem.tag - 100)
345             
346         case 100:
347             askCalcutaionTurnPoint()
348             
349         default: return
350         }
351         
352         notifyChangeValue(forKey: #keyPath(totalSakuteki))
353     }
354     
355     private func askCalcutaionTurnPoint() {
356         
357         guard let window = self.view.window else { return }
358         
359         let current = (sakutekiCalculator as? Formula33)?.condition ?? 1
360         
361         let wc = CalculateConditionPanelController()
362         wc.condition = Double(current)
363         wc.beginModal(for: window) {
364             
365             self.sakutekiCalculator = Formula33(Int($0))
366             
367             self.notifyChangeValue(forKey: #keyPath(totalSakuteki))
368         }
369     }
370     
371     private func setupShips() {
372         
373         let array: [Ship?] = (0..<6).map { fleet?[$0] }
374         zip(details, array).forEach { $0.0.ship = $0.1 }
375         ships = array.flatMap { $0 }
376         
377         [#keyPath(totalSakuteki), #keyPath(totalSeiku), #keyPath(totalCalclatedSeiku),
378          #keyPath(totalLevel), #keyPath(totalDrums), #keyPath(repairable),
379          #keyPath(totalTPValue)]
380             .forEach(notifyChangeValue(forKey:))
381     }
382 }
383
384 extension FleetViewController {
385     
386     override func validateMenuItem(_ menuItem: NSMenuItem) -> Bool {
387         
388         guard let action = menuItem.action else { return false }
389         
390         switch action {
391             
392         case #selector(changeSakutekiCalculator(_:)):
393             
394             if let _ = sakutekiCalculator as? SimpleCalculator {
395                 
396                 menuItem.state = (menuItem.tag == 0 ? .on : .off)
397                 
398                 return true
399                 
400             } else if let sakuObj = sakutekiCalculator as? Formula33 {
401                 
402                 let cond = 100 + sakuObj.condition
403                 
404                 menuItem.state = (menuItem.tag == cond ? .on : .off)
405                 
406                 return true
407             }
408             
409         default: ()
410         }
411         
412         return false
413     }
414 }
415
416 extension FleetViewController {
417     
418     private func reorder(order: [Int]) {
419         
420         guard order.count == 6 else { return Logger.shared.log("FleetViewController: order count is not 6.") }
421         
422         let views = details.map { $0.view }
423         let options = views.map { $0.autoresizingMask }
424         let reorderedOptions = order.map { options[$0] }
425         zip(views, reorderedOptions).forEach { $0.0.autoresizingMask = $0.1 }
426         
427         let frames = views.map { $0.frame }
428         let reorderedFrames = order.map { frames[$0] }
429         zip(views, reorderedFrames).forEach { $0.0.setFrame($0.1, animate: enableAnimation) }
430     }
431     
432     private func reorderShipToDoubleLine() {
433         
434         reorder(order: [0, 3, 1, 4, 2, 5])
435     }
436     
437     private func reorderShipToLeftToRight() {
438         
439         reorder(order: [0, 2, 4, 1, 3, 5])
440     }
441 }
442
443 extension FleetViewController {
444     
445     func buildAnchorageRepairHolder() {
446         
447         AppDelegate.shared.addCounterUpdate { [weak self] in
448             
449             guard let `self` = self else { return }
450             
451             self.repairTime = self.calcRepairTime()
452         }
453     }
454     
455     private func calcRepairTime() -> NSNumber? {
456         
457         let time = anchorageRepair.repairTime
458         let complete = time.timeIntervalSince1970
459         let now = Date(timeIntervalSinceNow: 0.0)
460         let diff = complete - now.timeIntervalSince1970
461         
462         return NSNumber(value: diff + 20.0 * 60)
463     }
464     
465     private var repairShipIds: [Int] { return [19] }
466     
467     @objc dynamic var repairable: Bool {
468         
469         guard let flagShip = fleet?[0] else { return false }
470         
471         return repairShipIds.contains(flagShip.master_ship.stype.id)
472     }
473 }
474
475 extension FleetViewController: NibLoadable {}