This commit is contained in:
2026-04-02 15:34:18 +08:00
parent 6697955054
commit c54b75611d
18 changed files with 2281 additions and 0 deletions
+264
View File
@@ -0,0 +1,264 @@
package service
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"time"
)
var placeholderPattern = regexp.MustCompile(`<(?:(?:int)|(?:float\d*))>`)
var filenameJobPrefix = regexp.MustCompile(`^\d+(?:-\d+){0,2}`)
type OpenAPIService struct {
projectRoot string
cache *Cache
adventure *AdventureService
}
func NewOpenAPIService(projectRoot string, cache *Cache, adventure *AdventureService) *OpenAPIService {
return &OpenAPIService{projectRoot: projectRoot, cache: cache, adventure: adventure}
}
func (s *OpenAPIService) SearchSkills(skillName string) ([]map[string]any, error) {
skills, err := GetOrLoad(s.cache, "openapi:skills", 30*time.Minute, func() ([]map[string]any, error) {
var data []map[string]any
if err := s.readJSON(filepath.Join(s.projectRoot, "openapi", "data", "skills.json"), &data); err != nil {
return nil, err
}
return data, nil
})
if err != nil {
return nil, err
}
normalized := normalizeWhitespace(skillName)
if normalized == "" {
return skills, nil
}
filtered := make([]map[string]any, 0)
for _, skill := range skills {
name, _ := skill["skillName"].(string)
if strings.Contains(normalizeWhitespace(name), normalized) {
filtered = append(filtered, skill)
}
}
return filtered, nil
}
func (s *OpenAPIService) JobsInfo(jobName string) ([]map[string]string, error) {
jobName = strings.NewReplacer("(", "(", ")", ")").Replace(jobName)
result := make([]map[string]string, 0)
for _, job := range s.adventure.Jobs() {
for _, child := range job.Children {
if jobName == "" || strings.Contains(child.Title, jobName) || strings.Contains(child.Name, jobName) {
result = append(result, map[string]string{
"jobId": job.Name,
"jobName": job.Title,
"jobGrowId": child.Name,
"jobGrowName": child.Title,
})
}
}
}
return result, nil
}
func (s *OpenAPIService) SkillsByJob(jobID string, jobGrowID string) (any, error) {
jobDir, jobGrowDir, err := s.adventure.ResolveDirectories(jobID, jobGrowID)
if err != nil {
return nil, err
}
var data any
path := filepath.Join(s.projectRoot, "openapi", "data", jobDir, jobGrowDir, "cn", "skill_tree.json")
if err := s.readJSON(path, &data); err != nil {
return nil, err
}
return data, nil
}
func (s *OpenAPIService) SkillDetail(jobID string, jobGrowID string, skillID string, level *int) (any, error) {
jobDir, jobGrowDir, err := s.adventure.ResolveDirectories(jobID, jobGrowID)
if err != nil {
return nil, err
}
var skillInfo map[string]any
path := filepath.Join(s.projectRoot, "openapi", "data", jobDir, jobGrowDir, "cn", "skillDetail", skillID+".json")
if err := s.readJSON(path, &skillInfo); err != nil {
return nil, err
}
if levelInfo, ok := skillInfo["levelInfo"].(map[string]any); ok {
if optionDesc, ok := levelInfo["optionDesc"].(string); ok {
levelInfo["optionDesc"] = replacePlaceholders(optionDesc)
}
}
if level == nil {
return skillInfo, nil
}
maxLevel := intFromAny(skillInfo["maxLevel"])
bounded := *level
if bounded < 0 {
bounded = 0
}
if maxLevel > 0 && bounded > maxLevel {
bounded = maxLevel
}
levelInfo, ok := skillInfo["levelInfo"].(map[string]any)
if !ok {
return skillInfo, nil
}
rows, ok := levelInfo["rows"].([]any)
if !ok {
return skillInfo, nil
}
for _, row := range rows {
rowMap, ok := row.(map[string]any)
if !ok || intFromAny(rowMap["level"]) != bounded {
continue
}
for key, value := range rowMap {
levelInfo[key] = value
}
detail, _ := levelInfo["optionDesc"].(string)
if optionValue, ok := levelInfo["optionValue"].(map[string]any); ok {
for key, value := range optionValue {
detail = strings.ReplaceAll(detail, "{"+key+"}", fmt.Sprint(value))
}
}
levelInfo["detail"] = detail
delete(levelInfo, "rows")
skillInfo["attribute"] = levelInfo
delete(skillInfo, "levelInfo")
break
}
return skillInfo, nil
}
func (s *OpenAPIService) SkillDetailByAlter(alter string, skillID string, level int) (any, error) {
jobDir, jobGrowDir, err := s.adventure.DirectoriesFromAlter(alter)
if err != nil {
return nil, err
}
return s.SkillDetail(jobDir, jobGrowDir, skillID, &level)
}
func (s *OpenAPIService) SkillDataSummary(job string, skills []string, weapons []string) ([]map[string]any, error) {
job = strings.NewReplacer("(", "(", ")", ")").Replace(job)
entries, err := os.ReadDir(filepath.Join(s.projectRoot, "openapi", "summary"))
if err != nil {
return nil, err
}
skillFilter := make(map[string]struct{})
for _, skill := range skills {
if skill != "" {
skillFilter[skill] = struct{}{}
}
}
weaponFilter := make(map[string]struct{})
for _, weapon := range weapons {
if weapon != "" {
weaponFilter[weapon] = struct{}{}
}
}
result := make([]map[string]any, 0)
for _, entry := range entries {
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".json") || !strings.Contains(entry.Name(), job) {
continue
}
parts := strings.Split(strings.TrimSuffix(entry.Name(), ".json"), "_")
weapon := "通用"
if len(parts) > 1 && parts[len(parts)-1] != "" {
weapon = parts[len(parts)-1]
}
if len(weaponFilter) > 0 {
if _, ok := weaponFilter[weapon]; !ok {
continue
}
}
var rows []map[string]any
if err := s.readJSON(filepath.Join(s.projectRoot, "openapi", "summary", entry.Name()), &rows); err != nil {
return nil, err
}
jobTitle := strings.TrimPrefix(parts[0], filenameJobPrefix.FindString(parts[0]))
for _, row := range rows {
if len(skillFilter) > 0 {
skillName, _ := row["技能名称"].(string)
if _, ok := skillFilter[skillName]; !ok {
continue
}
}
copied := cloneMap(row)
copied["武器类型"] = weapon
copied["职业"] = jobTitle
result = append(result, copied)
}
}
return result, nil
}
func (s *OpenAPIService) readJSON(path string, dest any) error {
content, err := os.ReadFile(path)
if err != nil {
return err
}
if err := json.Unmarshal(content, dest); err != nil {
return fmt.Errorf("unmarshal %s: %w", path, err)
}
return nil
}
func replacePlaceholders(template string) string {
count := 0
return placeholderPattern.ReplaceAllStringFunc(template, func(_ string) string {
count++
return fmt.Sprintf("{value%d}", count)
})
}
func normalizeWhitespace(value string) string {
return strings.Join(strings.Fields(value), "")
}
func intFromAny(value any) int {
switch typed := value.(type) {
case int:
return typed
case int32:
return int(typed)
case int64:
return int(typed)
case float64:
return int(typed)
case float32:
return int(typed)
default:
return 0
}
}
func cloneMap(source map[string]any) map[string]any {
cloned := make(map[string]any, len(source))
for key, value := range source {
cloned[key] = value
}
return cloned
}