|
| 1 | +// |
| 2 | +// AgentCharacterDescription.swift |
| 3 | +// Clippy |
| 4 | +// |
| 5 | +// Created by Devran on 06.09.19. |
| 6 | +// Copyright © 2019 Devran. All rights reserved. |
| 7 | +// |
| 8 | + |
| 9 | +import Foundation |
| 10 | +import SpriteKit |
| 11 | + |
| 12 | +enum AgentError: Error { |
| 13 | + case frameOutOfBounds |
| 14 | +} |
| 15 | + |
| 16 | +struct Agent { |
| 17 | + var character: AgentCharacter |
| 18 | + var balloon: AgentBalloon |
| 19 | + var animations: [AgentAnimation] |
| 20 | + var states: [AgentState] |
| 21 | + |
| 22 | + var agentURL: URL |
| 23 | + var resourceName: String |
| 24 | + var resourceNameWithSuffix: String |
| 25 | + var spriteMap: CGImage |
| 26 | + let soundsURL: URL |
| 27 | + |
| 28 | + init?(agentURL: URL) { |
| 29 | + self.agentURL = agentURL |
| 30 | + self.soundsURL = agentURL.appendingPathComponent("sounds") |
| 31 | + self.resourceNameWithSuffix = agentURL.lastPathComponent |
| 32 | + self.resourceName = resourceNameWithSuffix.replacingOccurrences(of: ".agent", with: "") |
| 33 | + |
| 34 | + let fileURL = agentURL.appendingPathComponent("\(resourceName).acd") |
| 35 | + let imageURL = agentURL.appendingPathComponent("\(resourceName)_sprite_map.png") |
| 36 | + |
| 37 | + guard let fileContent = try? String(contentsOf: fileURL, encoding: String.Encoding.utf8) else { return nil } |
| 38 | + |
| 39 | + // Character |
| 40 | + guard let characterText = fileContent.fetchInclusive("DefineCharacter", until: "EndCharacter").first else { return nil } |
| 41 | + let character = AgentCharacter.parse(content: characterText) |
| 42 | + |
| 43 | + // Balloon |
| 44 | + guard let balloonText = fileContent.fetchInclusive("DefineBalloon", until: "EndBalloon").first else { return nil } |
| 45 | + let balloon = AgentBalloon.parse(content: balloonText) |
| 46 | + |
| 47 | + // Animations |
| 48 | + let animationTexts = fileContent.fetchInclusive("DefineAnimation", until: "EndAnimation") |
| 49 | + let animations = animationTexts.compactMap { AgentAnimation.parse(content: $0) } |
| 50 | + |
| 51 | + // States |
| 52 | + let stateTexts = fileContent.fetchInclusive("DefineState", until: "EndState") |
| 53 | + let states = stateTexts.compactMap { AgentState.parse(content: $0) } |
| 54 | + |
| 55 | + // Sprite Map |
| 56 | + guard let image = NSImage(contentsOf: imageURL)?.cgImage(forProposedRect: nil, context: nil, hints: nil) else { return nil } |
| 57 | + spriteMap = image |
| 58 | + |
| 59 | + if let character = character, let balloon = balloon { |
| 60 | + self.character = character |
| 61 | + self.balloon = balloon |
| 62 | + self.animations = animations |
| 63 | + self.states = states |
| 64 | + } else { |
| 65 | + return nil |
| 66 | + } |
| 67 | + } |
| 68 | + |
| 69 | + init?(resourceName: String) { |
| 70 | + let directoryName = "\(resourceName).agent" |
| 71 | + self.init(agentURL: Agent.agentsURL().appendingPathComponent(directoryName)) |
| 72 | + } |
| 73 | + |
| 74 | + func soundURL(forIndex index: Int) -> URL { |
| 75 | + let fileName = "\(resourceName)_\(index).mp3" |
| 76 | + return soundsURL.appendingPathComponent(fileName) |
| 77 | + } |
| 78 | + |
| 79 | + func findAnimation(_ name: String) -> AgentAnimation? { |
| 80 | + return animations.first(where: { $0.name == name }) |
| 81 | + } |
| 82 | +} |
| 83 | + |
| 84 | +extension Agent { |
| 85 | + var columns: Int { |
| 86 | + let columns = Int(spriteMap.width) / character.width |
| 87 | + return columns |
| 88 | + } |
| 89 | + var rows: Int { |
| 90 | + let rows = Int(spriteMap.height) / character.height |
| 91 | + return rows |
| 92 | + } |
| 93 | + |
| 94 | + func textureAtPosition(x: Int, y: Int) throws -> CGImage { |
| 95 | + guard (0...rows ~= y && 0...columns ~= x) else { throw AgentError.frameOutOfBounds } |
| 96 | + let textureWidth = character.width |
| 97 | + let textureHeight = character.height |
| 98 | + let rect = CGRect(x: x * textureWidth, y: y * textureHeight, width: textureWidth, height: textureHeight) |
| 99 | + return spriteMap.cropping(to: rect)! |
| 100 | + } |
| 101 | + |
| 102 | + func textureAtIndex(index: Int) throws -> CGImage { |
| 103 | + let x = index % columns |
| 104 | + let y = index / columns |
| 105 | + return try! textureAtPosition(x: x, y: y) |
| 106 | + } |
| 107 | + |
| 108 | + func imageForFrame(_ frame: AgentFrame) -> CGImage { |
| 109 | + let cgImages = frame.images.reversed().map{ try! textureAtIndex(index: $0.imageNumber) } |
| 110 | + if let mergedImage = CGImage.mergeImages(cgImages) { |
| 111 | + return mergedImage |
| 112 | + } else { |
| 113 | + return try! textureAtIndex(index: 0) |
| 114 | + } |
| 115 | + } |
| 116 | +} |
| 117 | + |
| 118 | +extension Agent { |
| 119 | + static func agentsURL() -> URL { |
| 120 | + let fileManager = FileManager.default |
| 121 | + |
| 122 | + guard let applicationSupportURL = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first else { |
| 123 | + fatalError("Cant create Agents directory") |
| 124 | + } |
| 125 | + |
| 126 | + let agentsURL = applicationSupportURL.appendingPathComponent("Clippy/Agents", isDirectory: true) |
| 127 | + createAgentsDirectoriesIfNeeded(url: agentsURL) |
| 128 | + |
| 129 | + return agentsURL |
| 130 | + } |
| 131 | + |
| 132 | + static func createAgentsDirectoriesIfNeeded(url: URL) { |
| 133 | + let fileManager = FileManager.default |
| 134 | + if !fileManager.fileExists(atPath: url.path) { |
| 135 | + try? fileManager.createDirectory(at: url, |
| 136 | + withIntermediateDirectories: true, |
| 137 | + attributes: nil) |
| 138 | + ["clippit", "links", "merlin"].forEach { |
| 139 | + guard let agentsArchiveURL = Bundle.main.url(forResource: "\($0).agent", withExtension: "zip") else { |
| 140 | + return |
| 141 | + } |
| 142 | + try? fileManager.copyItem(at: agentsArchiveURL, to: url.appendingPathComponent("\($0).agent.zip")) |
| 143 | + } |
| 144 | + } |
| 145 | + } |
| 146 | + |
| 147 | + static func agentNames() -> [String] { |
| 148 | + var agentNames: [String] = [] |
| 149 | + let fileManager = FileManager.default |
| 150 | + guard let items = try? fileManager.contentsOfDirectory(at: agentsURL(), |
| 151 | + includingPropertiesForKeys: nil, |
| 152 | + options: []) else { |
| 153 | + return [] |
| 154 | + } |
| 155 | + |
| 156 | + for item in items { |
| 157 | + if item.hasDirectoryPath && item.lastPathComponent.hasSuffix(".agent") { |
| 158 | + agentNames.append(item.lastPathComponent.replacingOccurrences(of: ".agent", with: "")) |
| 159 | + } |
| 160 | + } |
| 161 | + return agentNames.sorted() |
| 162 | + } |
| 163 | + |
| 164 | + static func randomAgentName() -> String? { |
| 165 | + agentNames().randomElement() |
| 166 | + } |
| 167 | +} |
| 168 | + |
0 commit comments