983 lines
24 KiB
Go
983 lines
24 KiB
Go
package service
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/json"
|
||
"equipment-analyzer/internal/config"
|
||
"equipment-analyzer/internal/model"
|
||
"equipment-analyzer/internal/parser"
|
||
"equipment-analyzer/internal/utils"
|
||
"fmt"
|
||
"io/ioutil"
|
||
"math"
|
||
"os"
|
||
"path/filepath"
|
||
"sort"
|
||
"net/http"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
)
|
||
|
||
type ParserService struct {
|
||
config *config.Config
|
||
logger *utils.Logger
|
||
hexParser *parser.HexParser
|
||
heroBase map[string]heroBaseStats
|
||
heroOnce sync.Once
|
||
heroErr error
|
||
fallbackMainStatCount int
|
||
}
|
||
|
||
func NewParserService(cfg *config.Config, logger *utils.Logger) *ParserService {
|
||
return &ParserService{
|
||
config: cfg,
|
||
logger: logger,
|
||
hexParser: parser.NewHexParser(),
|
||
heroBase: make(map[string]heroBaseStats),
|
||
}
|
||
}
|
||
|
||
|
||
func (ps *ParserService) writeParseSnapshot(kind string, data []byte) {
|
||
if len(data) == 0 {
|
||
return
|
||
}
|
||
homeDir, err := os.UserHomeDir()
|
||
if err != nil {
|
||
return
|
||
}
|
||
dir := filepath.Join(homeDir, ".equipment-analyzer", "remote_parse_snapshots")
|
||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||
return
|
||
}
|
||
stamp := time.Now().Format("20060102_150405.000")
|
||
filename := filepath.Join(dir, stamp+"_"+kind+".json")
|
||
_ = os.WriteFile(filename, data, 0644)
|
||
}
|
||
|
||
// ParseHexData 解析十六进制数据
|
||
func (ps *ParserService) ParseHexData(hexDataList []string) (*model.ParsedResult, string, error) {
|
||
if len(hexDataList) == 0 {
|
||
ps.logger.Warn("no data to parse")
|
||
return &model.ParsedResult{
|
||
Items: make([]interface{}, 0),
|
||
Heroes: make([]interface{}, 0),
|
||
}, "", nil
|
||
}
|
||
|
||
ps.logger.Info("remote parse start", "count", len(hexDataList))
|
||
|
||
url := "https://krivpfvxi0.execute-api.us-west-2.amazonaws.com/dev/getItems"
|
||
reqBody := map[string]interface{}{
|
||
"data": hexDataList,
|
||
}
|
||
jsonBytes, _ := json.Marshal(reqBody)
|
||
ps.writeParseSnapshot("request", jsonBytes)
|
||
client := &http.Client{Timeout: 60 * time.Second}
|
||
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonBytes))
|
||
if err == nil {
|
||
req.Header.Set("Content-Type", "application/json")
|
||
resp, err := client.Do(req)
|
||
if err == nil && resp.StatusCode == 200 {
|
||
defer resp.Body.Close()
|
||
body, _ := ioutil.ReadAll(resp.Body)
|
||
ps.writeParseSnapshot("response", body)
|
||
|
||
var raw map[string]interface{}
|
||
if err := json.Unmarshal(body, &raw); err != nil {
|
||
ps.logger.Error("remote json unmarshal failed", "error", err)
|
||
return nil, "", fmt.Errorf("remote json unmarshal failed: %v", err)
|
||
}
|
||
|
||
dataArr, dataOk := raw["data"].([]interface{})
|
||
if !dataOk || len(dataArr) == 0 {
|
||
ps.logger.Error("remote json validate failed: data missing or empty")
|
||
return nil, "", fmt.Errorf("remote json validate failed: data missing or empty")
|
||
}
|
||
|
||
ps.logger.Info("remote json validate ok, start parse")
|
||
parsedResult, err := ps.ReadRawJsonFile(string(body))
|
||
if err != nil {
|
||
return nil, "", err
|
||
}
|
||
|
||
return parsedResult, "", nil
|
||
} else if err != nil {
|
||
ps.logger.Error("remote parse request failed", "error", err)
|
||
return nil, "", fmt.Errorf("remote parse request failed: %v", err)
|
||
} else {
|
||
ps.logger.Error("remote parse http status", "status", resp.StatusCode)
|
||
return nil, "", fmt.Errorf("remote parse http status %d", resp.StatusCode)
|
||
}
|
||
} else {
|
||
ps.logger.Error("remote parse request build failed", "error", err)
|
||
return nil, "", fmt.Errorf("remote parse request build failed: %v", err)
|
||
}
|
||
}
|
||
|
||
// ReadRawJsonFile 读取rawJson内容并进行数据转换,返回ParsedResult对象
|
||
func (ps *ParserService) ReadRawJsonFile(rawJson string) (*model.ParsedResult, error) {
|
||
var rawData map[string]interface{}
|
||
if err := json.Unmarshal([]byte(rawJson), &rawData); err != nil {
|
||
ps.logger.Error("解析JSON失败", "error", err)
|
||
return nil, err
|
||
}
|
||
|
||
|
||
// If input is already parsed (gear.txt export), use items/heroes directly.
|
||
if itemsRaw, ok := rawData["items"].([]interface{}); ok {
|
||
heroesRaw, _ := rawData["heroes"].([]interface{})
|
||
parsedItems := ps.normalizeParsedItems(itemsRaw)
|
||
result := &model.ParsedResult{
|
||
Items: parsedItems,
|
||
Heroes: heroesRaw,
|
||
GearTxt: rawJson,
|
||
}
|
||
return result, nil
|
||
}
|
||
|
||
// If input is a mixed "data" array (remote parse response), split items/heroes.
|
||
if dataRaw, ok := rawData["data"].([]interface{}); ok && len(dataRaw) > 0 {
|
||
dataItems, dataHeroes := ps.splitDataArray(dataRaw)
|
||
if len(dataItems) > 0 || len(dataHeroes) > 0 {
|
||
ps.logger.Info("raw json format: mixed data array", "items", len(dataItems), "heroes", len(dataHeroes))
|
||
// If heroes are not present in data array, fall back to units.
|
||
var rawUnits []interface{}
|
||
if unitsRaw, ok := rawData["units"].([]interface{}); ok && len(unitsRaw) > 0 {
|
||
maxLen := 0
|
||
for _, u := range unitsRaw {
|
||
if arr, ok := u.([]interface{}); ok && len(arr) > maxLen {
|
||
maxLen = len(arr)
|
||
rawUnits = arr
|
||
}
|
||
}
|
||
}
|
||
|
||
parsedHeroes := dataHeroes
|
||
if len(rawUnits) > 0 {
|
||
parsedHeroes = rawUnits
|
||
}
|
||
|
||
ps.fallbackMainStatCount = 0
|
||
convertedItems := ps.convertItemsAllWithLog(dataItems)
|
||
ps.logger.Info("main stat fallback count", "count", ps.fallbackMainStatCount)
|
||
convertedHeroes := ps.convertUnits(parsedHeroes)
|
||
|
||
result := &model.ParsedResult{
|
||
Items: make([]interface{}, len(convertedItems)),
|
||
Heroes: make([]interface{}, len(convertedHeroes)),
|
||
}
|
||
for i, v := range convertedItems {
|
||
result.Items[i] = v
|
||
}
|
||
for i, v := range convertedHeroes {
|
||
result.Heroes[i] = v
|
||
}
|
||
result.GearTxt = rawJson
|
||
return result, nil
|
||
}
|
||
}
|
||
|
||
// 提取装备和英雄数?
|
||
equips, _ := rawData["data"].([]interface{})
|
||
// 修正:units 取最大长度的那组
|
||
var rawUnits []interface{}
|
||
if unitsRaw, ok := rawData["units"].([]interface{}); ok && len(unitsRaw) > 0 {
|
||
maxLen := 0
|
||
for _, u := range unitsRaw {
|
||
if arr, ok := u.([]interface{}); ok && len(arr) > maxLen {
|
||
maxLen = len(arr)
|
||
rawUnits = arr
|
||
}
|
||
}
|
||
}
|
||
|
||
// 过滤有效装备 (x => !!x.f)
|
||
var validEquips []interface{}
|
||
for _, equip := range equips {
|
||
if equipMap, ok := equip.(map[string]interface{}); ok {
|
||
if f, exists := equipMap["f"]; exists && f != nil && f != "" {
|
||
validEquips = append(validEquips, equip)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 转换装备数据
|
||
ps.fallbackMainStatCount = 0
|
||
convertedItems := ps.convertItemsAllWithLog(validEquips)
|
||
ps.logger.Info("main stat fallback count", "count", ps.fallbackMainStatCount)
|
||
// 转换英雄数据(只对最大组?
|
||
convertedHeroes := ps.convertUnits(rawUnits)
|
||
|
||
result := &model.ParsedResult{
|
||
Items: make([]interface{}, len(convertedItems)),
|
||
Heroes: make([]interface{}, len(convertedHeroes)),
|
||
}
|
||
for i, v := range convertedItems {
|
||
result.Items[i] = v
|
||
}
|
||
for i, v := range convertedHeroes {
|
||
result.Heroes[i] = v
|
||
}
|
||
result.GearTxt = rawJson
|
||
|
||
return result, nil
|
||
}
|
||
|
||
|
||
func (ps *ParserService) isGearRecord(itemMap map[string]interface{}) bool {
|
||
// Require set field if present in this raw format.
|
||
if f, ok := itemMap["f"].(string); ok && f != "" {
|
||
return true
|
||
}
|
||
// Allow already-normalized gear fields from other sources.
|
||
if _, ok := itemMap["set"]; ok {
|
||
return true
|
||
}
|
||
if _, ok := itemMap["gear"]; ok {
|
||
return true
|
||
}
|
||
if _, ok := itemMap["type"]; ok {
|
||
return true
|
||
}
|
||
if _, ok := itemMap["mainStatType"]; ok {
|
||
return true
|
||
}
|
||
// Otherwise treat as non-gear to avoid including material-like records.
|
||
return false
|
||
}
|
||
|
||
func (ps *ParserService) isHeroRecord(itemMap map[string]interface{}) bool {
|
||
if name, ok := itemMap["name"].(string); ok && name != "" && name != "Unknown" {
|
||
return true
|
||
}
|
||
if code, ok := itemMap["code"].(string); ok && strings.HasPrefix(code, "c") {
|
||
return true
|
||
}
|
||
// heroes commonly have opt/exp fields
|
||
if _, ok := itemMap["opt"]; ok {
|
||
return true
|
||
}
|
||
if _, ok := itemMap["exp"]; ok {
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
|
||
func (ps *ParserService) splitDataArray(data []interface{}) (items []interface{}, heroes []interface{}) {
|
||
for _, entry := range data {
|
||
itemMap, ok := entry.(map[string]interface{})
|
||
if !ok || itemMap == nil {
|
||
continue
|
||
}
|
||
if ps.isGearRecord(itemMap) {
|
||
items = append(items, entry)
|
||
continue
|
||
}
|
||
if ps.isHeroRecord(itemMap) {
|
||
heroes = append(heroes, entry)
|
||
continue
|
||
}
|
||
}
|
||
return items, heroes
|
||
}
|
||
|
||
|
||
func (ps *ParserService) normalizeParsedItems(items []interface{}) []interface{} {
|
||
for i, item := range items {
|
||
itemMap, ok := item.(map[string]interface{})
|
||
if !ok || itemMap == nil {
|
||
continue
|
||
}
|
||
if _, exists := itemMap["main"]; exists {
|
||
continue
|
||
}
|
||
op, opExists := itemMap["op"].([]interface{})
|
||
if !opExists || len(op) == 0 {
|
||
continue
|
||
}
|
||
mainOp, ok := op[0].([]interface{})
|
||
if !ok || len(mainOp) < 2 {
|
||
continue
|
||
}
|
||
mainOpType, _ := mainOp[0].(string)
|
||
mainOpValue, _ := mainOp[1].(float64)
|
||
if mainOpType == "" {
|
||
continue
|
||
}
|
||
mainType := statByIngameStat[mainOpType]
|
||
if mainType == "" {
|
||
continue
|
||
}
|
||
var mainValue float64
|
||
if ps.isFlat(mainOpType) {
|
||
mainValue = mainOpValue
|
||
} else {
|
||
mainValue = ps.round10ths(mainOpValue * 100)
|
||
}
|
||
if mainValue == 0 || mainValue != mainValue {
|
||
mainValue = 0
|
||
}
|
||
itemMap["main"] = map[string]interface{}{
|
||
"type": mainType,
|
||
"value": mainValue,
|
||
}
|
||
items[i] = itemMap
|
||
}
|
||
return items
|
||
}
|
||
|
||
// convertItems 转换装备数据
|
||
func (ps *ParserService) convertItems(rawItems []interface{}) []map[string]interface{} {
|
||
var convertedItems []map[string]interface{}
|
||
|
||
for _, rawItem := range rawItems {
|
||
if itemMap, ok := rawItem.(map[string]interface{}); ok {
|
||
convertedItem := ps.convertSingleItem(itemMap)
|
||
if convertedItem != nil {
|
||
convertedItems = append(convertedItems, convertedItem)
|
||
}
|
||
}
|
||
}
|
||
|
||
var filteredItems []map[string]interface{}
|
||
for _, item := range convertedItems {
|
||
filteredItems = append(filteredItems, item)
|
||
}
|
||
|
||
return filteredItems
|
||
}
|
||
|
||
// convertSingleItem 转换单个装备
|
||
func (ps *ParserService) convertSingleItem(item map[string]interface{}) map[string]interface{} {
|
||
converted := make(map[string]interface{})
|
||
|
||
// 复制基本字段
|
||
for key, value := range item {
|
||
converted[key] = value
|
||
}
|
||
|
||
// 转换装备类型
|
||
ps.convertGear(converted)
|
||
|
||
// 转换等级
|
||
ps.convertRank(converted)
|
||
|
||
// 转换套装
|
||
ps.convertSet(converted)
|
||
|
||
// 转换名称
|
||
ps.convertName(converted)
|
||
|
||
// 转换等级
|
||
ps.convertLevel(converted)
|
||
|
||
// 转换增强
|
||
ps.convertEnhance(converted)
|
||
|
||
// 转换主属?
|
||
ps.convertMainStat(converted)
|
||
|
||
// 转换副属?
|
||
ps.convertSubStats(converted)
|
||
|
||
// 转换ID
|
||
ps.convertId(converted)
|
||
|
||
// 转换装备ID
|
||
ps.convertEquippedId(converted)
|
||
|
||
return converted
|
||
}
|
||
|
||
// convertUnits 转换英雄数据
|
||
func (ps *ParserService) convertUnits(rawUnits []interface{}) []map[string]interface{} {
|
||
var convertedUnits []map[string]interface{}
|
||
ps.ensureHeroBaseData()
|
||
|
||
|
||
for _, rawUnit := range rawUnits {
|
||
if unitMap, ok := rawUnit.(map[string]interface{}); ok {
|
||
if name, exists := unitMap["name"]; exists && name != nil && name != "" {
|
||
if id, exists := unitMap["id"]; exists && id != nil {
|
||
convertedUnit := make(map[string]interface{})
|
||
for key, value := range unitMap {
|
||
convertedUnit[key] = value
|
||
}
|
||
|
||
// 转换星星和觉?
|
||
if g, exists := unitMap["g"]; exists {
|
||
convertedUnit["stars"] = g
|
||
}
|
||
if z, exists := unitMap["z"]; exists {
|
||
convertedUnit["awaken"] = z
|
||
}
|
||
|
||
if code, ok := unitMap["code"].(string); ok && code != "" {
|
||
if base, ok := ps.heroBase[code]; ok {
|
||
ps.applyHeroBase(convertedUnit, base)
|
||
}
|
||
}
|
||
|
||
convertedUnits = append(convertedUnits, convertedUnit)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return convertedUnits
|
||
}
|
||
|
||
// 转换函数实现
|
||
func (ps *ParserService) convertGear(item map[string]interface{}) {
|
||
if _, exists := item["type"]; !exists {
|
||
if code, exists := item["code"].(string); exists {
|
||
baseCode := code
|
||
if parts := strings.Split(code, "_"); len(parts) > 0 {
|
||
baseCode = parts[0]
|
||
}
|
||
if idx := len(baseCode) - 1; idx >= 0 {
|
||
gearLetter := string(baseCode[idx])
|
||
item["gear"] = gearByGearLetter[gearLetter]
|
||
}
|
||
}
|
||
} else {
|
||
if itemType, exists := item["type"].(string); exists {
|
||
item["gear"] = gearByIngameType[itemType]
|
||
}
|
||
}
|
||
}
|
||
|
||
func (ps *ParserService) convertRank(item map[string]interface{}) {
|
||
if g, exists := item["g"].(float64); exists {
|
||
rankIndex := int(g)
|
||
if rankIndex >= 0 && rankIndex < len(rankByIngameGrade) {
|
||
item["rank"] = rankByIngameGrade[rankIndex]
|
||
}
|
||
}
|
||
}
|
||
|
||
func (ps *ParserService) convertSet(item map[string]interface{}) {
|
||
if f, exists := item["f"].(string); exists {
|
||
item["set"] = setsByIngameSet[f]
|
||
}
|
||
}
|
||
|
||
func (ps *ParserService) convertName(item map[string]interface{}) {
|
||
if _, exists := item["name"]; !exists {
|
||
item["name"] = "Unknown"
|
||
}
|
||
}
|
||
|
||
func (ps *ParserService) convertLevel(item map[string]interface{}) {
|
||
if _, exists := item["level"]; !exists {
|
||
item["level"] = 0
|
||
}
|
||
}
|
||
|
||
func (ps *ParserService) convertEnhance(item map[string]interface{}) {
|
||
rank, rankExists := item["rank"].(string)
|
||
op, opExists := item["op"].([]interface{})
|
||
|
||
if rankExists && opExists {
|
||
countByRank := map[string]int{
|
||
"Normal": 5,
|
||
"Good": 6,
|
||
"Rare": 7,
|
||
"Heroic": 8,
|
||
"Epic": 9,
|
||
}
|
||
offsetByRank := map[string]int{
|
||
"Normal": 0,
|
||
"Good": 1,
|
||
"Rare": 2,
|
||
"Heroic": 3,
|
||
"Epic": 4,
|
||
}
|
||
|
||
count := countByRank[rank]
|
||
offset := offsetByRank[rank]
|
||
subsCount := len(op) - 1
|
||
if subsCount > count {
|
||
subsCount = count
|
||
}
|
||
|
||
enhance := (subsCount - offset) * 3
|
||
if enhance < 0 {
|
||
enhance = 0
|
||
}
|
||
item["enhance"] = enhance
|
||
}
|
||
}
|
||
|
||
func (ps *ParserService) convertMainStat(item map[string]interface{}) {
|
||
op, opExists := item["op"].([]interface{})
|
||
mainStatValue, mainStatExists := item["mainStatValue"].(float64)
|
||
fallbackUsed := false
|
||
|
||
if opExists && len(op) > 0 && mainStatExists {
|
||
if mainOp, ok := op[0].([]interface{}); ok && len(mainOp) > 0 {
|
||
if mainOpType, ok := mainOp[0].(string); ok {
|
||
mainType := statByIngameStat[mainOpType]
|
||
var mainValue float64
|
||
|
||
if ps.isFlat(mainOpType) {
|
||
mainValue = mainStatValue
|
||
} else {
|
||
mainValue = ps.round10ths(mainStatValue * 100)
|
||
}
|
||
|
||
if mainValue == 0 || mainValue != mainValue { // NaN check
|
||
mainValue = 0
|
||
}
|
||
|
||
item["main"] = map[string]interface{}{
|
||
"type": mainType,
|
||
"value": mainValue,
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Fallback: if main is missing, derive from op[0] value directly.
|
||
if _, exists := item["main"]; !exists {
|
||
if opExists && len(op) > 0 {
|
||
if mainOp, ok := op[0].([]interface{}); ok && len(mainOp) >= 2 {
|
||
mainOpType, _ := mainOp[0].(string)
|
||
mainOpValue, _ := mainOp[1].(float64)
|
||
if mainOpType != "" {
|
||
mainType := statByIngameStat[mainOpType]
|
||
if mainType != "" {
|
||
var mainValue float64
|
||
if ps.isFlat(mainOpType) {
|
||
mainValue = mainOpValue
|
||
} else {
|
||
mainValue = ps.round10ths(mainOpValue * 100)
|
||
}
|
||
if mainValue == 0 || mainValue != mainValue {
|
||
mainValue = 0
|
||
}
|
||
item["main"] = map[string]interface{}{
|
||
"type": mainType,
|
||
"value": mainValue,
|
||
}
|
||
fallbackUsed = true
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if fallbackUsed {
|
||
ps.fallbackMainStatCount++
|
||
}
|
||
}
|
||
|
||
func (ps *ParserService) convertSubStats(item map[string]interface{}) {
|
||
op, opExists := item["op"].([]interface{})
|
||
|
||
if !opExists || len(op) <= 1 {
|
||
item["substats"] = []interface{}{}
|
||
return
|
||
}
|
||
|
||
statAcc := make(map[string]map[string]interface{})
|
||
|
||
// 处理副属?(从索?开?
|
||
for i := 1; i < len(op); i++ {
|
||
if opItem, ok := op[i].([]interface{}); ok && len(opItem) >= 2 {
|
||
opType, _ := opItem[0].(string)
|
||
opValue, _ := opItem[1].(float64)
|
||
|
||
annotation := ""
|
||
if len(opItem) > 2 {
|
||
annotation, _ = opItem[2].(string)
|
||
}
|
||
|
||
statType := statByIngameStat[opType]
|
||
var value float64
|
||
|
||
if ps.isFlat(opType) {
|
||
value = opValue
|
||
} else {
|
||
value = ps.round10ths(opValue * 100)
|
||
}
|
||
|
||
if existingStat, exists := statAcc[statType]; exists {
|
||
existingStat["value"] = existingStat["value"].(float64) + value
|
||
|
||
if annotation == "c" {
|
||
existingStat["modified"] = true
|
||
} else if annotation != "u" {
|
||
rolls := existingStat["rolls"].(int) + 1
|
||
existingStat["rolls"] = rolls
|
||
existingStat["ingameRolls"] = rolls
|
||
}
|
||
} else {
|
||
rolls := 1
|
||
if annotation == "u" {
|
||
rolls = 0
|
||
}
|
||
|
||
statAcc[statType] = map[string]interface{}{
|
||
"value": value,
|
||
"rolls": rolls,
|
||
"ingameRolls": rolls,
|
||
"modified": annotation == "c",
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 转换为最终格?
|
||
var substats []interface{}
|
||
for statType, statData := range statAcc {
|
||
substat := map[string]interface{}{
|
||
"type": statType,
|
||
"value": statData["value"],
|
||
"rolls": statData["rolls"],
|
||
"ingameRolls": statData["ingameRolls"],
|
||
"modified": statData["modified"],
|
||
}
|
||
substats = append(substats, substat)
|
||
}
|
||
|
||
item["substats"] = substats
|
||
}
|
||
|
||
func (ps *ParserService) convertId(item map[string]interface{}) {
|
||
if id, exists := item["id"]; exists {
|
||
item["ingameId"] = id
|
||
}
|
||
}
|
||
|
||
func (ps *ParserService) convertEquippedId(item map[string]interface{}) {
|
||
if p, exists := item["p"]; exists {
|
||
item["ingameEquippedId"] = fmt.Sprintf("%v", p)
|
||
}
|
||
}
|
||
|
||
func (ps *ParserService) isFlat(text string) bool {
|
||
return text == "max_hp" || text == "speed" || text == "att" || text == "def"
|
||
}
|
||
|
||
func (ps *ParserService) round10ths(value float64) float64 {
|
||
return math.Round(value*10) / 10
|
||
}
|
||
|
||
// 映射常量
|
||
var (
|
||
rankByIngameGrade = []string{
|
||
"Unknown",
|
||
"Normal",
|
||
"Good",
|
||
"Rare",
|
||
"Heroic",
|
||
"Epic",
|
||
}
|
||
|
||
gearByIngameType = map[string]string{
|
||
"weapon": "Weapon",
|
||
"helm": "Helmet",
|
||
"armor": "Armor",
|
||
"neck": "Necklace",
|
||
"ring": "Ring",
|
||
"boot": "Boots",
|
||
}
|
||
|
||
gearByGearLetter = map[string]string{
|
||
"w": "Weapon",
|
||
"h": "Helmet",
|
||
"a": "Armor",
|
||
"n": "Necklace",
|
||
"r": "Ring",
|
||
"b": "Boots",
|
||
}
|
||
|
||
setsByIngameSet = map[string]string{
|
||
"set_acc": "HitSet",
|
||
"set_att": "AttackSet",
|
||
"set_coop": "UnitySet",
|
||
"set_counter": "CounterSet",
|
||
"set_cri_dmg": "DestructionSet",
|
||
"set_cri": "CriticalSet",
|
||
"set_def": "DefenseSet",
|
||
"set_immune": "ImmunitySet",
|
||
"set_max_hp": "HealthSet",
|
||
"set_penetrate": "PenetrationSet",
|
||
"set_rage": "RageSet",
|
||
"set_res": "ResistSet",
|
||
"set_revenge": "RevengeSet",
|
||
"set_scar": "InjurySet",
|
||
"set_speed": "SpeedSet",
|
||
"set_vampire": "LifestealSet",
|
||
"set_shield": "ProtectionSet",
|
||
"set_torrent": "TorrentSet",
|
||
"set_revenant": "ReversalSet",
|
||
"set_riposte": "RiposteSet",
|
||
"set_chase": "PursuitSet",
|
||
"set_opener": "WarfareSet",
|
||
}
|
||
|
||
statByIngameStat = map[string]string{
|
||
"att_rate": "AttackPercent",
|
||
"max_hp_rate": "HealthPercent",
|
||
"def_rate": "DefensePercent",
|
||
"att": "Attack",
|
||
"max_hp": "Health",
|
||
"def": "Defense",
|
||
"speed": "Speed",
|
||
"res": "EffectResistancePercent",
|
||
"cri": "CriticalHitChancePercent",
|
||
"cri_dmg": "CriticalHitDamagePercent",
|
||
"acc": "EffectivenessPercent",
|
||
"coop": "DualAttackChancePercent",
|
||
}
|
||
)
|
||
|
||
// 新增:不做enhance过滤的convertItems
|
||
func (ps *ParserService) convertItemsAllWithLog(rawItems []interface{}) []map[string]interface{} {
|
||
var convertedItems []map[string]interface{}
|
||
for _, rawItem := range rawItems {
|
||
if itemMap, ok := rawItem.(map[string]interface{}); ok {
|
||
convertedItem := ps.convertSingleItem(itemMap)
|
||
if convertedItem != nil {
|
||
convertedItems = append(convertedItems, convertedItem)
|
||
}
|
||
}
|
||
}
|
||
return convertedItems
|
||
}
|
||
|
||
|
||
type heroBaseStats struct {
|
||
Atk float64
|
||
Def float64
|
||
Hp float64
|
||
Spd float64
|
||
Cr float64
|
||
Cd float64
|
||
Eff float64
|
||
Res float64
|
||
}
|
||
|
||
type heroDataEntry struct {
|
||
Code string `json:"code"`
|
||
Name string `json:"name"`
|
||
CalculatedStatus struct {
|
||
Lv60SixStarFullyAwakened struct {
|
||
Atk float64 `json:"atk"`
|
||
Def float64 `json:"def"`
|
||
Hp float64 `json:"hp"`
|
||
Spd float64 `json:"spd"`
|
||
Chc float64 `json:"chc"`
|
||
Chd float64 `json:"chd"`
|
||
Eff float64 `json:"eff"`
|
||
Efr float64 `json:"efr"`
|
||
} `json:"lv60SixStarFullyAwakened"`
|
||
} `json:"calculatedStatus"`
|
||
}
|
||
|
||
func (ps *ParserService) ensureHeroBaseData() {
|
||
ps.heroOnce.Do(func() {
|
||
homeDir, err := os.UserHomeDir()
|
||
if err != nil {
|
||
ps.heroErr = err
|
||
ps.logger.Error("load hero data failed", "error", err)
|
||
return
|
||
}
|
||
path := filepath.Join(homeDir, ".equipment-analyzer", "herodata.json")
|
||
body, err := ioutil.ReadFile(path)
|
||
if err != nil {
|
||
ps.heroErr = err
|
||
ps.logger.Error("load hero data failed", "error", err)
|
||
return
|
||
}
|
||
var raw map[string]heroDataEntry
|
||
if err := json.Unmarshal(body, &raw); err != nil {
|
||
ps.heroErr = err
|
||
ps.logger.Error("load hero data failed", "error", err)
|
||
return
|
||
}
|
||
for _, entry := range raw {
|
||
if entry.Code == "" {
|
||
continue
|
||
}
|
||
stats := entry.CalculatedStatus.Lv60SixStarFullyAwakened
|
||
ps.heroBase[entry.Code] = heroBaseStats{
|
||
Atk: stats.Atk,
|
||
Def: stats.Def,
|
||
Hp: stats.Hp,
|
||
Spd: stats.Spd,
|
||
Cr: stats.Chc * 100,
|
||
Cd: stats.Chd * 100,
|
||
Eff: stats.Eff * 100,
|
||
Res: stats.Efr * 100,
|
||
}
|
||
}
|
||
ps.logger.Info("hero base data loaded", "count", len(ps.heroBase))
|
||
})
|
||
}
|
||
|
||
|
||
func (ps *ParserService) FillHeroBase(heroes []model.OptimizeHero) error {
|
||
ps.ensureHeroBaseData()
|
||
if len(ps.heroBase) == 0 {
|
||
return fmt.Errorf("hero base data not loaded")
|
||
}
|
||
for i := range heroes {
|
||
code := heroes[i].Code
|
||
if code == "" {
|
||
continue
|
||
}
|
||
base, ok := ps.heroBase[code]
|
||
if !ok {
|
||
continue
|
||
}
|
||
if heroes[i].BaseAtk == 0 {
|
||
heroes[i].BaseAtk = base.Atk
|
||
}
|
||
if heroes[i].BaseDef == 0 {
|
||
heroes[i].BaseDef = base.Def
|
||
}
|
||
if heroes[i].BaseHp == 0 {
|
||
heroes[i].BaseHp = base.Hp
|
||
}
|
||
if heroes[i].BaseSpd == 0 {
|
||
heroes[i].BaseSpd = base.Spd
|
||
}
|
||
if heroes[i].BaseCr == 0 {
|
||
heroes[i].BaseCr = base.Cr
|
||
}
|
||
if heroes[i].BaseCd == 0 {
|
||
heroes[i].BaseCd = base.Cd
|
||
}
|
||
if heroes[i].BaseEff == 0 {
|
||
heroes[i].BaseEff = base.Eff
|
||
}
|
||
if heroes[i].BaseRes == 0 {
|
||
heroes[i].BaseRes = base.Res
|
||
}
|
||
|
||
if heroes[i].Atk == 0 {
|
||
heroes[i].Atk = base.Atk
|
||
}
|
||
if heroes[i].Def == 0 {
|
||
heroes[i].Def = base.Def
|
||
}
|
||
if heroes[i].Hp == 0 {
|
||
heroes[i].Hp = base.Hp
|
||
}
|
||
if heroes[i].Spd == 0 {
|
||
heroes[i].Spd = base.Spd
|
||
}
|
||
if heroes[i].Cr == 0 {
|
||
heroes[i].Cr = base.Cr
|
||
}
|
||
if heroes[i].Cd == 0 {
|
||
heroes[i].Cd = base.Cd
|
||
}
|
||
if heroes[i].Eff == 0 {
|
||
heroes[i].Eff = base.Eff
|
||
}
|
||
if heroes[i].Res == 0 {
|
||
heroes[i].Res = base.Res
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (ps *ParserService) GetHeroTemplates() ([]model.HeroTemplate, error) {
|
||
homeDir, err := os.UserHomeDir()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
path := filepath.Join(homeDir, ".equipment-analyzer", "herodata.json")
|
||
body, err := ioutil.ReadFile(path)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var raw map[string]heroDataEntry
|
||
if err := json.Unmarshal(body, &raw); err != nil {
|
||
return nil, err
|
||
}
|
||
list := make([]model.HeroTemplate, 0, len(raw))
|
||
for _, entry := range raw {
|
||
if entry.Code == "" {
|
||
continue
|
||
}
|
||
stats := entry.CalculatedStatus.Lv60SixStarFullyAwakened
|
||
list = append(list, model.HeroTemplate{
|
||
Code: entry.Code,
|
||
Name: entry.Name,
|
||
BaseAtk: stats.Atk,
|
||
BaseDef: stats.Def,
|
||
BaseHp: stats.Hp,
|
||
BaseSpd: stats.Spd,
|
||
BaseCr: stats.Chc * 100,
|
||
BaseCd: stats.Chd * 100,
|
||
BaseEff: stats.Eff * 100,
|
||
BaseRes: stats.Efr * 100,
|
||
})
|
||
}
|
||
sort.Slice(list, func(i, j int) bool {
|
||
return list[i].Name < list[j].Name
|
||
})
|
||
return list, nil
|
||
}
|
||
|
||
func (ps *ParserService) applyHeroBase(unit map[string]interface{}, base heroBaseStats) {
|
||
if _, ok := unit["baseAtk"]; !ok {
|
||
unit["baseAtk"] = base.Atk
|
||
}
|
||
if _, ok := unit["baseDef"]; !ok {
|
||
unit["baseDef"] = base.Def
|
||
}
|
||
if _, ok := unit["baseHp"]; !ok {
|
||
unit["baseHp"] = base.Hp
|
||
}
|
||
if _, ok := unit["baseSpd"]; !ok {
|
||
unit["baseSpd"] = base.Spd
|
||
}
|
||
if _, ok := unit["baseCr"]; !ok {
|
||
unit["baseCr"] = base.Cr
|
||
}
|
||
if _, ok := unit["baseCd"]; !ok {
|
||
unit["baseCd"] = base.Cd
|
||
}
|
||
if _, ok := unit["baseEff"]; !ok {
|
||
unit["baseEff"] = base.Eff
|
||
}
|
||
if _, ok := unit["baseRes"]; !ok {
|
||
unit["baseRes"] = base.Res
|
||
}
|
||
|
||
if _, ok := unit["atk"]; !ok {
|
||
unit["atk"] = base.Atk
|
||
}
|
||
if _, ok := unit["def"]; !ok {
|
||
unit["def"] = base.Def
|
||
}
|
||
if _, ok := unit["hp"]; !ok {
|
||
unit["hp"] = base.Hp
|
||
}
|
||
if _, ok := unit["spd"]; !ok {
|
||
unit["spd"] = base.Spd
|
||
}
|
||
if _, ok := unit["cr"]; !ok {
|
||
unit["cr"] = base.Cr
|
||
}
|
||
if _, ok := unit["cd"]; !ok {
|
||
unit["cd"] = base.Cd
|
||
}
|
||
if _, ok := unit["eff"]; !ok {
|
||
unit["eff"] = base.Eff
|
||
}
|
||
if _, ok := unit["res"]; !ok {
|
||
unit["res"] = base.Res
|
||
}
|
||
}
|
||
|