Skip to content

Commit

Permalink
GachaKit // +GachaItemExpressible, etc.
Browse files Browse the repository at this point in the history
  • Loading branch information
ShikiSuen committed Sep 13, 2024
1 parent 30a3683 commit 824fefa
Show file tree
Hide file tree
Showing 10 changed files with 886 additions and 44 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
// (c) 2024 and onwards Pizza Studio (AGPL v3.0 License or later).
// ====================
// This code is released under the SPDX-License-Identifier: `AGPL-3.0-or-later`.

import EnkaKit
import Foundation
import PZAccountKit
import PZBaseKit
import SwiftUI

// MARK: - GachaItemExpressible

/// 专用于 PZGachaEntry 的前端表述框架。
public struct GachaItemExpressible: Identifiable, Equatable, Sendable {
// MARK: Public

public let id: String
public let uid: String
public let game: Pizza.SupportedGame
public let pool: GachaPoolExpressible
public let itemID: String
public let count: String
public let time: Date
public let gachaID: String

// MARK: Fileprivate

fileprivate let name: String
fileprivate let rankType: GachaItemRankType
}

extension GachaItemExpressible {
public init(rawEntry: PZGachaEntryProtocol) {
self.id = rawEntry.id
self.uid = rawEntry.uid
self.game = rawEntry.game
self.pool = .init(rawEntry.gachaType, game: rawEntry.game) // 从 GachaType 解读。
self.itemID = rawEntry.itemID // 这里假设原神的 itemID 已被修复。
self.count = rawEntry.count
self.name = rawEntry.name
// self.itemType = .init(rawString4GI: rawEntry.itemType) // 改用 ItemID 推断。
self.rankType = .init(rawValueStr: rawEntry.rankType, game: rawEntry.game) ?? .rank3
self.gachaID = rawEntry.gachaID
let tzDelta = GachaKit.getServerTimeZoneDelta(uid: rawEntry.uid, game: rawEntry.game)
self.time = .init(rawEntry.time, tzDelta: tzDelta) ?? .distantPast
}
}

extension GachaItemExpressible {
public var uidWithGame: String {
"\(game.uidPrefix)-\(uid)"
}

/// Is Lose5050 (i.e. Surinuked).
/// Surinuke 此处作动词使用,意思是「歪了」。
public var isSurinuked: Bool {
guard rankType == .rank5, pool.isSurinukable else { return true }
switch game {
case .starRail:
return switch itemID {
case "1003": true // Nanashibito: Himeko
case "1004": true // Nanashibito: Welt
case "1101": true // Belobog: Bronya
case "1104": true // Belobog: Gepard
case "1107": true // Belobog: Clara
case "1209": true // Luofu: Yanqing
case "1211": true // Luofu: Bailu
case "23000": true // 银河铁道之夜 (Himeko)
case "23002": true // 无可取代的东西 (Clara)
case "23003": true // 但战鬥还未结束 (Bronya)
case "23004": true // 以世界之名 (Welt)
case "23005": true // 制胜的瞬间 (Gepard)
case "23012": true // 如泥酣眠 (Yanqing)
case "23013": true // 时节不居 (Bailu)
default: false
}
case .genshinImpact:
return switch itemID {
case "15502": true // 阿莫斯之弓
case "15501": true // 天空之翼
case "14502": true // 四风原典
case "14501": true // 天空之卷
case "13505": true // 和璞鸢
case "13502": true // 天空之脊
case "12502": true // 狼的末路
case "12501": true // 天空之傲
case "11501": true // 风鹰剑
case "11502": true // 天空之刃
case "10000016": true // Diluc
case "10000003": true // Jean
case "10000035": true // Qiqi
case "10000041": true // Mona
case "10000042": // Keqing
checkSurinukeByTime(
from: .init(year: 2021, month: 2, day: 17),
to: .init(year: 2021, month: 3, day: 2)
)
case "10000069": // Tighnari
checkSurinukeByTime(
from: .init(year: 2022, month: 8, day: 24),
to: .init(year: 2022, month: 9, day: 9)
)
case "10000079": // Dehya
checkSurinukeByTime(
from: .init(year: 2023, month: 3, day: 1),
to: .init(year: 2023, month: 3, day: 21)
)
default: false
}
case .zenlessZone: return false // 暂不实作。
}
}

/// 处理一开始是限定五星、后来变成常驻五星的角色。
private func checkSurinukeByTime(from startDate: DateComponents, to endDate: DateComponents) -> Bool {
let calendar = Calendar(identifier: .gregorian)
let dateStarted = calendar.date(from: startDate)!
let dateEnded = calendar.date(from: endDate)!
guard dateStarted <= dateEnded else { return false } // 不这样处理的话,会 runtime error。
return !(dateStarted ... dateEnded).contains(time)
}
}

extension GachaItemExpressible {
public var itemType: GachaItemType {
guard let itemIDInt = Int(itemID) else { return .unknown }
switch game {
case .genshinImpact:
switch itemID.count {
case 8: return .character
case 5: return .weapon
default: return .unknown
}
case .starRail:
switch itemID.count {
case 4: return .character
case 5: return .weapon
default: return .unknown
}
case .zenlessZone:
switch itemID.count {
case 4: return .character
case 5: switch itemIDInt {
case 50000...: return .bangboo
default: return .weapon
}
default: return .unknown
}
}
}

/// 建议在实际使用时以该变数取代用作垫底值的 rankType。
public var rarity: GachaItemRankType {
var rarityInt: Int?
switch game {
case .genshinImpact: rarityInt = GachaMetaDBExposed.shared.mainDB4GI.plainQueryForRarity(itemID: itemID)
case .starRail: rarityInt = GachaMetaDBExposed.shared.mainDB4HSR.plainQueryForRarity(itemID: itemID)
case .zenlessZone: rarityInt = nil // 警告:绝区零的 rankType 需要 +1 才能用。
}
return switch rarityInt {
case 3: .rank3
case 4: .rank4
case 5: .rank5
default: rankType
}
}

public func nameLocalized(for lang: GachaLanguage = .current, realName: Bool = true) -> String {
switch game {
case .genshinImpact:
var result: String?
if lang == .current {
result = Enka.Sputnik.shared.db4GI.getFailableTranslationFor(id: itemID, realName: realName)
} else {
result = nil
}
return result ?? GachaMetaDBExposed.shared.mainDB4GI.plainQueryForNames(
itemID: itemID,
langID: lang.rawValue
)
?? name
case .starRail:
var result: String?
if lang == .current {
result = Enka.Sputnik.shared.db4HSR.getFailableTranslationFor(id: itemID, realName: realName)
} else {
result = nil
}
return result ?? GachaMetaDBExposed.shared.mainDB4HSR
.plainQueryForNames(itemID: itemID, langID: lang.rawValue) ?? name
case .zenlessZone: return name // 暂不处理。
}
}

// 如果是大图表的话,建议尺寸是 40;否则是 30。
@MainActor @ViewBuilder
public func icon(_ size: CGFloat = 30) -> some View {
switch (game, itemType) {
case (_, .unknown): AnonymousIconView(size, cutType: .circleClipped)
case (.zenlessZone, .bangboo): AnonymousIconView(size, cutType: .circleClipped).colorMultiply(.red)
case (_, .character): CharacterIconView(charID: itemID, size: size, circleClipped: true, clipToHead: true)
case (.genshinImpact, .weapon): Enka.queryImageAssetSUI(for: "gi_weapon_\(itemID)")
case (.starRail, .weapon): Enka.queryImageAssetSUI(for: "hsr_light_cone_\(itemID)")
default: AnonymousIconView(size, cutType: .circleClipped).colorMultiply(.gray)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,20 @@ import SwiftData

// MARK: - PZGachaEntryProtocol

public protocol PZGachaEntryProtocol {}
public protocol PZGachaEntryProtocol {
var game: Pizza.SupportedGame { get set }
var uid: String { get set }
var gachaType: String { get set }
var itemID: String { get set }
var count: String { get set }
var time: String { get set }
var name: String { get set }
var lang: String { get set }
var itemType: String { get set }
var rankType: String { get set }
var id: String { get set }
var gachaID: String { get set }
}

// MARK: - PZGachaEntryMO

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,20 @@
// This code is released under the SPDX-License-Identifier: `AGPL-3.0-or-later`.

import Foundation
import PZAccountKit
import PZBaseKit

// MARK: - GachaItemType

enum GachaItemType: String {
public enum GachaItemType: String, Sendable, Hashable, Codable, Identifiable {
case character
case weapon
case bangboo /// ZZZ Only
case unknown
case bangboo /// ZZZ Only.

// MARK: Public

public var id: String { rawValue }
}

extension GachaItemType {
Expand All @@ -23,7 +29,7 @@ extension GachaItemType {
case 5...: self = .weapon
default: self = .character
}
case .zenlessZone: self = .bangboo // 暂时不处理。
case .zenlessZone: self = .weapon // 暂时不处理。
}
}

Expand All @@ -41,7 +47,7 @@ extension GachaItemType {
}
}

public func getTranslatedRaw(for lang: GachaLanguage, game: Pizza.SupportedGame) -> String {
public func getTranslatedRaw(for lang: GachaLanguage = .current, game: Pizza.SupportedGame) -> String {
switch game {
case .genshinImpact:
switch (self, lang) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,62 @@
// ====================
// This code is released under the SPDX-License-Identifier: `AGPL-3.0-or-later`.

public enum GachaItemRankType: Int {
case three = 3
case four = 4
case five = 5
import Foundation
import PZBaseKit
import SwiftUI

// MARK: - GachaItemRankType

public enum GachaItemRankType: Int, Identifiable, Sendable, Hashable, Codable {
case rank3 = 3
case rank4 = 4
case rank5 = 5

// MARK: Lifecycle

public init?(rawValueStr: String) {
guard let intRawValue = Int(rawValueStr) else { return nil }
public init?(rawValueStr: String, game: Pizza.SupportedGame) {
guard var intRawValue = Int(rawValueStr) else { return nil }
if game == .zenlessZone { intRawValue += 1 }
switch intRawValue {
case 3: self = .three
case 4: self = .four
case 5: self = .five
case 3: self = .rank3
case 4: self = .rank4
case 5: self = .rank5
default: return nil
}
}

// MARK: Public

public var id: Int { rawValue }

public func uigfRankType(game: Pizza.SupportedGame) -> String {
var rawValueInt = rawValue
if game == .zenlessZone { rawValueInt -= 1 }
return rawValueInt.description
}
}

// MARK: - Background Gradients.

extension GachaItemRankType {
private var gradientColors: [CGColor] {
switch self {
case .rank3: [
CGColor(red: 0.34, green: 0.45, blue: 0.59, alpha: 1.00),
CGColor(red: 0.33, green: 0.57, blue: 0.72, alpha: 1.00),
]
case .rank4: [
CGColor(red: 0.37, green: 0.34, blue: 0.54, alpha: 1.00),
CGColor(red: 0.61, green: 0.46, blue: 0.72, alpha: 1.00),
]
case .rank5: [
CGColor(red: 0.58, green: 0.36, blue: 0.17, alpha: 1.00),
CGColor(red: 0.70, green: 0.45, blue: 0.19, alpha: 1.00),
]
}
}

public var backgroundGradient: LinearGradient {
.init(colors: gradientColors.map { Color(cgColor: $0) }, startPoint: .top, endPoint: .bottom)
}
}
Loading

0 comments on commit 824fefa

Please sign in to comment.