1 #1

Merged
imelon merged 1 commits from optimize into master 2026-06-02 23:30:59 +08:00
38 changed files with 1304 additions and 532 deletions
+3 -5
View File
@@ -1,9 +1,8 @@
import base64 import base64
import hmac
import json import json
import time import time
from core.basic.character import Character from core.basic.character import Character
from fastapi import Header from fastapi import Header
from api.core.exception import ResponseException from api.core.exception import ResponseException
@@ -36,7 +35,7 @@ def dict2obj(dictObj):
return d return d
def createToken(alter: str, equVersion: str, redis = None, expire=86400 / 24 * 2)->str: def createToken(alter: str, equVersion: str, redis=None, expire=86400 / 24 * 2) -> str:
payload = json.dumps({"alter": alter, "equVersion": equVersion, "time": int(time.time() * 1000)}) payload = json.dumps({"alter": alter, "equVersion": equVersion, "time": int(time.time() * 1000)})
token = base64.urlsafe_b64encode(payload.encode('utf-8')).decode('utf-8') token = base64.urlsafe_b64encode(payload.encode('utf-8')).decode('utf-8')
# character = createCharacter(alter) # character = createCharacter(alter)
@@ -51,7 +50,7 @@ def readToken(token: str, redis) -> AlterState:
info = dict2obj(json.loads(redis.get(f'token:{token}'))) info = dict2obj(json.loads(redis.get(f'token:{token}')))
except Exception as ex: except Exception as ex:
print(ex) print(ex)
ResponseException('登录过期或无效Token,请刷新后重试') raise ResponseException('登录过期或无效Token,请刷新后重试') from ex
if info is None: if info is None:
raise ResponseException('登录过期或无效Token,请刷新后重试') raise ResponseException('登录过期或无效Token,请刷新后重试')
return info return info
@@ -67,4 +66,3 @@ def uid(access_uid: str | None = Header(None)):
return int(access_uid) return int(access_uid)
except Exception: except Exception:
return -1 return -1
+4 -4
View File
@@ -6,7 +6,7 @@ from api.routes import openapi
from api.routes import colg from api.routes import colg
api_router = APIRouter() api_router = APIRouter()
api_router.include_router(info.router,prefix="/api",tags=["info"]) api_router.include_router(info.router, prefix="/api", tags=["info"])
api_router.include_router(calc.router,prefix="/api",tags=["calc"]) api_router.include_router(calc.router, prefix="/api", tags=["calc"])
api_router.include_router(openapi.router,prefix="/api/open",tags=["openapi"]) api_router.include_router(openapi.router, prefix="/api/open", tags=["openapi"])
api_router.include_router(colg.router,prefix="/api/open",tags=["colg"]) api_router.include_router(colg.router, prefix="/api/open", tags=["colg"])
+7 -9
View File
@@ -1,17 +1,15 @@
from api.core.Auth import createToken
from api.core.Gzip import register_gzip_request from api.core.Gzip import register_gzip_request
import json from fastapi import APIRouter, Body
from fastapi import APIRouter, Body,Request from api.core.Response import response
from api.core.Response import Return, response from api.dp import AltersDep
from api.core.Redis import get_redis_info
from api.dp import RedisDep,AltersDep
from core.basic.character import createCharacter from core.basic.character import createCharacter
router = APIRouter() router = APIRouter()
register_gzip_request(router) register_gzip_request(router)
@router.post('/calc') @router.post('/calc')
async def calc(request: Request,state:AltersDep,config=Body(None)): async def calc(state: AltersDep, config=Body(None)):
character = createCharacter(state.alter, state.equVersion) character = createCharacter(state.alter, state.equVersion)
return response(data=character.calc(config)) return response(data=character.calc(config))
+1 -1
View File
@@ -65,7 +65,7 @@ async def get_real_time_gold(redis: RedisDep) -> Response[list[RealTimeGold]]:
resp = response.json().get('data', {}) resp = response.json().get('data', {})
res.append({'updataTime': get_date_str(resp['x'][-1]), 'region': region_name, 'goldPrice': resp['y'][-1]}) res.append({'updataTime': get_date_str(resp['x'][-1]), 'region': region_name, 'goldPrice': resp['y'][-1]})
return res return res
except Exception as e: except Exception:
# print(f"File not found: {e}") # print(f"File not found: {e}")
return response(data=[]) return response(data=[])
# 设置半小时过期一次 # 设置半小时过期一次
+6 -36
View File
@@ -1,11 +1,8 @@
import asyncio
import re
from api.core.Auth import createToken
from api.core.Gzip import register_gzip_request from api.core.Gzip import register_gzip_request
import json import json
from fastapi import APIRouter, Request, Depends, Body from fastapi import APIRouter, Body, Request
from typing import Annotated from api.core.Auth import createToken
from api.core.Response import Return, response from api.core.Response import response
from api.core.Redis import get_redis_info, get_redis_info_async from api.core.Redis import get_redis_info, get_redis_info_async
from api.dp import RedisDep, AltersDep from api.dp import RedisDep, AltersDep
from core.basic.equipment import get_equipment from core.basic.equipment import get_equipment
@@ -14,25 +11,13 @@ from core.basic.character import createCharacter
from core.open.helper import get_user_info_by_id, get_user_info_by_role_id, get_user_list_by_id, getRoleJobCode from core.open.helper import get_user_info_by_id, get_user_info_by_role_id, get_user_list_by_id, getRoleJobCode
from core.open.skillTree import get_skill_tree_info from core.open.skillTree import get_skill_tree_info
from api.core.exception import ResponseException from api.core.exception import ResponseException
from api.utils.skill_detail import normalize_skill_detail, skill_detail_for_level
router = APIRouter() router = APIRouter()
register_gzip_request(router) register_gzip_request(router)
def replace_placeholders(template):
# 匹配 <int>、<float>、<float1>、<float2> 等
pattern = re.compile(r'<(int|float\d*)>')
idx = 0
def repl(match):
nonlocal idx
idx += 1
return f'{{value{idx}}}'
return pattern.sub(repl, template)
@router.get('/adventure') @router.get('/adventure')
async def get_adventure_info(request: Request, redis: RedisDep, name: str = None): async def get_adventure_info(request: Request, redis: RedisDep, name: str = None):
adventure_info = get_redis_info(redis, 'dcalc:adventure', get_adv_list) adventure_info = get_redis_info(redis, 'dcalc:adventure', get_adv_list)
@@ -95,25 +80,10 @@ async def get_skill_info(state: AltersDep, skillId: str, level: int, redis: Redi
# For now, we return a placeholder dictionary # For now, we return a placeholder dictionary
with open(f'./openapi/data/{jobId}/{jobGrowId}/cn/skillDetail/{skillId}.json', encoding='utf-8') as f: with open(f'./openapi/data/{jobId}/{jobGrowId}/cn/skillDetail/{skillId}.json', encoding='utf-8') as f:
skill_data = json.load(f) skill_data = json.load(f)
if 'levelInfo' in skill_data and 'optionDesc' in skill_data['levelInfo'] and skill_data['levelInfo']['optionDesc'] is not None: return normalize_skill_detail(skill_data)
skill_data['levelInfo']['optionDesc'] = replace_placeholders(skill_data['levelInfo']['optionDesc'])
return skill_data
skill_info = get_redis_info(redis, key, get_skill_info) skill_info = get_redis_info(redis, key, get_skill_info)
if level is not None: skill_info = skill_detail_for_level(skill_info, level)
level = max(0, min(level, skill_info.get('maxLevel', 0)))
if 'levelInfo' in skill_info and 'rows' in skill_info['levelInfo']:
detail = list(filter(lambda x: x['level'] == level, skill_info['levelInfo']['rows']))
if detail and len(detail) > 0:
skill_info['levelInfo'].update(detail[0])
skill_info['levelInfo']['detail'] = skill_info['levelInfo'].get('optionDesc', '')
if skill_info['levelInfo']['optionValue'] is not None:
for key in skill_info['levelInfo']['optionValue'].keys():
skill_info['levelInfo']['detail'] = skill_info['levelInfo']['detail'].replace(f'{{{key}}}', str(skill_info['levelInfo']['optionValue'][key]))
del skill_info['levelInfo']['rows']
skill_info['attribute'] = {}
skill_info['attribute'].update(skill_info['levelInfo'])
del skill_info['levelInfo']
except FileNotFoundError: except FileNotFoundError:
return response(code=404, message=f'技能信息未找到: {jobId} {jobGrowId} {skillId}', data=None) return response(code=404, message=f'技能信息未找到: {jobId} {jobGrowId} {skillId}', data=None)
return response(data=skill_info) return response(data=skill_info)
+3 -32
View File
@@ -4,6 +4,7 @@ from fastapi import APIRouter, Path, Query
from api.core.Response import response from api.core.Response import response
from api.core.Redis import get_redis_info from api.core.Redis import get_redis_info
from api.dp import RedisDep from api.dp import RedisDep
from api.utils.skill_detail import normalize_skill_detail, skill_detail_for_level
import re import re
from typing import Annotated from typing import Annotated
@@ -23,19 +24,6 @@ router = APIRouter()
register_gzip_request(router) register_gzip_request(router)
def replace_placeholders(template):
# 匹配 <int>、<float>、<float1>、<float2> 等
pattern = re.compile(r'<(int|float\d*)>')
idx = 0
def repl(match):
nonlocal idx
idx += 1
return f'{{value{idx}}}'
return pattern.sub(repl, template)
@router.get('/skills/', operation_id='skillsList', response_model=SearchSkillResponse) @router.get('/skills/', operation_id='skillsList', response_model=SearchSkillResponse)
async def get_skills_list( async def get_skills_list(
redis: RedisDep, redis: RedisDep,
@@ -151,27 +139,10 @@ async def get_cn_skill_info(
encoding='utf-8', encoding='utf-8',
) as f: ) as f:
skill_data = json.load(f) skill_data = json.load(f)
if 'levelInfo' in skill_data and 'optionDesc' in skill_data['levelInfo'] and skill_data['levelInfo']['optionDesc'] is not None: return normalize_skill_detail(skill_data)
skill_data['levelInfo']['optionDesc'] = replace_placeholders(skill_data['levelInfo']['optionDesc'])
return skill_data
skill_info = get_redis_info(redis, key, get_skill_info) skill_info = get_redis_info(redis, key, get_skill_info)
if level is not None: skill_info = skill_detail_for_level(skill_info, level)
level = max(0, min(level, skill_info.get('maxLevel', 0)))
if 'levelInfo' in skill_info and 'rows' in skill_info['levelInfo']:
detail = list(filter(lambda x: x['level'] == level, skill_info['levelInfo']['rows']))
if detail and len(detail) > 0:
skill_info['levelInfo'].update(detail[0])
skill_info['levelInfo']['detail'] = skill_info['levelInfo'].get('optionDesc', '')
for key in skill_info['levelInfo']['optionValue'].keys():
skill_info['levelInfo']['detail'] = skill_info['levelInfo']['detail'].replace(
f'{{{key}}}',
str(skill_info['levelInfo']['optionValue'][key]),
)
del skill_info['levelInfo']['rows']
skill_info['attribute'] = {}
skill_info['attribute'].update(skill_info['levelInfo'])
del skill_info['levelInfo']
except FileNotFoundError: except FileNotFoundError:
return response( return response(
code=404, code=404,
-3
View File
@@ -1,7 +1,4 @@
from enum import Enum
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from typing import Generic, TypeVar
from api.core.Response import Return as Response
class RealTimeGold(BaseModel): class RealTimeGold(BaseModel):
+60
View File
@@ -0,0 +1,60 @@
from copy import deepcopy
import re
from typing import Any
PLACEHOLDER_PATTERN = re.compile(r'<(?:int|float\d*)[^>]*>')
VALUE_PATTERN = re.compile(r'\{value\d+\}')
def replace_placeholders(template: str) -> str:
idx = 0
def repl(_: re.Match[str]) -> str:
nonlocal idx
idx += 1
return f'{{value{idx}}}'
return PLACEHOLDER_PATTERN.sub(repl, template)
def normalize_skill_detail(skill_data: dict[str, Any]) -> dict[str, Any]:
level_info = skill_data.get('levelInfo')
if not isinstance(level_info, dict):
return skill_data
option_desc = level_info.get('optionDesc')
if option_desc is not None:
level_info['optionDesc'] = replace_placeholders(option_desc)
return skill_data
def skill_detail_for_level(skill_data: dict[str, Any], level: int | None) -> dict[str, Any]:
result = deepcopy(skill_data)
if level is None:
return result
level = max(0, min(level, result.get('maxLevel', 0)))
level_info = result.get('levelInfo')
if not isinstance(level_info, dict):
return result
rows = level_info.get('rows')
if not isinstance(rows, list):
return result
detail = next((row for row in rows if row.get('level') == level), None)
if not detail:
return result
attribute = {**level_info, **detail}
attribute['detail'] = attribute.get('optionDesc', '')
for key, value in (attribute.get('optionValue') or {}).items():
attribute['detail'] = attribute['detail'].replace(f'{{{key}}}', str(value))
attribute['detail'] = VALUE_PATTERN.sub('-', attribute['detail'])
attribute.pop('rows', None)
result['attribute'] = attribute
result.pop('levelInfo', None)
return result
+5 -4
View File
@@ -1,5 +1,6 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Any, Literal, Mapping from collections.abc import Mapping
from typing import Any, Literal
class CharacterProperty(ABC): class CharacterProperty(ABC):
@@ -202,17 +203,17 @@ class CharacterProperty(ABC):
pass pass
@abstractmethod @abstractmethod
def AddSkillLv(self, min: int, max: int, lv: int, type=-1, exceptSkills: list[str] = []) -> None: def AddSkillLv(self, min: int, max: int, lv: int, type=-1, exceptSkills: tuple[str, ...] = ()) -> None:
"""增加技能等级""" """增加技能等级"""
pass pass
@abstractmethod @abstractmethod
def SetSkillCD(self, min=1, max=100, cd=0, exclude=[50, 85, 100]) -> None: def SetSkillCD(self, min=1, max=100, cd=0, exclude: tuple[int, ...] = (50, 85, 100)) -> None:
"""设置技能CD""" """设置技能CD"""
pass pass
@abstractmethod @abstractmethod
def SetSkillCDRecover(self, min=1, max=100, cd=0, exclude=[50, 85, 100]) -> None: def SetSkillCDRecover(self, min=1, max=100, cd=0, exclude: tuple[int, ...] = (50, 85, 100)) -> None:
"""设置技能CD恢复""" """设置技能CD恢复"""
pass pass
+19 -13
View File
@@ -1,5 +1,4 @@
from collections.abc import Mapping from collections.abc import Mapping
import sys
from typing import Any, Literal from typing import Any, Literal
from core.abstract.character import CharacterProperty from core.abstract.character import CharacterProperty
@@ -15,8 +14,11 @@ class CharacterBase(CharacterProperty):
BindAwake: int BindAwake: int
"""绑定觉醒等级""" """绑定觉醒等级"""
skills: list['Skill'] = [] skills: list['Skill']
skills_dict: dict[str, 'Skill'] = {} skills_dict: dict[str, 'Skill']
skills_id_dict: dict[str, 'Skill']
skills_uuid_dict: dict[str, 'Skill']
skill_names_cache: dict[tuple[str, bool | str], list[str]]
equVersion: str = '0' equVersion: str = '0'
"""装备版本""" """装备版本"""
charEquipInfo: dict[str, 'CharacterEquipInfo'] charEquipInfo: dict[str, 'CharacterEquipInfo']
@@ -33,9 +35,9 @@ class CharacterBase(CharacterProperty):
"""誓约套装信息""" """誓约套装信息"""
oathSkillInfo: list['OathSuitSkill'] oathSkillInfo: list['OathSuitSkill']
"""誓约技能信息""" """誓约技能信息"""
equ_effect: list['EquEffect'] = [] equ_effect: list['EquEffect']
"""装备效果列表""" """装备效果列表"""
equ_options: dict[str, int] = {} equ_options: dict[str, int]
"""装备选项列表""" """装备选项列表"""
EquEffectRatio = 1.0 EquEffectRatio = 1.0
"""装备效果倍率""" """装备效果倍率"""
@@ -148,6 +150,9 @@ class CharacterBase(CharacterProperty):
self.max_point = 0 self.max_point = 0
self.skills = [] self.skills = []
self.skills_dict = {} self.skills_dict = {}
self.skills_id_dict = {}
self.skills_uuid_dict = {}
self.skill_names_cache = {}
self.load_skills() self.load_skills()
# region 角色属性设置 # region 角色属性设置
@@ -285,7 +290,7 @@ class CharacterBase(CharacterProperty):
value = int(value * (1 + self.jade_effect.ElementIncrease)) value = int(value * (1 + self.jade_effect.ElementIncrease))
self.ElementDB[element] += value self.ElementDB[element] += value
def AddSkillLv(self, min: int, max: int, lv: int, type=-1, exceptSkills: list[str] = []) -> None: def AddSkillLv(self, min: int, max: int, lv: int, type=-1, exceptSkills: tuple[str, ...] = ()) -> None:
""" """
增加技能等级 增加技能等级
type: -1 全部, 0 被动, 1 主动 type: -1 全部, 0 被动, 1 主动
@@ -296,13 +301,13 @@ class CharacterBase(CharacterProperty):
if (skillType == 'all' or skill.type == skillType) and skill.lv > 0 and skill.name not in exceptSkills: if (skillType == 'all' or skill.type == skillType) and skill.lv > 0 and skill.name not in exceptSkills:
skill.lv += lv skill.lv += lv
def SetSkillCD(self, min=1, max=100, cd=0, exclude=[50, 85, 100]) -> None: def SetSkillCD(self, min=1, max=100, cd=0, exclude: tuple[int, ...] = (50, 85, 100)) -> None:
"""设置技能CD""" """设置技能CD"""
for skill in self.skills: for skill in self.skills:
if min <= skill.learnLv <= max and (skill.type == 'active' or skill.damage) and skill.learnLv not in exclude: if min <= skill.learnLv <= max and (skill.type == 'active' or skill.damage) and skill.learnLv not in exclude:
skill.cdReduce *= 1 - cd skill.cdReduce *= 1 - cd
def SetSkillCDRecover(self, min=1, max=100, cd=0, exclude=[50, 85, 100]) -> None: def SetSkillCDRecover(self, min=1, max=100, cd=0, exclude: tuple[int, ...] = (50, 85, 100)) -> None:
"""设置技能CD恢复""" """设置技能CD恢复"""
for skill in self.skills: for skill in self.skills:
if min <= skill.learnLv <= max and (skill.type == 'active' or skill.damage) and skill.learnLv not in exclude: if min <= skill.learnLv <= max and (skill.type == 'active' or skill.damage) and skill.learnLv not in exclude:
@@ -330,17 +335,18 @@ class CharacterBase(CharacterProperty):
def GetSkillByID(self, id: str) -> 'Skill': def GetSkillByID(self, id: str) -> 'Skill':
"""通过技能ID获取技能""" """通过技能ID获取技能"""
skill = list(filter(lambda x: str(x.id) == id, self.skills)) return self.skills_id_dict.get(str(id), None)
return skill[0] if len(skill) > 0 else None
def GetSkillByUUID(self, uuid: str) -> 'Skill': def GetSkillByUUID(self, uuid: str) -> 'Skill':
"""通过技能UUID获取技能""" """通过技能UUID获取技能"""
skill = list(filter(lambda x: str(x.uuid) == uuid, self.skills)) return self.skills_uuid_dict.get(str(uuid), None)
return skill[0] if len(skill) > 0 else None
def GetSkillNames(self, type: Literal['active', 'passive', 'all'] = 'all', damage: Literal[True, False, 'all'] = 'all') -> list[str]: def GetSkillNames(self, type: Literal['active', 'passive', 'all'] = 'all', damage: Literal[True, False, 'all'] = 'all') -> list[str]:
"""获取技能名字""" """获取技能名字"""
return [skill.name for skill in self.skills if (type == 'all' or skill.type == type) and (damage == 'all' or skill.damage == damage)] key = (type, damage)
if key not in self.skill_names_cache:
self.skill_names_cache[key] = [skill.name for skill in self.skills if (type == 'all' or skill.type == type) and (damage == 'all' or skill.damage == damage)]
return self.skill_names_cache[key]
def GetWeaponType(self) -> tuple[str, str]: def GetWeaponType(self) -> tuple[str, str]:
"""获取武器类型""" """获取武器类型"""
+55 -42
View File
@@ -1,4 +1,5 @@
import sys import sys
from functools import cache
from uuid import uuid1 from uuid import uuid1
from core.basic.equipment import OathSuit, Suit, get_equipment from core.basic.equipment import OathSuit, Suit, get_equipment
@@ -7,6 +8,16 @@ from ..roleinfo import CharacterEmblemInfo, CharacterEquipInfo, CharacterOathInf
from .base import CharacterBase from .base import CharacterBase
@cache
def get_skill_classes(module_name: str) -> tuple[type, ...]:
module = sys.modules[module_name]
return tuple(
memberClass
for name in dir(module)
if isinstance((memberClass := getattr(module, name)), type) and memberClass.__name__.startswith('Skill')
)
class CalcEquipMixin(CharacterBase): class CalcEquipMixin(CharacterBase):
"""装备计算、角色初始化相关方法""" """装备计算、角色初始化相关方法"""
@@ -22,11 +33,12 @@ class CalcEquipMixin(CharacterBase):
info['emblems'] = {} info['emblems'] = {}
if info.get('oaths', None) is None: if info.get('oaths', None) is None:
info['oaths'] = {} info['oaths'] = {}
equs = get_equipment(self.equVersion)
for key in info['equips']: for key in info['equips']:
self.charEquipInfo[key] = CharacterEquipInfo(info['equips'][key], self.equVersion, key) self.charEquipInfo[key] = CharacterEquipInfo(info['equips'][key], self.equVersion, key, equs)
for key in info['oaths']: for key in info['oaths']:
# 导入誓约打造信息、誓约信息 # 导入誓约打造信息、誓约信息
self.charOathInfo.append(CharacterOathInfo(info['oaths'][key], self.equVersion, key)) self.charOathInfo.append(CharacterOathInfo(info['oaths'][key], self.equVersion, key, equs))
try: try:
# 异常处理 彩色等级自动取最小值 # 异常处理 彩色等级自动取最小值
minEmblemLv = min([info['emblems'][key]['lv'] for key in ['红色', '蓝色', '绿色', '黄色']], default=1) minEmblemLv = min([info['emblems'][key]['lv'] for key in ['红色', '蓝色', '绿色', '黄色']], default=1)
@@ -36,7 +48,7 @@ class CalcEquipMixin(CharacterBase):
info['emblems'][key]['part'] = key info['emblems'][key]['part'] = key
self.charEmblemInfo.append(CharacterEmblemInfo(info['emblems'][key])) self.charEmblemInfo.append(CharacterEmblemInfo(info['emblems'][key]))
pass pass
except Exception as e: except Exception:
pass pass
self.charOathSkillId = info.get('oathSkill', 1) self.charOathSkillId = info.get('oathSkill', 1)
@@ -59,13 +71,12 @@ class CalcEquipMixin(CharacterBase):
def load_skills(self) -> None: def load_skills(self) -> None:
"""加载技能""" """加载技能"""
module = sys.modules[self.moduleName] for memberClass in get_skill_classes(self.moduleName):
for name in dir(module): skill = memberClass(char=self)
memberClass = getattr(module, name) self.skills_dict[skill.name] = skill
if isinstance(memberClass, type) and memberClass.__name__.startswith('Skill'): self.skills_id_dict[str(skill.id)] = skill
skill = memberClass(char=self) self.skills_uuid_dict[str(skill.uuid)] = skill
self.skills_dict[skill.name] = skill self.skills.append(skill)
self.skills.append(skill)
# endregion # endregion
@@ -77,27 +88,27 @@ class CalcEquipMixin(CharacterBase):
oathSuitInfo = {} oathSuitInfo = {}
correction = 0 correction = 0
equs = get_equipment(self.equVersion) equs = get_equipment(self.equVersion)
for item in filter(lambda x: x.equInfo is not None, self.charEquipInfo.values()): for item in self.charEquipInfo.values():
partEqu = item.equInfo partEqu = item.equInfo
if partEqu is None:
continue
# partFusion = item.fusionInfo # partFusion = item.fusionInfo
if partEqu is not None: for suit in partEqu.suit:
for suit in partEqu.suit: suit_item = suitInfo.setdefault(suit, {'point': 0, 'count': 0})
if suitInfo.get(suit, None) is None: if partEqu.Point == 0:
suitInfo[suit] = {'point': 0, 'count': 0} suit_item['count'] += 1
if partEqu.Point == 0: else:
suitInfo[suit]['count'] += 1 suit_item['point'] += partEqu.Point
else:
suitInfo[suit]['point'] += partEqu.Point
for item in self.charOathInfo: for item in self.charOathInfo:
partOath = item.oathInfo partOath = item.oathInfo
if partOath is not None: if partOath is None:
for suit in partOath.suit: continue
if oathSuitInfo.get(suit, None) is None: for suit in partOath.suit:
oathSuitInfo[suit] = {'point': 0, 'count': 0} oath_suit_item = oathSuitInfo.setdefault(suit, {'point': 0, 'count': 0})
if partOath.Point == 0: if partOath.Point == 0:
oathSuitInfo[suit]['count'] += 1 oath_suit_item['count'] += 1
else: else:
oathSuitInfo[suit]['point'] += partOath.Point oath_suit_item['point'] += partOath.Point
max_point_suit = max((suit for suit in suitInfo if suitInfo[suit]['point'] > 0), key=lambda suit: suitInfo[suit]['point'], default=None) max_point_suit = max((suit for suit in suitInfo if suitInfo[suit]['point'] > 0), key=lambda suit: suitInfo[suit]['point'], default=None)
max_point_oath_suit = max((suit for suit in oathSuitInfo if oathSuitInfo[suit]['point'] > 0), key=lambda suit: oathSuitInfo[suit]['point'], default=None) max_point_oath_suit = max((suit for suit in oathSuitInfo if oathSuitInfo[suit]['point'] > 0), key=lambda suit: oathSuitInfo[suit]['point'], default=None)
zero_point_suits = [suit for suit in suitInfo if suitInfo[suit]['point'] == 0] zero_point_suits = [suit for suit in suitInfo if suitInfo[suit]['point'] == 0]
@@ -140,17 +151,19 @@ class CalcEquipMixin(CharacterBase):
oathSuitList += equs.get_oath_suit_info(suit, oathSuitInfo[suit]['point'], 0) oathSuitList += equs.get_oath_suit_info(suit, oathSuitInfo[suit]['point'], 0)
if len(oathSuitList) == 0: if len(oathSuitList) == 0:
continue continue
oath_suit_by_suit_id = {item.suitId: item for item in oathSuitList}
for suit in suitList: for suit in suitList:
i = suit.id i = suit.id
func = equs.funs.execture(f'suit_{i}') func = equs.funs.execture(f'suit_{i}')
suit = next((x for x in equs.suits if str(x.id) == str(i)), None) suit = equs.suit_id_dict.get(str(i), None)
if suit is None:
continue
# 誓约和套装进行匹配 # 誓约和套装进行匹配
# 如果存在誓约和套装是同一个套装 # 如果存在誓约和套装是同一个套装
# 则直接不执行原套装效果,执行誓约套装增强的效果 # 则直接不执行原套装效果,执行誓约套装增强的效果
oathEffect = next((x for x in oathSuitList if x.suitId == suit.suitId), None) oathEffect = oath_suit_by_suit_id.get(suit.suitId, None)
if suit is not None: filtered_dict = {k: v for k, v in suit.__dict__.items() if k[0].isupper()}
filtered_dict = {k: v for k, v in suit.__dict__.items() if k[0].isupper()} self.SetStatus(**filtered_dict)
self.SetStatus(**filtered_dict)
if func is not None and oathEffect is None: if func is not None and oathEffect is None:
# print(f'套装 {suit.name} 生效') # print(f'套装 {suit.name} 生效')
func(self) func(self)
@@ -160,15 +173,15 @@ class CalcEquipMixin(CharacterBase):
self.max_point = 0 if res['suit'] is None else res.get('suit', {}).get('point', 0) self.max_point = 0 if res['suit'] is None else res.get('suit', {}).get('point', 0)
try: try:
self.oath_max_point = oathSuitInfo.get(max_point_oath_suit, {}).get('point', 0) self.oath_max_point = oathSuitInfo.get(max_point_oath_suit, {}).get('point', 0)
except: except Exception:
self.oath_max_point = 0 self.oath_max_point = 0
self.oathSuitInfo = [] self.oathSuitInfo = []
self.oathSkillInfo = [] self.oathSkillInfo = []
# 当套装点数达到太初时,誓约技能生效 # 当套装点数达到太初时,誓约技能生效
if self.max_point >= 2550: if self.max_point >= 2550:
oathSuit = next((x for x in oathSuitList if x.suitId == max_point_oath_suit), None) oathSuit = oath_suit_by_suit_id.get(max_point_oath_suit, None)
if oathSuit is not None: if oathSuit is not None:
skills = list(filter(lambda x: x.suitId == oathSuit.suitId, oathSuit.skills)) skills = [x for x in oathSuit.skills if x.suitId == oathSuit.suitId]
skill = next((x for x in oathSuit.skills if str(x.skillId) == str(self.charOathSkillId)), None) skill = next((x for x in oathSuit.skills if str(x.skillId) == str(self.charOathSkillId)), None)
# 誓约套装效果生效 # 誓约套装效果生效
filtered_dict = {k: v for k, v in oathSuit.__dict__.items() if k[0].isupper()} filtered_dict = {k: v for k, v in oathSuit.__dict__.items() if k[0].isupper()}
@@ -214,11 +227,11 @@ class CalcEquipMixin(CharacterBase):
"""计算装备基础效果、附魔""" """计算装备基础效果、附魔"""
effects = get_equipment(self.equVersion).funs effects = get_equipment(self.equVersion).funs
# for detail in [(item.equInfo, item.fusionInfo, item.enchant, item.emblem_0, item.emblem_1) for item in filter(lambda x: x.equInfo is not None, self.charEquipInfo.values())]: # for detail in [(item.equInfo, item.fusionInfo, item.enchant, item.emblem_0, item.emblem_1) for item in filter(lambda x: x.equInfo is not None, self.charEquipInfo.values())]:
for detail in [(item.equInfo, item.enchant) for item in filter(lambda x: x.equInfo is not None, self.charEquipInfo.values())]: for item in self.charEquipInfo.values():
equ = detail[0] equ = item.equInfo
if equ is None or equ.itemType == '副武器': if equ is None or equ.itemType == '副武器':
continue continue
enchat = detail[1] enchat = item.enchant
fun = effects.execture(f'equ_{equ.id}') fun = effects.execture(f'equ_{equ.id}')
if fun is None: if fun is None:
continue continue
@@ -316,20 +329,20 @@ class CalcEquipMixin(CharacterBase):
def calc_basic(self): def calc_basic(self):
"""计算基础属性:防具精通、增幅、强化等""" """计算基础属性:防具精通、增幅、强化等"""
char_equip_info = self.charEquipInfo
for part in ['上衣', '头肩', '下装', '腰带', '鞋']: for part in ['上衣', '头肩', '下装', '腰带', '鞋']:
cur = self.charEquipInfo[part] cur = char_equip_info[part]
if cur.equInfo is None: if cur.equInfo is None:
continue continue
equ = cur.equInfo equ = cur.equInfo
reinforce = self.charEquipInfo[part].reinforce reinforce = cur.reinforce
if equ is None: if equ is None:
continue continue
for key in self.防具精通属性: for key in self.防具精通属性:
value = 精通计算(115, equ.rarity, reinforce, equ.itemDetailType, self.防具类型, self.buffer, key) value = 精通计算(115, equ.rarity, reinforce, equ.itemDetailType, self.防具类型, self.buffer, key)
key = get_key_by_value(key) key = get_key_by_value(key)
setattr(self, key, getattr(self, key) + value) setattr(self, key, getattr(self, key) + value)
for part in self.charEquipInfo.keys(): for part, cur in char_equip_info.items():
cur = self.charEquipInfo[part]
if cur.equInfo is None: if cur.equInfo is None:
continue continue
if cur.reinforceType == 1: if cur.reinforceType == 1:
+14 -8
View File
@@ -1,18 +1,21 @@
import importlib import importlib
from database.models import EmblemData, OathData, OathSuitData, OathSuitSkillData, PrimerData, Session, EquData, StoneData, SuitData, EnchantData, JadeData from collections.abc import Sequence
from functools import cache
from sqlalchemy.orm import Session
from database.models import EmblemData, OathData, OathSuitData, OathSuitSkillData, PrimerData, EquData, SuitData, EnchantData, JadeData
from database.connect import get_db_engine as get_engine from database.connect import get_db_engine as get_engine
def parse_to_number_list(info: str, default: list[float] = [0]) -> list[float]: def parse_to_number_list(info: str, default: Sequence[float] = (0,)) -> list[float]:
return default if not info else [float(i) for i in info.split(',')] return list(default) if not info else [float(i) for i in info.split(',')]
def parse_to_string_list(info: str, default: list[str] = ['']) -> list[str]: def parse_to_string_list(info: str, default: Sequence[str] = ('',)) -> list[str]:
return default if not info else [i for i in info.split(',')] return list(default) if not info else [i for i in info.split(',')]
def parse_to_int_list(info: str, default: list[int] = [0]) -> list[int]: def parse_to_int_list(info: str, default: Sequence[int] = (0,)) -> list[int]:
return default if not info else [int(i) for i in info.split(',')] return list(default) if not info else [int(i) for i in info.split(',')]
class Equ: class Equ:
@@ -89,6 +92,7 @@ class Equipments:
self.emblems = [] self.emblems = []
self.suits: list[Suit] = [] self.suits: list[Suit] = []
self.suit_dict: dict[str, Suit] = {} self.suit_dict: dict[str, Suit] = {}
self.suit_id_dict: dict[str, Suit] = {}
self.oath_suits: list[OathSuit] = [] self.oath_suits: list[OathSuit] = []
self.oath_suit_dict: dict[str, OathSuit] = {} self.oath_suit_dict: dict[str, OathSuit] = {}
self.init_equs() self.init_equs()
@@ -99,7 +103,6 @@ class Equipments:
self.init_jades() self.init_jades()
self.init_oaths() self.init_oaths()
self.init_oath_suits() self.init_oath_suits()
self.engine.dispose()
def init_equs(self): def init_equs(self):
"""从数据库中获取所有装备信息""" """从数据库中获取所有装备信息"""
@@ -166,6 +169,7 @@ class Equipments:
suit_obj = Suit(**suit) suit_obj = Suit(**suit)
self.suits.append(suit_obj) self.suits.append(suit_obj)
self.suit_dict.setdefault(str(suit['suitId']), []).append(suit_obj) self.suit_dict.setdefault(str(suit['suitId']), []).append(suit_obj)
self.suit_id_dict[str(suit_obj.id)] = suit_obj
pass pass
pass pass
@@ -279,6 +283,7 @@ class Equipments:
funs = importlib.import_module('core.equipment.version_0') funs = importlib.import_module('core.equipment.version_0')
return funs return funs
@cache
def get_suit_info(self, suitId: str | int, point: int = 0, count: int = 0) -> list[Suit]: def get_suit_info(self, suitId: str | int, point: int = 0, count: int = 0) -> list[Suit]:
"""根据套装点数返回对应适用的套装属性\n """根据套装点数返回对应适用的套装属性\n
新套装只需要传入suitID和point即可,count默认为0\n 新套装只需要传入suitID和point即可,count默认为0\n
@@ -295,6 +300,7 @@ class Equipments:
result[suit.count] = suit result[suit.count] = suit
return list(result.values()) return list(result.values())
@cache
def get_oath_suit_info(self, suitId: str | int, point: int = 0, count: int = 0) -> list['OathSuit']: def get_oath_suit_info(self, suitId: str | int, point: int = 0, count: int = 0) -> list['OathSuit']:
"""根据套装点数返回对应适用的誓约套装属性\n """根据套装点数返回对应适用的誓约套装属性\n
新套装只需要传入suitID和point即可,count默认为0\n 新套装只需要传入suitID和point即可,count默认为0\n
+1 -1
View File
@@ -39,7 +39,7 @@ class CharacterInfo(enum.Enum):
AtkI = '独立攻击', '/ui/UI_ProfileIcon_008.png', lambda x: round(x, 0) AtkI = '独立攻击', '/ui/UI_ProfileIcon_008.png', lambda x: round(x, 0)
"""独立攻击力""" """独立攻击力"""
ElementDB = '攻击属性', '/ui/UI_ProfileIcon_009.png', lambda x: f'火({int(x['火'])})/冰({int(x['冰'])})/光({int(x['光'])})/暗({int(x['暗'])})' ElementDB = '攻击属性', '/ui/UI_ProfileIcon_009.png', lambda x: f"火({int(x['火'])})/冰({int(x['冰'])})/光({int(x['光'])})/暗({int(x['暗'])})"
"""攻击属性""" """攻击属性"""
SkillAttack = '技能伤害', '/ui/UI_ProfileIcon_047.png', lambda x: f'{format(x * 100 - 100, ".1f")}%' SkillAttack = '技能伤害', '/ui/UI_ProfileIcon_047.png', lambda x: f'{format(x * 100 - 100, ".1f")}%'
+16 -6
View File
@@ -32,12 +32,15 @@ class CharacterEquipInfo:
precision: float precision: float
"""秘宝精度""" """秘宝精度"""
def __init__(self, info={}, equVerison=0, part='') -> None: def __init__(self, info=None, equVerison=0, part='', equipments=None) -> None:
if info is None:
info = {}
# info['fusion'] = info.get('fusion', None) # info['fusion'] = info.get('fusion', None)
info['precision'] = info.get('precision', 0) info['precision'] = info.get('precision', 0)
# info['weaponFusion'] = info.get('weaponFusion', None) # info['weaponFusion'] = info.get('weaponFusion', None)
self.__dict__.update(info) self.__dict__.update(info)
equ = get_equipment(equVerison).equ_dict.get(self.id, None) equipments = equipments or get_equipment(equVerison)
equ = equipments.equ_dict.get(self.id, None)
# fusion = get_equipment(equVerison).stone_dict.get(self.fusion, None) # fusion = get_equipment(equVerison).stone_dict.get(self.fusion, None)
if equ is not None and (equ.itemType == part or equ.itemDetailType == part or (equ.itemType == '武器' and part == '副武器')): if equ is not None and (equ.itemType == part or equ.itemDetailType == part or (equ.itemType == '武器' and part == '副武器')):
self.equInfo = equ.adapt(self.adaptation) self.equInfo = equ.adapt(self.adaptation)
@@ -60,10 +63,13 @@ class CharacterOathInfo:
oathInfo: Union['Oath', None] oathInfo: Union['Oath', None]
"""誓约信息""" """誓约信息"""
def __init__(self, info={}, equVerison=0, position='') -> None: def __init__(self, info=None, equVerison=0, position='', equipments=None) -> None:
if info is None:
info = {}
self.__dict__.update(info) self.__dict__.update(info)
self.id = str(self.id) self.id = str(self.id)
oath = get_equipment(equVerison).oath_dict.get(self.id, None) equipments = equipments or get_equipment(equVerison)
oath = equipments.oath_dict.get(self.id, None)
if oath is not None and position in oath.position: if oath is not None and position in oath.position:
self.oathInfo = oath.adapt(self.adaptation) self.oathInfo = oath.adapt(self.adaptation)
self.adaptation = min(self.adaptation, self.oathInfo.max_adaptation) self.adaptation = min(self.adaptation, self.oathInfo.max_adaptation)
@@ -79,7 +85,9 @@ class CharacterEmblemInfo:
lv: int lv: int
"""星徽等级""" """星徽等级"""
def __init__(self, info={}) -> None: def __init__(self, info=None) -> None:
if info is None:
info = {}
self.__dict__.update(info) self.__dict__.update(info)
@@ -89,7 +97,9 @@ class CharacterWeaponInfo(CharacterEquipInfo):
upgradeDetail: dict upgradeDetail: dict
"""贴膜详情""" """贴膜详情"""
def __init__(self, info={}) -> None: def __init__(self, info=None) -> None:
if info is None:
info = {}
self.__dict__.update(info) self.__dict__.update(info)
+82 -43
View File
@@ -1,50 +1,77 @@
from copy import deepcopy from copy import copy
from collections.abc import Callable
from functools import cache from functools import cache
import json import json
import re import re
from typing import Literal, TYPE_CHECKING from typing import Any, Literal, TYPE_CHECKING
from .formula import 武器冷却惩罚 from .formula import 武器冷却惩罚
from database.connect import get_redis from database.connect import BACKEND_DIR, get_redis_client
from api.core.Redis import get_redis_info from api.core.Redis import get_redis_info
if TYPE_CHECKING: if TYPE_CHECKING:
from core.basic.character import Character from core.basic.character import Character
characterLv = 115 + 5 characterLv = 115 + 5
OPENAPI_DIR = BACKEND_DIR / 'openapi'
def _identity(value: Any) -> Any:
return value
@cache @cache
def get_data(prefix: str, key: int | str, func = lambda x: x): def _get_skill_data_suffixes(skill_class: type) -> tuple[str, ...]:
redis = next(get_redis()) return tuple(key.replace('data', '') for key in dir(skill_class) if key.startswith('data') and not key.startswith('plus'))
@cache
def _get_skill_damage_suffixes(skill_class: type) -> tuple[str, ...]:
return tuple(key.replace('data', '') for key in dir(skill_class) if key.startswith('data') and not key.startswith('dataplus'))
@cache
def _get_skill_dataplus_suffixes(skill_class: type) -> tuple[str, ...]:
return tuple(key.replace('dataplus', '') for key in dir(skill_class) if key.startswith('dataplus'))
@cache
def get_data(prefix: str, key: int | str, func: Callable[[Any], Any] = _identity):
prefix_parts = prefix.split('/')
job = prefix_parts[0]
jobGrow = prefix_parts[1]
skill = prefix_parts[-1]
def get_custom_cn(): def get_custom_cn():
with open('./openapi/custom_cn.json', encoding='utf-8') as f: with (OPENAPI_DIR / 'custom_cn.json').open(encoding='utf-8') as f:
custom_cn = json.load(f) custom_cn = json.load(f)
return custom_cn return custom_cn
def get_skill_info(): def get_skill_info():
# This function should retrieve the skill info based on job and jobGrow with (OPENAPI_DIR / 'data' / f'{prefix}.json').open(encoding='utf-8') as f:
# For now, we return a placeholder dictionary
with open(f'./openapi/data/{prefix}.json', encoding='utf-8') as f:
skill_data = json.load(f) skill_data = json.load(f)
custom_cn = get_redis_info(redis, 'dcalc:custom_cn', get_custom_cn, without_gzip=True) custom_cn = get_redis_info(redis, 'dcalc:custom_cn', get_custom_cn, without_gzip=True)
job = prefix.split('/')[0]
jobGrow = prefix.split('/')[1]
skill = prefix.split('/')[-1]
try: try:
if isinstance(key, int): if isinstance(key, int):
data = [0] + list(map(lambda x: x["optionValue"].get("value" + str(key+1), 0), skill_data["levelInfo"]["rows"])) data = [0] + [row["optionValue"].get("value" + str(key+1), 0) for row in skill_data["levelInfo"]["rows"]]
data = [0 if x is None or x == "-" else x for x in data] data = [0 if x is None or x == "-" else x for x in data]
elif key == "vps": elif key == "vps":
data = [ {"name":x["name"],"desc":x["desc"]} for x in skill_data.get("evolution",[])] data = [ {"name":x["name"],"desc":x["desc"]} for x in skill_data.get("evolution",[])]
elif key == "custom": elif key == "custom":
data = next((item.get('custom', 1.0) for item in custom_cn data = next((item.get('custom', 1.0) for item in custom_cn or []
if item['job'] == job and item['jobGrow'] == jobGrow and item['skillId'] == skill), 1.0) if item['job'] == job and item['jobGrow'] == jobGrow and item['skillId'] == skill), 1.0)
except Exception as e: except Exception:
# print(f"get skill {prefix}/{key} data error",e) # print(f"get skill {prefix}/{key} data error")
data = None data = None
if isinstance(key, int) and not data:
return [0]
if key == 'vps' and data is None:
return []
if key == 'custom' and data is None:
return 1.0
return data return data
data = get_redis_info(redis, f'dcalc:skill:{prefix}:{key}', get_skill_info, without_gzip=True) with get_redis_client() as redis:
data = get_redis_info(redis, f'dcalc:skill:{prefix}:{key}', get_skill_info, without_gzip=True)
return func(data) return func(data)
@@ -174,65 +201,76 @@ class Skill:
self._lv = value self._lv = value
def effect(self, old: int, new: int): def effect(self, old: int, new: int):
weapon_type = self.char.GetWeaponType()[0]
weapon_options = self.char.武器选项
for assoc in self.associate: for assoc in self.associate:
type = assoc.get('type', '*skillDamage') type = assoc.get('type', '*skillDamage')
skills = assoc.get('skills', '*') skills = assoc.get('skills', '*')
data = assoc.get('data', [0] * self.maxLv) data = assoc.get('data', [0] * self.maxLv)
ratio = assoc.get('ratio', 100) ratio = assoc.get('ratio', 100)
weapon = assoc.get('weapon', self.char.武器选项) weapon = assoc.get('weapon', weapon_options)
exceptSkills = assoc.get('exceptSkills', []) exceptSkills = assoc.get('exceptSkills', [])
if type[0] not in ['*', '+', '=', '$']: if type[0] not in ['*', '+', '=', '$']:
if self.precondition() and (self.char.GetWeaponType()[0] in weapon or len(weapon) == len(self.char.武器选项)): if self.precondition() and (weapon_type in weapon or len(weapon) == len(weapon_options)):
getattr(self, type)(old, new, data, skills, exceptSkills, ratio, weapon) getattr(self, type)(old, new, data, skills, exceptSkills, ratio, weapon)
continue continue
if self.precondition(): if self.precondition():
self.apply_association(type, old, new, data, skills, exceptSkills, ratio, weapon) self.apply_association(type, old, new, data, skills, exceptSkills, ratio, weapon, weapon_type, weapon_options)
def precondition(self): def precondition(self):
"""effect的前置条件""" """effect的前置条件"""
return True return True
def apply_association(self, type, old, new, data, skills, exceptSkills, ratio=100, weapon=[]): def apply_association(self, type, old, new, data, skills, exceptSkills, ratio=100, weapon=(), weapon_type=None, weapon_options=()):
try: try:
if (self.char.GetWeaponType()[0] not in weapon) and not (self.char.GetWeaponType()[0] is None and len(weapon) == len(self.char.武器选项)): if (weapon_type not in weapon) and not (weapon_type is None and len(weapon) == len(weapon_options)):
return return
if type.startswith('$*'): if type.startswith('$*'):
if 'cdReduce' in type: if 'cdReduce' in type:
value = (1 - data[new] / ratio) / (1 - data[old] / ratio) value = (1 - self.get_data_value(data, new) / ratio) / (1 - self.get_data_value(data, old) / ratio)
else: else:
value = (1 + data[new] / ratio) / (1 + data[old] / ratio) value = (1 + self.get_data_value(data, new) / ratio) / (1 + self.get_data_value(data, old) / ratio)
eval(f'self.char.SetStatus({type[2:]}={value})') self.char.SetStatus(**{type[2:]: value})
elif type.startswith('$+'): elif type.startswith('$+'):
value = data[new] / ratio - data[old] / ratio value = self.get_data_value(data, new) / ratio - self.get_data_value(data, old) / ratio
eval(f'self.char.SetStatus({type[2:]}={value})') self.char.SetStatus(**{type[2:]: value})
else: else:
if skills == '*': if skills == '*':
skills = self.char.GetSkillNames('all', True) skills = self.char.GetSkillNames('all', True)
exceptSkills = set(exceptSkills)
for name in skills: for name in skills:
if name in exceptSkills: if name in exceptSkills:
continue continue
skill = self.char.GetSkillByName(name) skill = self.char.GetSkillByName(name)
if skill is not None: if skill is not None:
self.update_skill_attribute(skill, type, old, new, data, ratio, weapon) self.update_skill_attribute(skill, type, old, new, data, ratio, weapon, weapon_type, weapon_options)
except Exception as e: except Exception as e:
# print(f"Error applying association {type} for skill {self.name}: {e}") # print(f"Error applying association {type} for skill {self.name}: {e}")
raise e raise e
def update_skill_attribute(self, skill, type, old, new, data, ratio=100, weapon=[]): def update_skill_attribute(self, skill, type, old, new, data, ratio=100, weapon=(), weapon_type=None, weapon_options=()):
if (self.char.GetWeaponType()[0] not in weapon) and not (self.char.GetWeaponType()[0] is None and len(weapon) == len(self.char.武器选项)): if (weapon_type not in weapon) and not (weapon_type is None and len(weapon) == len(weapon_options)):
return return
old_value = self.get_data_value(data, old)
new_value = self.get_data_value(data, new)
if type.startswith('*'): if type.startswith('*'):
if 'cdReduce' in type or type == 'cd' or 'cdRatio' in type: if 'cdReduce' in type or type == 'cd' or 'cdRatio' in type:
setattr(skill, type[1:], getattr(skill, type[1:]) * (1 - data[new] / ratio) / (1 - data[old] / ratio)) setattr(skill, type[1:], getattr(skill, type[1:]) * (1 - new_value / ratio) / (1 - old_value / ratio))
else: else:
setattr(skill, type[1:], getattr(skill, type[1:]) * (1 + data[new] / ratio) / (1 + data[old] / ratio)) setattr(skill, type[1:], getattr(skill, type[1:]) * (1 + new_value / ratio) / (1 + old_value / ratio))
elif type.startswith('+'): elif type.startswith('+'):
if type[1:] == 'lv' and self.char.setInfo.get('skills', {}).get(str(skill.id),{}).get('lv',0) == 0: if type[1:] == 'lv' and self.char.setInfo.get('skills', {}).get(str(skill.id),{}).get('lv',0) == 0:
return return
else: else:
setattr(skill, type[1:], getattr(skill, type[1:]) + data[new] / ratio - data[old] / ratio) setattr(skill, type[1:], getattr(skill, type[1:]) + new_value / ratio - old_value / ratio)
elif type.startswith('='): elif type.startswith('='):
setattr(skill, type[1:], data[new]) setattr(skill, type[1:], new_value)
@staticmethod
def get_data_value(data, index: int):
if 0 <= index < len(data):
return data[index]
return 0
def skillInfo(self, mode: str | None = None): def skillInfo(self, mode: str | None = None):
pass pass
@@ -252,8 +290,7 @@ class ActiveSkill(Skill):
def __init__(self, char): def __init__(self, char):
super().__init__(char) super().__init__(char)
keys = [key.replace('data', '') for key in dir(self) if key.startswith('data') and not key.startswith('plus')] for i in _get_skill_data_suffixes(self.__class__):
for i in keys:
power = getattr(self, f'power{i}', None) power = getattr(self, f'power{i}', None)
if power is None: if power is None:
setattr(self, f'power{i}', 1) setattr(self, f'power{i}', 1)
@@ -262,7 +299,12 @@ class ActiveSkill(Skill):
setattr(self, f'hit{i}', 0) setattr(self, f'hit{i}', 0)
def skillInfo(self, mode: str | None = None): def skillInfo(self, mode: str | None = None):
basic = deepcopy(self) basic = copy(self)
for key, value in self.__dict__.items():
if isinstance(value, list):
setattr(basic, key, value.copy())
elif isinstance(value, dict):
setattr(basic, key, value.copy())
basic.setUP() basic.setUP()
if mode is not None: if mode is not None:
basic.currentMode = mode basic.currentMode = mode
@@ -298,8 +340,7 @@ class ActiveSkill(Skill):
def getSkillData(self, lv: int = 0): def getSkillData(self, lv: int = 0):
res = 0 res = 0
keys = [key.replace('data', '') for key in dir(self) if key.startswith('data') and not key.startswith('dataplus')] for i in _get_skill_damage_suffixes(self.__class__):
for i in keys:
data = getattr(self, f'data{i}', []) data = getattr(self, f'data{i}', [])
plus = getattr(self, f'plus{i}', 0) plus = getattr(self, f'plus{i}', 0)
hit = getattr(self, f'hit{i}', 0) hit = getattr(self, f'hit{i}', 0)
@@ -309,8 +350,7 @@ class ActiveSkill(Skill):
hit = getattr(self, f'hit{i}', 0) hit = getattr(self, f'hit{i}', 0)
power = getattr(self, f'power{i}', 1) power = getattr(self, f'power{i}', 1)
res += hit * power * (data[lv] + plus) res += hit * power * (data[lv] + plus)
keys = [key.replace('dataplus', '') for key in dir(self) if key.startswith('dataplus')] for i in _get_skill_dataplus_suffixes(self.__class__):
for i in keys:
data = getattr(self, f'dataplus{i}', 0) data = getattr(self, f'dataplus{i}', 0)
hit = getattr(self, f'hitplus{i}', 1) hit = getattr(self, f'hitplus{i}', 1)
power = getattr(self, f'powerplus{i}', 1) power = getattr(self, f'powerplus{i}', 1)
@@ -397,8 +437,7 @@ class BuffSkill(Skill):
def __init__(self, char): def __init__(self, char):
super().__init__(char) super().__init__(char)
keys = [key.replace('data', '') for key in dir(self) if key.startswith('data') and not key.startswith('plus')] for i in _get_skill_data_suffixes(self.__class__):
for i in keys:
power = getattr(self, f'power{i}', None) power = getattr(self, f'power{i}', None)
if power is None: if power is None:
setattr(self, f'power{i}', 1) setattr(self, f'power{i}', 1)
+3 -5
View File
@@ -1,5 +1,5 @@
# b9cb48777665de22c006fabaf9a560b3 # b9cb48777665de22c006fabaf9a560b3
from core.basic.skill import PassiveSkill, ActiveSkill, get_data from core.basic.skill import PassiveSkill, ActiveSkill, get_data, _get_skill_damage_suffixes, _get_skill_dataplus_suffixes
from core.basic.character import Character from core.basic.character import Character
prefix = 'archer/hunter/cn/skillDetail' prefix = 'archer/hunter/cn/skillDetail'
@@ -12,8 +12,7 @@ class HunterActiveSkill(ActiveSkill):
# 标记消耗,大于0为消耗,小于0为生成 # 标记消耗,大于0为消耗,小于0为生成
def getSkillData(self, lv: int = 0): def getSkillData(self, lv: int = 0):
res = 0 res = 0
keys = [key.replace('data', '') for key in dir(self) if key.startswith('data') and not key.startswith('dataplus')] for i in _get_skill_damage_suffixes(self.__class__):
for i in keys:
data = getattr(self, f'data{i}', []) data = getattr(self, f'data{i}', [])
plus = getattr(self, f'plus{i}', 0) plus = getattr(self, f'plus{i}', 0)
hit = getattr(self, f'hit{i}', 0) hit = getattr(self, f'hit{i}', 0)
@@ -24,8 +23,7 @@ class HunterActiveSkill(ActiveSkill):
power = getattr(self, f'power{i}', 1) power = getattr(self, f'power{i}', 1)
cost = getattr(self, f'cost{i}', 0) cost = getattr(self, f'cost{i}', 0)
res += hit * power * (data[lv] + plus) * ((1 + self.normalRation) if cost <= 0 else (1 + self.markRation * cost)) res += hit * power * (data[lv] + plus) * ((1 + self.normalRation) if cost <= 0 else (1 + self.markRation * cost))
keys = [key.replace('dataplus', '') for key in dir(self) if key.startswith('dataplus')] for i in _get_skill_dataplus_suffixes(self.__class__):
for i in keys:
data = getattr(self, f'dataplus{i}', 0) data = getattr(self, f'dataplus{i}', 0)
hit = getattr(self, f'hitplus{i}', 1) hit = getattr(self, f'hitplus{i}', 1)
power = getattr(self, f'powerplus{i}', 1) power = getattr(self, f'powerplus{i}', 1)
+3 -5
View File
@@ -1,5 +1,5 @@
# ddc49e9ad1ff72a00b53c6cff5b1e920 # ddc49e9ad1ff72a00b53c6cff5b1e920
from core.basic.skill import PassiveSkill, ActiveSkill, get_data from core.basic.skill import PassiveSkill, ActiveSkill, get_data, _get_skill_damage_suffixes, _get_skill_dataplus_suffixes
from core.basic.character import Character from core.basic.character import Character
prefix = 'thief/rogue/cn/skillDetail' prefix = 'thief/rogue/cn/skillDetail'
@@ -14,8 +14,7 @@ class ActiveSkill(ActiveSkill):
def getSkillData(self, lv: int = 0): def getSkillData(self, lv: int = 0):
res = 0 res = 0
keys = [key.replace('data', '') for key in dir(self) if key.startswith('data') and not key.startswith('dataplus')] for i in _get_skill_damage_suffixes(self.__class__):
for i in keys:
data = getattr(self, f'data{i}', []) data = getattr(self, f'data{i}', [])
plus = getattr(self, f'plus{i}', 0) plus = getattr(self, f'plus{i}', 0)
if len(data) == 0: if len(data) == 0:
@@ -25,8 +24,7 @@ class ActiveSkill(ActiveSkill):
power = getattr(self, f'power{i}', 1) power = getattr(self, f'power{i}', 1)
group = getattr(self, f'group{i}', 'normal') group = getattr(self, f'group{i}', 'normal')
res += hit * power * (data[lv] + plus) * (self.endpower if group == 'end' else 1) res += hit * power * (data[lv] + plus) * (self.endpower if group == 'end' else 1)
keys = [key.replace('dataplus', '') for key in dir(self) if key.startswith('dataplus')] for i in _get_skill_dataplus_suffixes(self.__class__):
for i in keys:
data = getattr(self, f'dataplus{i}', 0) data = getattr(self, f'dataplus{i}', 0)
hit = getattr(self, f'hitplus{i}', 1) hit = getattr(self, f'hitplus{i}', 1)
power = getattr(self, f'powerplus{i}', 1) power = getattr(self, f'powerplus{i}', 1)
-1
View File
@@ -1,4 +1,3 @@
from core.basic.equipment import EquEffect
from core.abstract.character import CharacterProperty from core.abstract.character import CharacterProperty
from .register import register from .register import register
+3 -4
View File
@@ -1,11 +1,10 @@
registry = {} registry = {}
def register(func): def register(func):
registry[func.__name__] = func registry[func.__name__] = func
return func return func
def execture(name): def execture(name):
if name in registry: return registry.get(name, None)
return registry[name]
else:
return None
-1
View File
@@ -120,7 +120,6 @@ def trans_to_dcalc_set(detail, alter: AltersDep):
basicAvatars = basic.funs.get_dress_list([]) basicAvatars = basic.funs.get_dress_list([])
equips = {} equips = {}
avatar = {} avatar = {}
avatarDisplay = {}
emblems = {} emblems = {}
oaths = {} oaths = {}
for equ in detail.get('equs', []): for equ in detail.get('equs', []):
+6 -8
View File
@@ -1,4 +1,3 @@
import os
import platform import platform
import re import re
@@ -14,9 +13,8 @@ def search_host(hostvalue):
host_path = host_mac host_path = host_mac
else: else:
host_path = host_window host_path = host_window
hostfile = open(host_path, 'r', errors='ignore') with open(host_path, errors='ignore') as hostfile:
each_line = hostfile.readlines() each_line = hostfile.readlines()
hostfile.close()
findresult = re.findall(hostvalue, ''.join(each_line)) findresult = re.findall(hostvalue, ''.join(each_line))
return findresult return findresult
@@ -27,10 +25,10 @@ def write_host(hostvalue):
else: else:
output = open(host_window, 'a') output = open(host_window, 'a')
for insid in hostvalue: with output:
output.write(insid) for insid in hostvalue:
output.write('\n') output.write(insid)
output.close() output.write('\n')
def host_init(): def host_init():
+14 -8
View File
@@ -1,22 +1,28 @@
import os from functools import cache
import sys from pathlib import Path
import redis import redis
from sqlalchemy import create_engine from sqlalchemy import create_engine
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from config.main import config from config.main import config
RedisPool = redis.ConnectionPool.from_url(config.REDIS_URL) RedisPool = redis.ConnectionPool.from_url(config.REDIS_URL)
BACKEND_DIR = Path(__file__).resolve().parents[1]
def get_redis_client():
return redis.Redis(connection_pool=RedisPool)
def get_redis(): def get_redis():
with redis.Redis(connection_pool=RedisPool) as redis_conn: with get_redis_client() as redis_conn:
yield redis_conn yield redis_conn
@cache
def get_db_engine(version='0'): def get_db_engine(version='0'):
try: db_path = BACKEND_DIR / 'dataFiles' / f'dcalc_{version}.db'
os.chdir(os.path.dirname(sys.argv[0])) return create_engine(f'sqlite:///{db_path}', connect_args={'check_same_thread': False})
except Exception:
pass
return create_engine(f'sqlite:///{os.path.join("./", "dataFiles", f"dcalc_{version}.db")}')
def get_db(): def get_db():
with Session(get_db_engine()) as session: with Session(get_db_engine()) as session:
+1 -2
View File
@@ -1,8 +1,7 @@
# import os # import os
# import sys # import sys
from sqlalchemy import Column, Integer, Text, Float from sqlalchemy import Column, Integer, Text, Float
from sqlalchemy import create_engine from sqlalchemy.orm import declarative_base
from sqlalchemy.orm import Session, declarative_base
Base = declarative_base() Base = declarative_base()
metadata = Base.metadata metadata = Base.metadata
+5
View File
@@ -12,3 +12,8 @@ dependencies = [
"requests>=2.32.3", "requests>=2.32.3",
"sqlalchemy>=2.0.38", "sqlalchemy>=2.0.38",
] ]
[dependency-groups]
dev = [
"ruff>=0.15.15",
]
+8
View File
@@ -1,7 +1,15 @@
# 限制每行最多20个字符 # 限制每行最多20个字符
line-length = 320 line-length = 320
extend-exclude = [
"api/core/zipfile.py",
"core/basic/character_old.py",
]
lint.select = ["F","E","W","UP"] lint.select = ["F","E","W","UP"]
[lint.per-file-ignores]
"core/character/**/*.py" = ["E501", "F401", "W291", "W292", "W293"]
"core/equipment/version_0/suit.py" = ["E501"]
[format] [format]
# 使用单引号 # 使用单引号
quote-style = "single" quote-style = "single"
+34 -1
View File
@@ -1,5 +1,5 @@
version = 1 version = 1
revision = 2 revision = 3
requires-python = ">=3.10" requires-python = ">=3.10"
resolution-markers = [ resolution-markers = [
"python_full_version >= '3.14'", "python_full_version >= '3.14'",
@@ -544,6 +544,11 @@ dependencies = [
{ name = "sqlalchemy" }, { name = "sqlalchemy" },
] ]
[package.dev-dependencies]
dev = [
{ name = "ruff" },
]
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "aiohttp", specifier = ">=3.13.3" }, { name = "aiohttp", specifier = ">=3.13.3" },
@@ -554,6 +559,9 @@ requires-dist = [
{ name = "sqlalchemy", specifier = ">=2.0.38" }, { name = "sqlalchemy", specifier = ">=2.0.38" },
] ]
[package.metadata.requires-dev]
dev = [{ name = "ruff", specifier = ">=0.15.15" }]
[[package]] [[package]]
name = "detect-installer" name = "detect-installer"
version = "0.1.0" version = "0.1.0"
@@ -2354,6 +2362,31 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" },
] ]
[[package]]
name = "ruff"
version = "0.15.15"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/84/6f/a76f7d96e5c962f5b69cee865e49c15c1116897c01990faa8a57edb62e7f/ruff-0.15.15.tar.gz", hash = "sha256:b8dff018130b46d8e5bf0f926ef6b60cf871d6d5ae45fc9334e09632daa741d6", size = 4706985, upload-time = "2026-05-28T14:16:57.784Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fa/9d/3a45c05b8ab04b4705989de70a79008e27c8003296a0feaee9edc18dd7e9/ruff-0.15.15-py3-none-linux_armv6l.whl", hash = "sha256:cf93e5388f412e1b108b1f8b34a6e036b70fe8aff89393befad96fe48670311b", size = 10710652, upload-time = "2026-05-28T14:16:06.701Z" },
{ url = "https://files.pythonhosted.org/packages/05/66/da974431624bf3b49f6ee1f9543c02d929ff1cba78b0d5a79c38cf21f744/ruff-0.15.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ac5a646d1f6a7dadd5d50842dae2c1f9862ac887ef5d1b1375e02def791fde6e", size = 11096615, upload-time = "2026-05-28T14:16:23.313Z" },
{ url = "https://files.pythonhosted.org/packages/8c/09/7443452e5d290230a712103f2fdceeef7184f3ec99a2bd01c8be78aaceb5/ruff-0.15.15-py3-none-macosx_11_0_arm64.whl", hash = "sha256:77d955a431430c66f72dd94e379ad38a16daea3d25094872ac4edf9e797be530", size = 10436683, upload-time = "2026-05-28T14:16:40.974Z" },
{ url = "https://files.pythonhosted.org/packages/53/01/d330c26a57fa4f3943a14424904027428315b700fe4d14a84bb123a649e5/ruff-0.15.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7614ee79c69788cf6cedd568069ade9cecc22a1ad20494efe8d0c9ebb4b622d4", size = 10769064, upload-time = "2026-05-28T14:16:28.905Z" },
{ url = "https://files.pythonhosted.org/packages/1d/85/cc8770f8bdff541b1da8392d1634141fe4a0e3f4ee596605959b7906c27f/ruff-0.15.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3cdb1679e06a1f6b47bc384714ae96f6e2fb65ca441eb78c43d2ca554176ce1f", size = 10511987, upload-time = "2026-05-28T14:16:43.732Z" },
{ url = "https://files.pythonhosted.org/packages/7c/29/8c190c1472b63013583ba391f3342036e02010544c1270455ed8e519bdf3/ruff-0.15.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2728b93d7b23a603ea2c0ac6eb73d760bd38ec9de35f35fb41e18f7a3fee7622", size = 11275100, upload-time = "2026-05-28T14:16:55.244Z" },
{ url = "https://files.pythonhosted.org/packages/9f/6b/7e145ce2cc8e63d6834eca03d83a0e18d121def5c69f91b4cf4011ed4879/ruff-0.15.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be582fcc0db438902c7792b08d6ddf6c9b9e21addaa10092c2c741cfb09e5a45", size = 12176903, upload-time = "2026-05-28T14:16:14.368Z" },
{ url = "https://files.pythonhosted.org/packages/80/a3/d5974637f68e451f7fadf015cf3101d1cd7d8ba5027cffe0b9e3826ebe6b/ruff-0.15.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7aa77465b8ecaf1a27bea098d696f7fed5e1eccbd10b321b682d6de586ae5627", size = 11404550, upload-time = "2026-05-28T14:16:20.138Z" },
{ url = "https://files.pythonhosted.org/packages/fe/1c/e6e5e568f22be4fb05d6244234aba384c06b451252453b821e1a529263cf/ruff-0.15.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48decfa11d740de4889de623be1463308346312f2409a56e24aa280c86162dc4", size = 11382027, upload-time = "2026-05-28T14:16:46.615Z" },
{ url = "https://files.pythonhosted.org/packages/1d/01/170921b49fcd2e8858825593f91cf7146c3e40a5c3e6df763e4bb0484dde/ruff-0.15.15-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a5015088452ca0081387063649ec67f06d3d1d6b8b936a1f836b5e9657ecd48c", size = 11366041, upload-time = "2026-05-28T14:16:26.247Z" },
{ url = "https://files.pythonhosted.org/packages/87/54/a7bad711d7de93254e15e06a4c375b89a03d18de45d3e5dcc86a4472fb1a/ruff-0.15.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5294aab6356c81600fcdea3a62bb1b924dfd5e91767c12318d3f68f86af57cd", size = 10741795, upload-time = "2026-05-28T14:16:17.11Z" },
{ url = "https://files.pythonhosted.org/packages/c9/31/38c075963668f8b41c6914ee0f6f318727fbe30ab9145cb29e6df464c5fa/ruff-0.15.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:db5bd4d802415cca656dc1616070b725952d6ae95eb5d4831e49fbd94a38f75f", size = 10511117, upload-time = "2026-05-28T14:16:31.767Z" },
{ url = "https://files.pythonhosted.org/packages/9d/96/6ff689e1f7e375d1d97075eca022f74c2bab59554a432fe4d2e6f091986a/ruff-0.15.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:587a6278ed42059191c1a466e490bd7930fb50bd2e255398bc29616c895a61cb", size = 10994867, upload-time = "2026-05-28T14:16:35.149Z" },
{ url = "https://files.pythonhosted.org/packages/c3/c2/5dce0ab9f92a8d534fa62b9bf9caca3eddb8c1a81b616f5e195ada4f0d6e/ruff-0.15.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:df0c1c084f5f4be9812f61518a45c440d3c30d69ce4bf6c5270e66d38338f02a", size = 11482101, upload-time = "2026-05-28T14:16:49.598Z" },
{ url = "https://files.pythonhosted.org/packages/b1/c0/1003b60edd697c649faf61f1a34094b1abb38fb3d1181e3f895781250a08/ruff-0.15.15-py3-none-win32.whl", hash = "sha256:29428ea79694afbe756d45fd59b36f22b6b020dc0443cf7de0173046236964b9", size = 10716774, upload-time = "2026-05-28T14:16:52.337Z" },
{ url = "https://files.pythonhosted.org/packages/02/a8/1269eddd6945a06c23f055ef7848886e37cf9d6a8bebb386a3115f01470c/ruff-0.15.15-py3-none-win_amd64.whl", hash = "sha256:8df0323902e15e24bc4bf246da830573d3cf3352bd0b9a164eab335d111ff4a4", size = 11868463, upload-time = "2026-05-28T14:16:11.333Z" },
{ url = "https://files.pythonhosted.org/packages/4e/b2/920464c907b191e37469d477a1aa8bc048b8f36c4c1610dfa4ab87b39e18/ruff-0.15.15-py3-none-win_arm64.whl", hash = "sha256:3c8ceca6792f38196b8f589bc92eccd03eef286602da92e5dc05cc42ef6441b7", size = 11138498, upload-time = "2026-05-28T14:16:38.425Z" },
]
[[package]] [[package]]
name = "secretstorage" name = "secretstorage"
version = "3.5.0" version = "3.5.0"
+9 -1
View File
@@ -1,5 +1,5 @@
import { defineRequest } from "../common"; import { defineRequest } from "../common";
import type { IAdventureInfo, ICharacterInfo, IHelperInfo, ISkillInfo } from "./type"; import type { IAdventureInfo, IAlterInfo, ICharacterInfo, IHelperInfo, ISkillInfo } from "./type";
export default defineRequest((request) => { export default defineRequest((request) => {
return { return {
@@ -7,6 +7,10 @@ export default defineRequest((request) => {
const res = await request.get<IAdventureInfo[]>("/adventure"); const res = await request.get<IAdventureInfo[]>("/adventure");
return res.data; return res.data;
}, },
async alterByName(name: string) {
const res = await request.get<IAlterInfo>("/adventure", { params: { name } });
return res.data;
},
async createToken(alter: string, equVersion?: string) { async createToken(alter: string, equVersion?: string) {
const res = await request.get<string>(`/token/get/${alter}`, { params: { equVersion } }); const res = await request.get<string>(`/token/get/${alter}`, { params: { equVersion } });
return res.data; return res.data;
@@ -23,6 +27,10 @@ export default defineRequest((request) => {
const res = await request.get(`/dnfhelper/${uid}/`); const res = await request.get(`/dnfhelper/${uid}/`);
return res.data; return res.data;
}, },
async getConfigFromHelperAlter(uid: string, alter: string) {
const res = await request.get(`/dnfhelper/${uid}/${alter}/`);
return res.data;
},
async getSkillFromCode(code: string) { async getSkillFromCode(code: string) {
const res = await request.post(`/skillTree/`, { code }); const res = await request.post(`/skillTree/`, { code });
return res.data; return res.data;
@@ -572,7 +572,7 @@ const leftPanelOpenSections = ["equipped", "stats", "suit", "equipped-list"];
<td class="px-1.5 py-1.5"> <td class="px-1.5 py-1.5">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<img <img
:src="skill.icon" :src="getImageURL(skill.icon)"
:alt="skill.name" :alt="skill.name"
class="size-4 rounded-sm object-cover sm:size-5" class="size-4 rounded-sm object-cover sm:size-5"
/> />
@@ -6,12 +6,21 @@ import {
ChevronRight, ChevronRight,
ChevronsLeft, ChevronsLeft,
ChevronsRight, ChevronsRight,
Settings2,
Sparkles, Sparkles,
} from "lucide-vue-next"; } from "lucide-vue-next";
import { useConfigStore, useInfoStore } from "@/stores"; import { useConfigStore, useInfoStore } from "@/stores";
import type { IEquipment, ISuit } from "@/api/info/type"; import type { IEquipment, ISuit } from "@/api/info/type";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
import Tabs from "@/components/ui/tabs/Tabs.vue"; import Tabs from "@/components/ui/tabs/Tabs.vue";
import TabsList from "@/components/ui/tabs/TabsList.vue"; import TabsList from "@/components/ui/tabs/TabsList.vue";
import TabsTrigger from "@/components/ui/tabs/TabsTrigger.vue"; import TabsTrigger from "@/components/ui/tabs/TabsTrigger.vue";
@@ -54,7 +63,8 @@ const suitId = ref("");
const oathSuitId = ref(""); const oathSuitId = ref("");
const suitRarity = ref<string>(""); const suitRarity = ref<string>("");
const oathSuitRarity = ref<string>(""); const oathSuitRarity = ref<string>("");
const sidePanelTab = ref<SidePanelTab>("detail"); const sidePanelTab = ref<SidePanelTab>("equipped");
const triggerOptionsOpen = ref(false);
const equippedPart = ref(""); const equippedPart = ref("");
const equippedOathPart = ref("0"); const equippedOathPart = ref("0");
const previewEquipKey = ref(""); const previewEquipKey = ref("");
@@ -75,7 +85,8 @@ const panelLayoutVars = computed(() => ({
const iconWrapStyle = computed(() => ({ const iconWrapStyle = computed(() => ({
...panelLayoutVars.value, ...panelLayoutVars.value,
width: "var(--equip-grid-width)", maxWidth: "var(--equip-grid-width)",
width: "100%",
})); }));
const isOathEquipment = (equip?: IEquipment) => const isOathEquipment = (equip?: IEquipment) =>
@@ -805,25 +816,29 @@ const toggleOathRarity = (value: unknown) => {
const next = normalizeRarityValue(value); const next = normalizeRarityValue(value);
oathSuitRarity.value = !next || oathSuitRarity.value === next ? "" : next; oathSuitRarity.value = !next || oathSuitRarity.value === next ? "" : next;
}; };
const setTriggerOption = (id: number, value: string) => {
configStore.config.options[id.toString()] = Number.parseInt(value, 10);
};
</script> </script>
<template> <template>
<div <div
class="grid min-h-full min-w-0 gap-4 pb-6 md:h-full md:pb-0 md:grid-cols-[minmax(0,1fr)_320px]" class="grid min-h-full min-w-0 gap-4 pb-6 lg:h-full lg:pb-0 lg:grid-cols-[minmax(0,1fr)_300px] xl:grid-cols-[minmax(0,1fr)_320px]"
> >
<div <div
class="min-h-0 min-w-0 grid items-stretch gap-4 md:h-full [grid-template-columns:repeat(auto-fit,minmax(min(100%,18rem),1fr))]" class="min-h-0 min-w-0 grid items-stretch gap-4 lg:h-full [grid-template-columns:repeat(auto-fit,minmax(min(100%,18rem),1fr))]"
:style="panelLayoutVars" :style="panelLayoutVars"
> >
<div class="min-h-0 min-w-0 md:h-full"> <div class="min-h-0 min-w-0 lg:h-full">
<Card <Card
class="w-full md:flex md:h-full md:flex-col md:overflow-hidden" class="w-full lg:flex lg:h-full lg:flex-col lg:overflow-hidden"
:style="panelLayoutVars" :style="panelLayoutVars"
> >
<!-- <CardHeader class="pb-3"> <!-- <CardHeader class="pb-3">
<CardTitle>武器/称号/宠物/秘宝</CardTitle> <CardTitle>武器/称号/宠物/秘宝</CardTitle>
</CardHeader> --> </CardHeader> -->
<CardContent class="space-y-4 md:min-h-0 md:flex-1 md:overflow-y-auto"> <CardContent class="space-y-4 overflow-x-hidden lg:min-h-0 lg:flex-1 lg:overflow-y-auto">
<Tabs v-model="weaponTab" class="gap-0" size="sm"> <Tabs v-model="weaponTab" class="gap-0" size="sm">
<TabsList> <TabsList>
<TabsTrigger value="weapon">武器</TabsTrigger> <TabsTrigger value="weapon">武器</TabsTrigger>
@@ -832,7 +847,7 @@ const toggleOathRarity = (value: unknown) => {
</TabsList> </TabsList>
</Tabs> </Tabs>
<div class="overflow-x-auto"> <div class="overflow-x-hidden">
<div class="mx-auto flex flex-wrap content-start gap-1" :style="iconWrapStyle"> <div class="mx-auto flex flex-wrap content-start gap-1" :style="iconWrapStyle">
<template v-if="weaponTab === 'weapon'"> <template v-if="weaponTab === 'weapon'">
<EquipmentCell <EquipmentCell
@@ -878,6 +893,50 @@ const toggleOathRarity = (value: unknown) => {
</TabsList> </TabsList>
</Tabs> </Tabs>
<Sheet v-model:open="triggerOptionsOpen">
<SheetTrigger as-child>
<Button
v-if="infoStore.options.length"
size="icon-sm"
variant="outline"
title="装备触发条件"
>
<Settings2 class="size-4" />
</Button>
</SheetTrigger>
<SheetContent class="w-full overflow-y-auto sm:max-w-md">
<SheetHeader>
<SheetTitle>装备触发条件</SheetTitle>
<SheetDescription>配置会立即写回当前角色计算条件。</SheetDescription>
</SheetHeader>
<div class="grid gap-3 px-4">
<label
v-for="option in infoStore.options"
:key="option.id"
class="grid gap-1 text-sm"
>
<span class="text-muted-foreground">{{ option.name }}</span>
<select
class="h-9 rounded-md border border-input bg-background px-2"
:value="String(configStore.config.options[option.id.toString()] ?? 0)"
@change="
(event) =>
setTriggerOption(option.id, (event.target as HTMLSelectElement).value)
"
>
<option
v-for="(item, index) in option.options"
:key="`${option.id}-${index}`"
:value="String(index)"
>
{{ item }}
</option>
</select>
</label>
</div>
</SheetContent>
</Sheet>
<ColorSwatchPickerRoot <ColorSwatchPickerRoot
v-if="equipTab === 'suit'" v-if="equipTab === 'suit'"
class="ml-auto flex items-center gap-2" class="ml-auto flex items-center gap-2"
@@ -908,7 +967,7 @@ const toggleOathRarity = (value: unknown) => {
<template v-if="equipTab === 'suit'"> <template v-if="equipTab === 'suit'">
<div <div
class="mx-auto flex w-max max-w-full flex-nowrap overflow-x-auto pb-1" class="mx-auto flex flex-wrap content-start gap-1 overflow-x-hidden pb-1"
:style="iconWrapStyle" :style="iconWrapStyle"
> >
<button <button
@@ -941,7 +1000,7 @@ const toggleOathRarity = (value: unknown) => {
</button> </button>
</div> </div>
<div class="overflow-x-auto"> <div class="overflow-x-hidden">
<div class="mx-auto flex flex-wrap content-start gap-1" :style="iconWrapStyle"> <div class="mx-auto flex flex-wrap content-start gap-1" :style="iconWrapStyle">
<EquipmentCell <EquipmentCell
v-for="(equip, index) in suitGrid" v-for="(equip, index) in suitGrid"
@@ -966,7 +1025,7 @@ const toggleOathRarity = (value: unknown) => {
> >
{{ section.title }} {{ section.title }}
</div> --> </div> -->
<div class="overflow-x-auto"> <div class="overflow-x-hidden">
<div class="mx-auto flex flex-wrap content-start gap-1" :style="iconWrapStyle"> <div class="mx-auto flex flex-wrap content-start gap-1" :style="iconWrapStyle">
<EquipmentCell <EquipmentCell
v-for="(equip, index) in section.items" v-for="(equip, index) in section.items"
@@ -991,21 +1050,21 @@ const toggleOathRarity = (value: unknown) => {
</Card> </Card>
</div> </div>
<div class="min-h-0 min-w-0 md:h-full"> <div class="min-h-0 min-w-0 lg:h-full">
<Card <Card
class="w-full md:flex md:h-full md:flex-col md:overflow-hidden" class="w-full lg:flex lg:h-full lg:flex-col lg:overflow-hidden"
:style="panelLayoutVars" :style="panelLayoutVars"
> >
<!-- <CardHeader> <!-- <CardHeader>
<CardTitle>誓约</CardTitle> <CardTitle>誓约</CardTitle>
</CardHeader> --> </CardHeader> -->
<CardContent class="space-y-4 md:min-h-0 md:flex-1 md:overflow-y-auto"> <CardContent class="space-y-4 overflow-x-hidden lg:min-h-0 lg:flex-1 lg:overflow-y-auto">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<Tabs v-model="oathTab" class="gap-0"> <Tabs v-model="oathTab" class="gap-0">
<TabsList> <TabsList>
<TabsTrigger value="suit">誓约</TabsTrigger> <TabsTrigger value="suit">誓约</TabsTrigger>
<!-- <TabsTrigger value="universal">通宝</TabsTrigger> <TabsTrigger value="universal">通宝</TabsTrigger>
<TabsTrigger value="other">其他</TabsTrigger> --> <TabsTrigger value="other">其他</TabsTrigger>
</TabsList> </TabsList>
</Tabs> </Tabs>
@@ -1039,7 +1098,7 @@ const toggleOathRarity = (value: unknown) => {
<template v-if="oathTab === 'suit'"> <template v-if="oathTab === 'suit'">
<div <div
class="mx-auto flex w-max max-w-full flex-nowrap overflow-x-auto pb-1" class="mx-auto flex flex-wrap content-start gap-1 overflow-x-hidden pb-1"
:style="iconWrapStyle" :style="iconWrapStyle"
> >
<button <button
@@ -1084,7 +1143,7 @@ const toggleOathRarity = (value: unknown) => {
> >
{{ section.title }} {{ section.title }}
</div> --> </div> -->
<div class="overflow-x-auto"> <div class="overflow-x-hidden">
<div class="mx-auto flex flex-wrap content-start gap-1" :style="iconWrapStyle"> <div class="mx-auto flex flex-wrap content-start gap-1" :style="iconWrapStyle">
<EquipmentCell <EquipmentCell
v-for="(equip, index) in section.items" v-for="(equip, index) in section.items"
@@ -1109,7 +1168,7 @@ const toggleOathRarity = (value: unknown) => {
</template> </template>
<template v-else-if="oathTab === 'universal'"> <template v-else-if="oathTab === 'universal'">
<div class="overflow-x-auto"> <div class="overflow-x-hidden">
<div class="mx-auto flex flex-wrap content-start gap-1" :style="iconWrapStyle"> <div class="mx-auto flex flex-wrap content-start gap-1" :style="iconWrapStyle">
<EquipmentCell <EquipmentCell
v-for="(equip, index) in oathUniversalGrid" v-for="(equip, index) in oathUniversalGrid"
@@ -1123,10 +1182,16 @@ const toggleOathRarity = (value: unknown) => {
/> />
</div> </div>
</div> </div>
<div
v-if="!oathUniversalGrid.some(Boolean)"
class="py-6 text-center text-xs text-muted-foreground"
>
当前暂无誓约通宝数据
</div>
</template> </template>
<template v-else> <template v-else>
<div class="overflow-x-auto"> <div class="overflow-x-hidden">
<div class="mx-auto flex flex-wrap content-start gap-1" :style="iconWrapStyle"> <div class="mx-auto flex flex-wrap content-start gap-1" :style="iconWrapStyle">
<EquipmentCell <EquipmentCell
v-for="(equip, index) in oathOtherGrid" v-for="(equip, index) in oathOtherGrid"
@@ -1140,14 +1205,20 @@ const toggleOathRarity = (value: unknown) => {
/> />
</div> </div>
</div> </div>
<div
v-if="!oathOtherGrid.some(Boolean)"
class="py-6 text-center text-xs text-muted-foreground"
>
当前暂无其它誓约数据
</div>
</template> </template>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
</div> </div>
<div class="min-h-0 space-y-4 md:h-full md:self-stretch"> <div class="min-h-0 space-y-4 lg:h-full lg:self-stretch">
<Card class="md:flex md:h-full md:flex-col md:overflow-hidden"> <Card class="lg:flex lg:h-full lg:flex-col lg:overflow-hidden">
<CardHeader> <CardHeader>
<CardTitle class="text-base"> <CardTitle class="text-base">
<Tabs v-model="sidePanelTab" class="gap-0"> <Tabs v-model="sidePanelTab" class="gap-0">
@@ -11,12 +11,13 @@ import {
AccordionItem, AccordionItem,
AccordionTrigger, AccordionTrigger,
} from "@/components/ui/accordion"; } from "@/components/ui/accordion";
import type { IJade } from "@/api/info/type";
import ResultPanel from "@/pages/Dcalc/Character/Result/Index.vue"; import ResultPanel from "@/pages/Dcalc/Character/Result/Index.vue";
const configStore = useConfigStore(); const configStore = useConfigStore();
const infoStore = useInfoStore(); const infoStore = useInfoStore();
const moduleMode = ref<"equip" | "oath" | "avatar" | "global">("equip"); const moduleMode = ref<"equip" | "oath" | "avatar" | "jade" | "sundry" | "global">("equip");
const forgeViewTab = ref<"config" | "result">("result"); const forgeViewTab = ref<"config" | "result">("result");
const notice = ref(""); const notice = ref("");
const noticeType = ref<"error" | "info">("info"); const noticeType = ref<"error" | "info">("info");
@@ -27,6 +28,8 @@ const avatarOptionKeyword = ref("");
const emblemOptionKeyword = ref(""); const emblemOptionKeyword = ref("");
const onlyConfiguredAvatarParts = ref(false); const onlyConfiguredAvatarParts = ref(false);
const onlyConfiguredEmblemSlots = ref(false); const onlyConfiguredEmblemSlots = ref(false);
const equipBatchApply = ref(false);
const oathBatchApply = ref(false);
const noEnchantAvatarParts = ["头部", "帽子", "脸部", "胸部", "上衣", "腰带", "下装", "鞋"]; const noEnchantAvatarParts = ["头部", "帽子", "脸部", "胸部", "上衣", "腰带", "下装", "鞋"];
@@ -49,6 +52,19 @@ const emblemLevels = [
"史诗Lv.1", "史诗Lv.1",
]; ];
const jadeKeys = ["0", "1", "2", "3"] as const;
const sundryFields = [
{ key: "fog", label: "迷雾系统", defaultValue: 10 },
{ key: "adventure", label: "冒险团", defaultValue: 30 },
{ key: "marriage_house", label: "结婚房屋", defaultValue: 4 },
{ key: "marriage_ring", label: "结婚戒指", defaultValue: 2 },
{ key: "contract", label: "晶体契约", defaultValue: 1 },
{ key: "collection_type", label: "收集箱类型", defaultValue: 1 },
{ key: "collection_num_0", label: "收集箱数量一", defaultValue: 5 },
{ key: "collection_num_1", label: "收集箱数量二", defaultValue: 5 },
{ key: "costume_card", label: "名称装扮", defaultValue: 1 },
] as const;
const setNotice = (message: string, type: "error" | "info" = "info") => { const setNotice = (message: string, type: "error" | "info" = "info") => {
notice.value = message; notice.value = message;
noticeType.value = type; noticeType.value = type;
@@ -71,12 +87,21 @@ const summary = computed(() => {
const emblemCount = Object.values(configStore.config.emblems || {}).reduce((acc, item) => { const emblemCount = Object.values(configStore.config.emblems || {}).reduce((acc, item) => {
return acc + (item.items?.filter(Boolean).length ?? 0); return acc + (item.items?.filter(Boolean).length ?? 0);
}, 0); }, 0);
const jadeCount = Object.values(configStore.config.jades || {}).filter(
(item) => Number(item.id) >= 0,
).length;
const sundryCount = sundryFields.filter((item) => {
const value = configStore.config.sundry?.[item.key] ?? item.defaultValue;
return value !== 0 && value !== "" && value !== undefined;
}).length;
return { return {
equipCount, equipCount,
avatarCount, avatarCount,
oathCount, oathCount,
emblemCount, emblemCount,
jadeCount,
sundryCount,
emblemSlots: infoStore.emblemParts.length, emblemSlots: infoStore.emblemParts.length,
}; };
}); });
@@ -182,9 +207,127 @@ const emblemMaxCount = computed(() => {
return 4; return 4;
}); });
const setNumberField = ( const setOathAdaptation = (part: string, value: string) => {
const target = configStore.config.oaths[part];
if (!target) return;
const parsed = Number.parseInt(value || "0", 10);
if (Number.isNaN(parsed)) {
setNotice(`誓约槽位 ${part} 的输入无效,请输入数字。`, "error");
return;
}
const partsToApply = oathBatchApply.value ? infoStore.oathParts : [part];
partsToApply.forEach((item) => {
if (!configStore.config.oaths[item]) {
configStore.config.oaths[item] = { ...configStore.defaultOathConfig };
}
configStore.config.oaths[item].adaptation = Math.max(0, parsed);
});
setNotice(oathBatchApply.value ? "已批量更新全部誓约槽位适配值。" : `已更新誓约槽位 ${part} 的适配值。`);
trackSource(oathBatchApply.value ? "誓约适配已批量更新" : `誓约槽位 ${part} 适配已更新`);
};
const equipCfg = (part: string) => configStore.config.equips[part] ?? configStore.defaultEqusConfig;
const oathCfg = (part: string) => configStore.config.oaths[part] ?? configStore.defaultOathConfig;
const avatarCfg = (part: string) =>
configStore.config.avatar[part] ?? configStore.defaultAvatarConfig;
const canUpgradeEquip = (part: string) => !["称号", "宠物"].includes(part);
const canSetSecret = (part: string) => ["武器", "辅助装备", "魔法石", "耳环"].includes(part);
const equipEnchantOptions = (part: string) =>
infoStore.enchants
.filter(
(item) =>
item.position.includes(part) &&
!item.position.includes("武器装扮") &&
!item.position.includes("宠物装备"),
)
.sort((a, b) => (b.fame ?? 0) - (a.fame ?? 0));
const formatFloat = (value: number, digits = 1) => Number(value.toFixed(digits)).toString();
const ensureJade = (key: string) => {
if (!configStore.config.jades[key]) {
configStore.config.jades[key] = { id: -1, value: 0 };
}
return configStore.config.jades[key];
};
const jadeValueOptions = (id: number) => {
if (id < 0) return [];
const current = infoStore.jades.find((item) => item.id === id) as IJade | undefined;
if (!current) return [];
const result: { id: number; value: string }[] = [];
const max = Number(current.max);
const min = Number(current.min);
const step = Number(current.pre);
if (current.unit.includes("等级")) {
result.push({ id: 1, value: current.unit });
} else if (step > 0) {
for (let value = max; value >= min; value = Number((value - step).toFixed(1))) {
if (value !== 0) {
result.push({
id: value,
value: `${value > 0 ? "+" : ""}${formatFloat(value, 1)}${current.unit}`,
});
}
}
}
return result;
};
const setJadeId = (key: string, value: string) => {
const target = ensureJade(key);
const id = Number.parseInt(value, 10);
target.id = Number.isNaN(id) ? -1 : id;
target.value = jadeValueOptions(target.id)[0]?.id ?? 0;
setNotice(`已更新辟邪玉槽位 ${Number(key) + 1}。`);
trackSource(`辟邪玉槽位 ${Number(key) + 1} 已更新`);
};
const setJadeValue = (key: string, value: string) => {
const target = ensureJade(key);
const parsed = Number(value);
target.value = Number.isNaN(parsed) ? 0 : parsed;
setNotice(`已更新辟邪玉槽位 ${Number(key) + 1} 数值。`);
trackSource(`辟邪玉槽位 ${Number(key) + 1} 数值已更新`);
};
const resetJades = () => {
jadeKeys.forEach((key) => {
configStore.config.jades[key] = { id: -1, value: 0 };
});
setNotice("已重置辟邪玉配置。");
trackSource("辟邪玉配置已重置");
};
const sundryOptions = (key: string) => infoStore.sundries[key]?.options ?? [];
const sundryValue = (key: string, defaultValue: number) => {
return String(configStore.config.sundry?.[key] ?? defaultValue);
};
const setSundryValue = (key: string, value: string) => {
const parsed = Number(value);
configStore.config.sundry[key] = Number.isNaN(parsed) ? value : parsed;
setNotice("已更新其它系统配置。");
trackSource(`其它系统 ${key} 已更新`);
};
const resetSundry = () => {
sundryFields.forEach((field) => {
configStore.config.sundry[field.key] = field.defaultValue;
});
setNotice("已重置其它系统配置。");
trackSource("其它系统配置已重置");
};
const setEquipNumberField = (
part: string, part: string,
field: "reinforce" | "refine" | "adaptation", field: "reinforce" | "reinforceType" | "refine" | "adaptation" | "precision",
value: string, value: string,
) => { ) => {
const target = configStore.config.equips[part]; const target = configStore.config.equips[part];
@@ -196,29 +339,45 @@ const setNumberField = (
return; return;
} }
target[field] = Math.max(0, parsed); const normalized =
setNotice(`已更新 ${part} 的打造参数。`); field === "precision" ? Math.max(0, Math.min(parsed, 100)) : Math.max(0, parsed);
const canBatch = equipBatchApply.value && ["reinforceType", "reinforce", "adaptation"].includes(field);
const partsToApply = canBatch ? infoStore.parts.filter(canUpgradeEquip) : [part];
partsToApply.forEach((item) => {
if (!configStore.config.equips[item]) {
configStore.config.equips[item] = { ...configStore.defaultEqusConfig };
}
configStore.config.equips[item][field] = normalized;
});
setNotice(canBatch ? "已批量更新装备打造参数。" : `已更新 ${part} 的打造参数。`);
trackSource(canBatch ? `装备 ${field} 已批量更新` : `装备 ${part} ${field} 已更新`);
}; };
const setOathAdaptation = (part: string, value: string) => { const setEquipEnchant = (part: string, value: string) => {
const target = configStore.config.oaths[part]; const target = configStore.config.equips[part];
if (!target) return; if (!target) return;
const parsed = Number.parseInt(value || "0", 10); const enchant = infoStore.enchants.find((item) => String(item.id) === value);
if (Number.isNaN(parsed)) { const partsToApply =
setNotice(`誓约槽位 ${part} 的输入无效,请输入数字。`, "error"); equipBatchApply.value && enchant?.position.length
return; ? enchant.position.filter((item) => configStore.config.equips[item])
} : [part];
target.adaptation = Math.max(0, parsed); partsToApply.forEach((item) => {
setNotice(`已更新誓约槽位 ${part} 的适配值。`); if (!configStore.config.equips[item]) {
configStore.config.equips[item] = { ...configStore.defaultEqusConfig };
}
configStore.config.equips[item].enchant = value;
});
setNotice(equipBatchApply.value && partsToApply.length > 1 ? "已批量更新装备附魔。" : `已更新 ${part} 的附魔。`);
trackSource(
equipBatchApply.value && partsToApply.length > 1 ? "装备附魔已批量更新" : `装备 ${part} 附魔已更新`,
);
}; };
const equipCfg = (part: string) => configStore.config.equips[part] ?? configStore.defaultEqusConfig;
const oathCfg = (part: string) => configStore.config.oaths[part] ?? configStore.defaultOathConfig;
const avatarCfg = (part: string) =>
configStore.config.avatar[part] ?? configStore.defaultAvatarConfig;
const ensureAvatarPart = (part: string) => { const ensureAvatarPart = (part: string) => {
if (!configStore.config.avatar[part]) { if (!configStore.config.avatar[part]) {
configStore.config.avatar[part] = { ...configStore.defaultAvatarConfig }; configStore.config.avatar[part] = { ...configStore.defaultAvatarConfig };
@@ -339,6 +498,12 @@ const resetAllForge = () => {
infoStore.emblemParts.forEach((part) => { infoStore.emblemParts.forEach((part) => {
configStore.config.emblems[part] = { ...configStore.defaultEmblemConfig }; configStore.config.emblems[part] = { ...configStore.defaultEmblemConfig };
}); });
jadeKeys.forEach((key) => {
configStore.config.jades[key] = { id: -1, value: 0 };
});
sundryFields.forEach((field) => {
configStore.config.sundry[field.key] = field.defaultValue;
});
setNotice("已重置当前角色的打造参数。", "info"); setNotice("已重置当前角色的打造参数。", "info");
trackSource("当前角色 Forge 配置已快速重置"); trackSource("当前角色 Forge 配置已快速重置");
}; };
@@ -352,7 +517,8 @@ const resetAllForge = () => {
<span>Forge 面板</span> <span>Forge 面板</span>
<span class="text-xs font-normal text-muted-foreground"> <span class="text-xs font-normal text-muted-foreground">
装备 {{ summary.equipCount }} · 时装 {{ summary.avatarCount }} · 誓约 装备 {{ summary.equipCount }} · 时装 {{ summary.avatarCount }} · 誓约
{{ summary.oathCount }} · 徽章 {{ summary.emblemCount }} {{ summary.oathCount }} · 徽章 {{ summary.emblemCount }} · 辟邪玉
{{ summary.jadeCount }} · 其它 {{ summary.sundryCount }}
</span> </span>
</CardTitle> </CardTitle>
<CardDescription>Forge 进阶:子模块可编辑,统一浅色/深色模式。</CardDescription> <CardDescription>Forge 进阶:子模块可编辑,统一浅色/深色模式。</CardDescription>
@@ -400,6 +566,20 @@ const resetAllForge = () => {
> >
时装徽章 时装徽章
</Button> </Button>
<Button
size="sm"
:variant="moduleMode === 'jade' ? 'default' : 'ghost'"
@click="moduleMode = 'jade'"
>
辟邪玉
</Button>
<Button
size="sm"
:variant="moduleMode === 'sundry' ? 'default' : 'ghost'"
@click="moduleMode = 'sundry'"
>
其它
</Button>
<Button <Button
size="sm" size="sm"
:variant="moduleMode === 'global' ? 'default' : 'ghost'" :variant="moduleMode === 'global' ? 'default' : 'ghost'"
@@ -433,6 +613,18 @@ const resetAllForge = () => {
>时装、徽章、等级与条目配置。</AccordionContent >时装、徽章、等级与条目配置。</AccordionContent
> >
</AccordionItem> </AccordionItem>
<AccordionItem value="jade">
<AccordionTrigger @click="moduleMode = 'jade'">辟邪玉</AccordionTrigger>
<AccordionContent class="text-xs text-muted-foreground"
>4 个辟邪玉词条与数值配置。</AccordionContent
>
</AccordionItem>
<AccordionItem value="sundry">
<AccordionTrigger @click="moduleMode = 'sundry'">其它</AccordionTrigger>
<AccordionContent class="text-xs text-muted-foreground"
>迷雾、冒险团、结婚、晶体契约等系统配置。</AccordionContent
>
</AccordionItem>
<AccordionItem value="global"> <AccordionItem value="global">
<AccordionTrigger @click="moduleMode = 'global'">全局</AccordionTrigger> <AccordionTrigger @click="moduleMode = 'global'">全局</AccordionTrigger>
<AccordionContent class="text-xs text-muted-foreground" <AccordionContent class="text-xs text-muted-foreground"
@@ -483,40 +675,121 @@ const resetAllForge = () => {
<CardDescription>强化、锻造、品级编辑并即时回写配置。</CardDescription> <CardDescription>强化、锻造、品级编辑并即时回写配置。</CardDescription>
</CardHeader> </CardHeader>
<CardContent class="space-y-2 text-xs sm:space-y-3 sm:text-sm"> <CardContent class="space-y-2 text-xs sm:space-y-3 sm:text-sm">
<label
class="flex flex-wrap items-center justify-between gap-2 rounded-md border bg-muted/30 p-2 text-muted-foreground"
>
<span>批量应用到同类部位</span>
<input v-model="equipBatchApply" type="checkbox" class="size-4" />
</label>
<div <div
v-for="part in parts" v-for="part in parts"
:key="part" :key="part"
class="grid grid-cols-1 gap-2 rounded-md border p-2 md:grid-cols-[7rem_1fr_6rem_6rem_6rem_auto] md:items-center md:p-3" class="space-y-3 rounded-md border p-2 md:p-3"
> >
<div class="font-medium">{{ part }}</div> <div class="grid grid-cols-[5rem_minmax(0,1fr)_auto] items-center gap-2">
<div class="truncate text-muted-foreground"> <div class="font-medium">{{ part }}</div>
{{ equipNameById.get(equipCfg(part).id) || "未选择装备(可在装备页选择)" }} <div class="truncate text-muted-foreground">
{{ equipNameById.get(equipCfg(part).id) || "未选择装备(可在装备页选择)" }}
</div>
<Button size="sm" variant="ghost" @click="resetEquipPart(part)">重置</Button>
</div>
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2 xl:grid-cols-4">
<label v-if="canUpgradeEquip(part)" class="space-y-1">
<span class="text-muted-foreground">强化类型</span>
<select
class="h-8 w-full rounded-md border border-input bg-background px-2"
:value="String(equipCfg(part).reinforceType)"
@change="
(event) =>
setEquipNumberField(
part,
'reinforceType',
(event.target as HTMLSelectElement).value,
)
"
>
<option value="0">强化</option>
<option value="1">增幅</option>
</select>
</label>
<label v-if="canUpgradeEquip(part)" class="space-y-1">
<span class="text-muted-foreground">强化/增幅</span>
<Input
:model-value="String(equipCfg(part).reinforce)"
type="number"
min="0"
max="30"
class="h-8 text-xs"
@update:model-value="
(value) => setEquipNumberField(part, 'reinforce', String(value))
"
/>
</label>
<label class="space-y-1">
<span class="text-muted-foreground">锻造</span>
<Input
:model-value="String(equipCfg(part).refine)"
type="number"
min="0"
max="8"
class="h-8 text-xs"
@update:model-value="
(value) => setEquipNumberField(part, 'refine', String(value))
"
/>
</label>
<label v-if="canUpgradeEquip(part)" class="space-y-1">
<span class="text-muted-foreground">调适</span>
<Input
:model-value="String(equipCfg(part).adaptation)"
type="number"
min="0"
max="3"
class="h-8 text-xs"
@update:model-value="
(value) => setEquipNumberField(part, 'adaptation', String(value))
"
/>
</label>
<label v-if="equipEnchantOptions(part).length" class="space-y-1 sm:col-span-2">
<span class="text-muted-foreground">附魔</span>
<select
class="h-8 w-full min-w-0 rounded-md border border-input bg-background px-2"
:value="equipCfg(part).enchant"
@change="
(event) => setEquipEnchant(part, (event.target as HTMLSelectElement).value)
"
>
<option value="">无</option>
<option
v-for="item in equipEnchantOptions(part)"
:key="item.id"
:value="String(item.id)"
>
{{ item.detail }}
</option>
</select>
</label>
<label v-if="canSetSecret(part)" class="space-y-1">
<span class="text-muted-foreground">秘宝精度</span>
<Input
:model-value="String(equipCfg(part).precision)"
type="number"
min="0"
max="100"
class="h-8 text-xs"
@update:model-value="
(value) => setEquipNumberField(part, 'precision', String(value))
"
/>
</label>
</div> </div>
<Input
:model-value="String(equipCfg(part).reinforce)"
type="number"
min="0"
class="h-8 text-xs"
placeholder="强化"
@update:model-value="(value) => setNumberField(part, 'reinforce', String(value))"
/>
<Input
:model-value="String(equipCfg(part).refine)"
type="number"
min="0"
class="h-8 text-xs"
placeholder="锻造"
@update:model-value="(value) => setNumberField(part, 'refine', String(value))"
/>
<Input
:model-value="String(equipCfg(part).adaptation)"
type="number"
min="0"
class="h-8 text-xs"
placeholder="品级"
@update:model-value="(value) => setNumberField(part, 'adaptation', String(value))"
/>
<Button size="sm" variant="ghost" @click="resetEquipPart(part)">重置</Button>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
@@ -532,6 +805,12 @@ const resetAllForge = () => {
<CardDescription>编辑誓约槽位适配值。</CardDescription> <CardDescription>编辑誓约槽位适配值。</CardDescription>
</CardHeader> </CardHeader>
<CardContent class="space-y-2 text-xs sm:space-y-3 sm:text-sm"> <CardContent class="space-y-2 text-xs sm:space-y-3 sm:text-sm">
<label
class="flex flex-wrap items-center justify-between gap-2 rounded-md border bg-muted/30 p-2 text-muted-foreground"
>
<span>批量应用到全部誓约槽位</span>
<input v-model="oathBatchApply" type="checkbox" class="size-4" />
</label>
<div <div
v-for="part in oathParts" v-for="part in oathParts"
:key="part" :key="part"
@@ -779,6 +1058,104 @@ const resetAllForge = () => {
</Card> </Card>
</div> </div>
<div
v-if="forgeViewTab === 'config' && moduleMode === 'jade'"
class="grid grid-cols-1 gap-4"
>
<Card class="border-border/60 bg-card/90">
<CardHeader>
<CardTitle class="flex items-center gap-2">
<Sparkles class="size-4" />
辟邪玉
</CardTitle>
<CardDescription>从旧版 Forge 迁移的 4 个辟邪玉词条与数值配置。</CardDescription>
</CardHeader>
<CardContent class="space-y-2 text-xs sm:space-y-3 sm:text-sm">
<div
v-for="key in jadeKeys"
:key="key"
class="grid grid-cols-1 gap-2 rounded-md border p-2 md:grid-cols-[6rem_1fr_12rem] md:items-center md:p-3"
>
<div class="font-medium">槽位 {{ Number(key) + 1 }}</div>
<select
class="h-8 min-w-0 rounded-md border border-input bg-background px-2"
:value="String(ensureJade(key).id)"
@change="(event) => setJadeId(key, (event.target as HTMLSelectElement).value)"
>
<option value="-1">无</option>
<option v-for="item in infoStore.jades" :key="item.id" :value="String(item.id)">
{{ item.name }}
</option>
</select>
<select
class="h-8 min-w-0 rounded-md border border-input bg-background px-2"
:value="String(ensureJade(key).value)"
:disabled="ensureJade(key).id < 0"
@change="(event) => setJadeValue(key, (event.target as HTMLSelectElement).value)"
>
<option value="0">无</option>
<option
v-for="item in jadeValueOptions(ensureJade(key).id)"
:key="item.id"
:value="String(item.id)"
>
{{ item.value }}
</option>
</select>
</div>
<div class="flex justify-end">
<Button size="sm" variant="outline" @click="resetJades">重置辟邪玉</Button>
</div>
</CardContent>
</Card>
</div>
<div
v-if="forgeViewTab === 'config' && moduleMode === 'sundry'"
class="grid grid-cols-1 gap-4"
>
<Card class="border-border/60 bg-card/90">
<CardHeader>
<CardTitle class="flex items-center gap-2">
<ShieldCheck class="size-4" />
其它系统
</CardTitle>
<CardDescription>迷雾、冒险团、结婚系统、晶体契约、收集箱等配置。</CardDescription>
</CardHeader>
<CardContent class="space-y-2 text-xs sm:space-y-3 sm:text-sm">
<div class="grid grid-cols-1 gap-2 md:grid-cols-2 xl:grid-cols-3">
<div
v-for="field in sundryFields"
:key="field.key"
class="grid grid-cols-1 gap-2 rounded-md border p-2 sm:grid-cols-[6rem_1fr] sm:items-center sm:p-3"
>
<div class="font-medium">{{ field.label }}</div>
<select
class="h-8 min-w-0 rounded-md border border-input bg-background px-2"
:value="sundryValue(field.key, field.defaultValue)"
@change="
(event) => setSundryValue(field.key, (event.target as HTMLSelectElement).value)
"
>
<option
v-for="item in sundryOptions(field.key)"
:key="`${field.key}-${item.value}`"
:value="String(item.value)"
>
{{ item.name }}
</option>
</select>
</div>
</div>
<div class="flex justify-end">
<Button size="sm" variant="outline" @click="resetSundry">重置其它系统</Button>
</div>
</CardContent>
</Card>
</div>
<div <div
v-if="forgeViewTab === 'config' && moduleMode === 'global'" v-if="forgeViewTab === 'config' && moduleMode === 'global'"
class="grid grid-cols-1 gap-4 lg:grid-cols-2" class="grid grid-cols-1 gap-4 lg:grid-cols-2"
@@ -815,6 +1192,8 @@ const resetAllForge = () => {
<div>誓约槽位: {{ summary.oathCount }}</div> <div>誓约槽位: {{ summary.oathCount }}</div>
<div>已配置时装: {{ summary.avatarCount }}</div> <div>已配置时装: {{ summary.avatarCount }}</div>
<div>已配置徽章条目: {{ summary.emblemCount }}</div> <div>已配置徽章条目: {{ summary.emblemCount }}</div>
<div>已配置辟邪玉: {{ summary.jadeCount }}</div>
<div>其它系统项目: {{ summary.sundryCount }}</div>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
+230 -2
View File
@@ -1,13 +1,27 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, onUnmounted, ref, watch } from "vue"; import { computed, onMounted, onUnmounted, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router"; import { useRoute, useRouter } from "vue-router";
import { PanelRight } from "lucide-vue-next"; import { FileInput, PanelRight, WandSparkles } from "lucide-vue-next";
import api from "@/api";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet"; import { Input } from "@/components/ui/input";
import {
Sheet,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
import { Textarea } from "@/components/ui/textarea";
import Tabs from "@/components/ui/tabs/Tabs.vue"; import Tabs from "@/components/ui/tabs/Tabs.vue";
import TabsList from "@/components/ui/tabs/TabsList.vue"; import TabsList from "@/components/ui/tabs/TabsList.vue";
import TabsTrigger from "@/components/ui/tabs/TabsTrigger.vue"; import TabsTrigger from "@/components/ui/tabs/TabsTrigger.vue";
import { useConfigStore, useInfoStore } from "@/stores"; import { useConfigStore, useInfoStore } from "@/stores";
import { useDnfHelperStore } from "@/stores/dnfhelper";
import type { IConfigEmblem, IConfigEquip, IConfigOath } from "@/stores/config";
import type { IHelperInfo } from "@/api/info/type";
import ResultPanel from "@/pages/Dcalc/Character/Result/Index.vue"; import ResultPanel from "@/pages/Dcalc/Character/Result/Index.vue";
const props = defineProps<{ const props = defineProps<{
alter: string; alter: string;
@@ -17,14 +31,50 @@ const route = useRoute();
const router = useRouter(); const router = useRouter();
const infoStore = useInfoStore(); const infoStore = useInfoStore();
const configStore = useConfigStore(); const configStore = useConfigStore();
const helperStore = useDnfHelperStore();
const loading = ref(false); const loading = ref(false);
const loadError = ref(""); const loadError = ref("");
const isExecuting = ref(false); const isExecuting = ref(false);
const isMobileLayout = ref(false); const isMobileLayout = ref(false);
const isResultSheetOpen = ref(false); const isResultSheetOpen = ref(false);
const helperImportOpen = ref(false);
const skillImportOpen = ref(false);
const helperRoleImportOpen = ref(false);
const helperUid = ref("");
const skillCode = ref("");
const helperImportLoading = ref(false);
const skillImportLoading = ref(false);
const importStatus = ref("");
const importError = ref("");
const helperRole = ref<IHelperInfo["roleList"][number]>();
let mediaQuery: MediaQueryList | null = null; let mediaQuery: MediaQueryList | null = null;
type AvatarConfig = Record<
string,
{
id: string;
enchant: string;
emblem_0: string;
emblem_1: string;
option: string;
}
>;
interface HelperConfigPayload {
equips?: Record<string, IConfigEquip>;
avatar?: AvatarConfig;
avatars?: AvatarConfig;
avatarLink?: string;
emblems?: Record<string, IConfigEmblem>;
fogOathLv?: unknown;
oaths?: Record<string, IConfigOath>;
}
interface SkillImportPayload {
skills?: typeof configStore.config.skills;
}
const updateMobileLayout = () => { const updateMobileLayout = () => {
isMobileLayout.value = mediaQuery?.matches ?? false; isMobileLayout.value = mediaQuery?.matches ?? false;
if (!isMobileLayout.value) { if (!isMobileLayout.value) {
@@ -55,6 +105,7 @@ const loadCharacter = async () => {
loadError.value = ""; loadError.value = "";
try { try {
await infoStore.createCharacter(props.alter); await infoStore.createCharacter(props.alter);
prepareHelperRoleImport();
} catch (error) { } catch (error) {
loadError.value = error instanceof Error ? error.message : "角色数据加载失败"; loadError.value = error instanceof Error ? error.message : "角色数据加载失败";
} finally { } finally {
@@ -66,6 +117,103 @@ const switchTab = (tab: (typeof tabs)[number]["key"]) => {
router.push(`/dcalc/character/${props.alter}/${tab}`); router.push(`/dcalc/character/${props.alter}/${tab}`);
}; };
const resetImportMessage = () => {
importStatus.value = "";
importError.value = "";
};
const applyHelperConfig = (payload: HelperConfigPayload) => {
if (payload.equips) configStore.config.equips = payload.equips;
if (payload.avatars || payload.avatar) {
configStore.config.avatar = payload.avatars ?? payload.avatar ?? configStore.config.avatar;
}
if (payload.avatarLink !== undefined) configStore.config.avatarLink = payload.avatarLink;
if (payload.emblems) configStore.config.emblems = payload.emblems;
if (payload.fogOathLv !== undefined) configStore.config.sundry.fog = payload.fogOathLv;
if (payload.oaths) configStore.config.oaths = payload.oaths;
configStore.pushChangeSource("Import", "助手打造配置已导入");
};
const importHelperConfig = async () => {
resetImportMessage();
const uid = helperUid.value.trim();
if (!uid) {
importError.value = "请输入助手社区 ID。";
return;
}
helperImportLoading.value = true;
try {
const payload = (await api.getConfigFromHelper(uid)) as HelperConfigPayload;
applyHelperConfig(payload);
helperImportOpen.value = false;
helperUid.value = "";
importStatus.value = "打造导入成功。";
} catch (error) {
importError.value = error instanceof Error ? error.message : "打造导入失败。";
} finally {
helperImportLoading.value = false;
}
};
const importHelperRoleConfig = async () => {
resetImportMessage();
const uid = helperStore.helperInfo?.id;
const roleId = helperRole.value?.roleId;
if (!uid || !roleId) {
importError.value = "未找到可导入的助手角色。";
return;
}
helperImportLoading.value = true;
try {
const payload = (await api.getConfigFromHelperAlter(uid, roleId)) as HelperConfigPayload;
applyHelperConfig(payload);
helperRoleImportOpen.value = false;
importStatus.value = "助手角色打造导入成功。";
} catch (error) {
importError.value = error instanceof Error ? error.message : "助手角色打造导入失败。";
} finally {
helperImportLoading.value = false;
}
};
const importSkillConfig = async () => {
resetImportMessage();
const code = skillCode.value.trim();
if (!code) {
importError.value = "请输入技能加点代码。";
return;
}
skillImportLoading.value = true;
try {
const payload = (await api.getSkillFromCode(code)) as SkillImportPayload;
if (payload.skills) {
configStore.config.skills = payload.skills;
configStore.pushChangeSource("Import", "技能加点代码已导入");
}
skillImportOpen.value = false;
skillCode.value = "";
importStatus.value = "技能导入成功。";
} catch (error) {
importError.value = error instanceof Error ? error.message : "技能导入失败。";
} finally {
skillImportLoading.value = false;
}
};
const prepareHelperRoleImport = () => {
const roleId = route.query.roleId;
if (typeof roleId !== "string") return;
const target = helperStore.helperInfo?.roleList.find((item) => item.roleId === roleId);
if (!target) return;
helperRole.value = target;
helperRoleImportOpen.value = true;
};
onMounted(() => { onMounted(() => {
loadCharacter(); loadCharacter();
window.addEventListener("beforeunload", configStore.saveConfig); window.addEventListener("beforeunload", configStore.saveConfig);
@@ -81,6 +229,13 @@ watch(
}, },
); );
watch(
() => route.query.roleId,
() => {
prepareHelperRoleImport();
},
);
watch(showGlobalResultPanel, (show) => { watch(showGlobalResultPanel, (show) => {
if (!show) { if (!show) {
isResultSheetOpen.value = false; isResultSheetOpen.value = false;
@@ -125,6 +280,79 @@ onUnmounted(() => {
</TabsList> </TabsList>
</Tabs> </Tabs>
<div class="flex flex-wrap items-center gap-2">
<Sheet v-model:open="helperImportOpen">
<SheetTrigger as-child>
<Button size="sm" variant="outline" @click="resetImportMessage">
<FileInput class="size-4" />
打造导入
</Button>
</SheetTrigger>
<SheetContent class="w-full overflow-y-auto sm:max-w-md">
<SheetHeader>
<SheetTitle>打造导入</SheetTitle>
<SheetDescription>助手社区 ID</SheetDescription>
</SheetHeader>
<div class="grid gap-3 px-4">
<Input v-model="helperUid" placeholder="社区 ID" @keyup.enter="importHelperConfig" />
<p v-if="importError" class="text-sm text-destructive">{{ importError }}</p>
</div>
<SheetFooter>
<Button :disabled="helperImportLoading" @click="importHelperConfig">
{{ helperImportLoading ? "导入中..." : "导入" }}
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
<Sheet v-model:open="skillImportOpen">
<SheetTrigger as-child>
<Button size="sm" variant="outline" @click="resetImportMessage">
<WandSparkles class="size-4" />
技能导入
</Button>
</SheetTrigger>
<SheetContent class="w-full overflow-y-auto sm:max-w-md">
<SheetHeader>
<SheetTitle>技能导入</SheetTitle>
<SheetDescription>技能加点代码</SheetDescription>
</SheetHeader>
<div class="grid gap-3 px-4">
<Textarea v-model="skillCode" class="min-h-28" placeholder="技能代码" />
<p v-if="importError" class="text-sm text-destructive">{{ importError }}</p>
</div>
<SheetFooter>
<Button :disabled="skillImportLoading" @click="importSkillConfig">
{{ skillImportLoading ? "导入中..." : "导入" }}
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
<p v-if="importStatus" class="text-xs text-muted-foreground">{{ importStatus }}</p>
</div>
<Sheet v-model:open="helperRoleImportOpen">
<SheetContent class="w-full overflow-y-auto sm:max-w-md">
<SheetHeader>
<SheetTitle>助手角色导入</SheetTitle>
<SheetDescription>{{ helperRole?.roleName || "助手角色" }}</SheetDescription>
</SheetHeader>
<div class="grid gap-2 px-4 text-sm">
<div class="rounded-md border p-3">
<div class="font-medium">{{ helperRole?.roleJob }}</div>
<div class="text-muted-foreground">名望 {{ helperRole?.fame ?? "-" }}</div>
</div>
<p v-if="importError" class="text-sm text-destructive">{{ importError }}</p>
</div>
<SheetFooter>
<Button :disabled="helperImportLoading" @click="importHelperRoleConfig">
{{ helperImportLoading ? "导入中..." : "导入" }}
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
<div <div
:class="[ :class="[
'grid min-h-0 flex-1 grid-cols-1 gap-4', 'grid min-h-0 flex-1 grid-cols-1 gap-4',
@@ -1,21 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from "vue"; import { computed, ref } from "vue";
import type { IResultSkill } from "@/api/calc/type";
import { useConfigStore, useInfoStore } from "@/stores";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
import Tabs from "@/components/ui/tabs/Tabs.vue"; import Tabs from "@/components/ui/tabs/Tabs.vue";
import TabsList from "@/components/ui/tabs/TabsList.vue"; import TabsList from "@/components/ui/tabs/TabsList.vue";
import TabsTrigger from "@/components/ui/tabs/TabsTrigger.vue"; import TabsTrigger from "@/components/ui/tabs/TabsTrigger.vue";
import EquList from "@/components/dnf/Equipment/List/index.vue"; import EquList from "@/components/dnf/Equipment/List/index.vue";
import OathList from "@/components/dnf/Equipment/Oath/index.vue"; import OathList from "@/components/dnf/Equipment/Oath/index.vue";
import { useConfigStore, useInfoStore } from "@/stores";
const configStore = useConfigStore(); const configStore = useConfigStore();
const infoStore = useInfoStore(); const infoStore = useInfoStore();
@@ -26,8 +18,6 @@ const equippedOathPart = ref("0");
const resultState = computed(() => configStore.result.state); const resultState = computed(() => configStore.result.state);
const isLoading = computed(() => configStore.result.isLoading); const isLoading = computed(() => configStore.result.isLoading);
const isBufferClass = computed(() => !!resultState.value?.buffer);
const standard = computed(() => infoStore.standard);
const resultError = computed(() => { const resultError = computed(() => {
const error = configStore.result.error; const error = configStore.result.error;
@@ -35,57 +25,26 @@ const resultError = computed(() => {
return error instanceof Error ? error.message : String(error); return error instanceof Error ? error.message : String(error);
}); });
const retryCalc = async () => {
await configStore.result.execute();
};
const setStandard = () => {
if (!resultState.value?.uuid) return;
infoStore.setStandard(resultState.value, configStore.skillCountConfig);
};
const clearStandard = () => {
infoStore.setStandard(undefined);
};
const getSkillCountEntry = (skill: IResultSkill) => {
const name = skill.name;
const mode = skill.mode;
const existed = configStore.skillCountConfig.find(
(item) => item.name === name && item.mode === mode,
);
if (existed) return existed;
const created = { name, mode, count: 1 };
configStore.skillCountConfig.push(created);
return created;
};
const getSkillCount = (skill: IResultSkill) =>
Math.max(0, Number(getSkillCountEntry(skill).count) || 0);
const getStandardSkill = (skill: IResultSkill) =>
standard.value?.skills.find((item) => item.name === skill.name && item.mode === skill.mode);
const getStandardSkillCount = (skill: IResultSkill) => {
const count = standard.value?.skillCount.find(
(item) => item.name === skill.name && item.mode === skill.mode,
)?.count;
return Number(count ?? 0);
};
const skills = computed(() => resultState.value?.skills ?? []); const skills = computed(() => resultState.value?.skills ?? []);
const totalWeightedDamage = computed(() => const totalWeightedDamage = computed(() =>
skills.value.reduce((sum, item) => sum + item.damage * getSkillCount(item), 0), skills.value.reduce((sum, item) => {
const count = configStore.skillCountConfig.find(
(config) => config.name === item.name && config.mode === item.mode,
)?.count;
return sum + item.damage * Math.max(0, Number(count ?? 1) || 0);
}, 0),
); );
const totalStandardDamage = computed(() => { const hasVisibleResult = computed(
if (!standard.value) return 0; () => (resultState.value?.skills?.length ?? 0) > 0 || (resultState.value?.info?.length ?? 0) > 0,
return skills.value.reduce((sum, item) => { );
const base = getStandardSkill(item);
if (!base) return sum; const statusText = computed(() => {
return sum + base.damage * getStandardSkillCount(item); if (isLoading.value) return "计算中...";
}, 0); if (resultError.value) return resultError.value;
if (!hasVisibleResult.value) return "暂无结果";
return "";
}); });
const formatNumber = (value: number) => { const formatNumber = (value: number) => {
@@ -93,152 +52,54 @@ const formatNumber = (value: number) => {
if (Math.abs(value) >= 100000000) return `${(value / 100000000).toFixed(2)}e`; if (Math.abs(value) >= 100000000) return `${(value / 100000000).toFixed(2)}e`;
return value.toLocaleString("zh-CN", { maximumFractionDigits: 2 }); return value.toLocaleString("zh-CN", { maximumFractionDigits: 2 });
}; };
const totalDiffLabel = computed(() => {
if (!totalStandardDamage.value) return formatNumber(totalWeightedDamage.value);
const diff =
((totalWeightedDamage.value - totalStandardDamage.value) / totalStandardDamage.value) * 100;
return `${diff >= 0 ? "+" : ""}${diff.toFixed(2)}%`;
});
const totalDiffClass = computed(() => {
if (!totalStandardDamage.value) return "text-foreground";
return totalWeightedDamage.value >= totalStandardDamage.value
? "text-emerald-600"
: "text-red-600";
});
const hasVisibleResult = computed(
() => (resultState.value?.skills?.length ?? 0) > 0 || (resultState.value?.info?.length ?? 0) > 0,
);
const changeSources = computed(() => configStore.changeSourceLogs.slice(0, 8));
const clearChangeSources = () => {
configStore.clearChangeSource();
};
const defaultOpenSections = ["overview", "equipped"];
const statusText = computed(() => {
if (isLoading.value) return "计算中...";
if (resultError.value) return resultError.value;
if (!hasVisibleResult.value) return "暂无结果,请先编辑配置。";
if (isBufferClass.value) return "请前往计算详情查看。";
return "";
});
</script> </script>
<template> <template>
<div class="flex h-full min-h-0 flex-col gap-3"> <div class="flex h-full min-h-0 flex-col gap-3">
<Card> <Card class="shrink-0">
<CardContent class="py-2"> <CardContent class="p-3">
<Accordion type="multiple" :default-value="defaultOpenSections" class="w-full"> <div class="rounded-md border border-border/60 px-3 py-2">
<AccordionItem value="overview"> <div class="text-xs text-muted-foreground">总伤害</div>
<AccordionTrigger>结果概览</AccordionTrigger> <div
<AccordionContent> v-if="statusText"
<div class="space-y-3 px-1"> class="mt-1 text-sm"
<div class="flex flex-wrap gap-2"> :class="resultError ? 'text-destructive' : 'text-muted-foreground'"
<Button size="sm" variant="outline" class="h-8 px-2.5" @click="setStandard" >
>设为基准</Button {{ statusText }}
> </div>
<Button size="sm" variant="outline" class="h-8 px-2.5" @click="clearStandard" <div v-else class="mt-1 truncate text-xl font-semibold tabular-nums">
>清空基准</Button {{ formatNumber(totalWeightedDamage) }}
> </div>
</div> </div>
</CardContent>
</Card>
<div <Card class="min-h-0 flex-1">
v-if="isLoading || resultError || !hasVisibleResult || isBufferClass" <CardContent class="flex h-full min-h-0 flex-col gap-2 p-3 text-xs">
class="rounded-md border border-border/60 px-3 py-2 text-center text-sm" <Tabs :model-value="displayTab" class="shrink-0">
:class="resultError ? 'text-destructive' : 'text-muted-foreground'" <TabsList class="grid w-full grid-cols-2">
> <TabsTrigger value="equ" @click="displayTab = 'equ'">装备</TabsTrigger>
{{ statusText }} <TabsTrigger value="oath" @click="displayTab = 'oath'">誓约</TabsTrigger>
</div> </TabsList>
</Tabs>
<div <ScrollArea class="min-h-0 flex-1 rounded-md border p-2">
v-else <div v-if="displayTab === 'equ'">
class="flex items-center justify-between rounded-md border border-border/60 px-3 py-2" <EquList
> v-model:part="equippedPart"
<span class="text-sm text-muted-foreground">合计伤害</span> :detail="configStore.config.equips"
<span class="text-sm font-semibold" :class="totalDiffClass">{{ :with-sub-weapon="infoStore.infos?.alter?.includes('vegabond') ?? false"
totalDiffLabel :with-pet="true"
}}</span> />
</div> </div>
<div v-else>
<OathList v-model:part="equippedOathPart" :detail="configStore.config.oaths" />
</div>
<div v-if="resultError" class="flex justify-center"> <template #scrollbar>
<Button size="sm" variant="outline" @click="retryCalc">重试计算</Button> <ScrollBar orientation="vertical" />
</div> </template>
</div> </ScrollArea>
</AccordionContent>
</AccordionItem>
<AccordionItem value="equipped">
<AccordionTrigger>穿戴信息</AccordionTrigger>
<AccordionContent>
<div class="space-y-2 px-1 text-xs">
<Tabs :model-value="displayTab">
<TabsList class="grid w-full grid-cols-2">
<TabsTrigger value="equ" @click="displayTab = 'equ'">装备</TabsTrigger>
<TabsTrigger value="oath" @click="displayTab = 'oath'">誓约</TabsTrigger>
</TabsList>
</Tabs>
<ScrollArea class="max-h-56 rounded-md border p-2">
<div v-if="displayTab === 'equ'">
<EquList
v-model:part="equippedPart"
:detail="configStore.config.equips"
:with-sub-weapon="infoStore.infos?.alter?.includes('vegabond') ?? false"
:with-pet="true"
/>
</div>
<div v-else>
<OathList v-model:part="equippedOathPart" :detail="configStore.config.oaths" />
</div>
<template #scrollbar>
<ScrollBar orientation="vertical" />
</template>
</ScrollArea>
</div>
</AccordionContent>
</AccordionItem>
<AccordionItem value="changes">
<AccordionTrigger>配置变更来源</AccordionTrigger>
<AccordionContent>
<div class="space-y-2 px-1">
<div class="flex items-center justify-between text-xs text-muted-foreground">
<span>最近触发计算的操作记录</span>
<Button size="sm" variant="ghost" class="h-7 px-2" @click="clearChangeSources"
>清空</Button
>
</div>
<div v-if="changeSources.length === 0" class="text-xs text-muted-foreground">
暂无变更来源记录。
</div>
<ScrollArea v-else class="max-h-40 rounded-md border border-border/60 p-2">
<div class="space-y-1">
<div
v-for="item in changeSources"
:key="item.id"
class="rounded-md border border-border/60 bg-muted/20 px-2 py-1 text-xs"
>
<div class="flex items-center justify-between gap-2">
<span class="font-medium">{{ item.source }}</span>
<span class="text-muted-foreground">{{ item.time }}</span>
</div>
<div class="text-muted-foreground">{{ item.detail }}</div>
</div>
</div>
<template #scrollbar>
<ScrollBar orientation="vertical" />
</template>
</ScrollArea>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
@@ -28,7 +28,7 @@ const noticeType = ref<"error" | "info">("info");
const openUpPickerSlot = ref<number | null>(null); const openUpPickerSlot = ref<number | null>(null);
const rightPanelTab = ref<"detail" | "result">("result"); const rightPanelTab = ref<"detail" | "result">("result");
const leftPanelWidthClass = computed(() => const leftPanelWidthClass = computed(() =>
rightPanelTab.value === "result" ? "xl:w-[460px]" : "xl:w-[550px]", rightPanelTab.value === "result" ? "xl:w-[360px]" : "xl:w-[550px]",
); );
/* ─── Constants ─── */ /* ─── Constants ─── */
@@ -193,6 +193,19 @@ const upOptionsForSlot = (slotIdx: number) => {
.sort((a, b) => a.learnLv - b.learnLv); .sort((a, b) => a.learnLv - b.learnLv);
}; };
const firstAwakeSkill = computed(() =>
infoStore.skills.find((item) => item.learnLv === 50 && item.type === "active" && item.bind),
);
const secondAwakeSkill = computed(() =>
infoStore.skills.find((item) => item.learnLv === 85 && item.type === "active" && item.bind),
);
const canBindAwake = computed(() => !!firstAwakeSkill.value || !!secondAwakeSkill.value);
const setBindAwake = (level: 50 | 85) => {
configStore.config.bindAwake = level;
track(`觉醒绑定切换为 ${level === 50 ? "一觉" : "二觉"}`);
};
const setUPSlotSkill = (slotIdx: number, nextId: string) => { const setUPSlotSkill = (slotIdx: number, nextId: string) => {
const slot = upSlots.value[slotIdx]; const slot = upSlots.value[slotIdx];
if (!slot) return; if (!slot) return;
@@ -337,6 +350,33 @@ const resetAllSkills = () => {
: '', : '',
]" ]"
/> />
<div
v-if="skill.learnLv === 100 && skill.bind && canBindAwake"
class="awake-bind-panel"
@click.stop
>
<button
v-if="firstAwakeSkill"
class="awake-bind-btn"
:class="{ active: configStore.config.bindAwake === 50 }"
title="绑定一觉"
@click.stop="setBindAwake(50)"
>
<img :src="getImageURL(firstAwakeSkill.icon)" :alt="firstAwakeSkill.name" />
</button>
<button
v-if="secondAwakeSkill"
class="awake-bind-btn"
:class="{ active: configStore.config.bindAwake === 85 }"
title="绑定二觉"
@click.stop="setBindAwake(85)"
>
<img
:src="getImageURL(secondAwakeSkill.icon)"
:alt="secondAwakeSkill.name"
/>
</button>
</div>
<div <div
v-if="activeSkillId === skill.id.toString()" v-if="activeSkillId === skill.id.toString()"
class="action-row" class="action-row"
@@ -399,6 +439,36 @@ const resetAllSkills = () => {
: '', : '',
]" ]"
/> />
<div
v-if="cell.learnLv === 100 && cell.bind && canBindAwake"
class="awake-bind-panel"
@click.stop
>
<button
v-if="firstAwakeSkill"
class="awake-bind-btn"
:class="{ active: configStore.config.bindAwake === 50 }"
title="绑定一觉"
@click.stop="setBindAwake(50)"
>
<img
:src="getImageURL(firstAwakeSkill.icon)"
:alt="firstAwakeSkill.name"
/>
</button>
<button
v-if="secondAwakeSkill"
class="awake-bind-btn"
:class="{ active: configStore.config.bindAwake === 85 }"
title="绑定二觉"
@click.stop="setBindAwake(85)"
>
<img
:src="getImageURL(secondAwakeSkill.icon)"
:alt="secondAwakeSkill.name"
/>
</button>
</div>
<div <div
v-if="activeSkillId === cell.id.toString()" v-if="activeSkillId === cell.id.toString()"
class="action-row" class="action-row"
@@ -844,6 +914,41 @@ const resetAllSkills = () => {
border-radius: 2px; border-radius: 2px;
} }
.awake-bind-panel {
position: absolute;
top: 10px;
left: 50%;
z-index: 8;
display: flex;
gap: 2px;
transform: translateX(-50%);
border-radius: 4px;
background: color-mix(in oklab, var(--background) 88%, transparent);
padding: 2px;
box-shadow: 0 0 0 1px color-mix(in oklab, var(--border) 80%, transparent);
}
.awake-bind-btn {
width: 22px;
height: 22px;
border: 1px solid color-mix(in oklab, var(--border) 70%, transparent);
border-radius: 3px;
overflow: hidden;
opacity: 0.62;
}
.awake-bind-btn.active {
border-color: var(--primary);
opacity: 1;
box-shadow: 0 0 0 1px var(--primary);
}
.awake-bind-btn img {
width: 100%;
height: 100%;
object-fit: cover;
}
/* 4 action buttons at the bottom */ /* 4 action buttons at the bottom */
.action-row { .action-row {
position: absolute; position: absolute;
+10 -1
View File
@@ -1,5 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from "vue"; import { computed } from "vue";
import { useRouter } from "vue-router";
import api from "@/api";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
// import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; // import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { import {
@@ -26,6 +28,7 @@ const props = defineProps<{
const { list } = toRefs(props); const { list } = toRefs(props);
const helperStore = useDnfHelperStore(); const helperStore = useDnfHelperStore();
const router = useRouter();
const sortedList = computed(() => { const sortedList = computed(() => {
return [...list.value].sort((left, right) => right.fame - left.fame); return [...list.value].sort((left, right) => right.fame - left.fame);
@@ -34,6 +37,12 @@ const sortedList = computed(() => {
const formatFame = (fame: number) => { const formatFame = (fame: number) => {
return new Intl.NumberFormat("zh-CN").format(fame); return new Intl.NumberFormat("zh-CN").format(fame);
}; };
const importRole = async (item: (typeof props.list)[number]) => {
const alter = await api.alterByName(item.roleJob);
if (!alter?.value) return;
await router.push(`/dcalc/character/${alter.value}/equipment?roleId=${item.roleId}`);
};
</script> </script>
<template> <template>
@@ -64,7 +73,7 @@ const formatFame = (fame: number) => {
<CardHeader class="pb-3"> <CardHeader class="pb-3">
<CardTitle class="text-base flex"> <CardTitle class="text-base flex">
{{ item.roleName }} {{ item.roleName }}
<Button class="ml-auto" size="sm" variant="outline" @click="$router.push('/dcalc/home')" <Button class="ml-auto" size="sm" variant="outline" @click="importRole(item)"
>导入</Button >导入</Button
> >
</CardTitle> </CardTitle>
+2 -2
View File
@@ -102,7 +102,7 @@ onMounted(() => {
:disabled="!child.open" :disabled="!child.open"
@click="openAlter(child)" @click="openAlter(child)"
> >
<item class="flex items-center gap-2"> <div class="flex items-center gap-2">
<img <img
:src=" :src="
getImageURL(`characters/${adventure.name}/${child.name}/material/Seicing_3.png`) getImageURL(`characters/${adventure.name}/${child.name}/material/Seicing_3.png`)
@@ -111,7 +111,7 @@ onMounted(() => {
class="size-8 rounded-sm object-cover" class="size-8 rounded-sm object-cover"
/> />
<span class="truncate">{{ child.title }}</span> <span class="truncate">{{ child.title }}</span>
</item> </div>
<ArrowRight class="size-3" /> <ArrowRight class="size-3" />
</Button> </Button>
</CardContent> </CardContent>