From bb3ad71d21650327ffc0b47c1c94f2fb1f0beab1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A5=BF=E7=93=9C=C2=B0?= Date: Tue, 2 Jun 2026 20:56:07 +0800 Subject: [PATCH] 1 --- backend/api/core/Auth.py | 8 +- backend/api/core/Redis.py | 2 +- backend/api/main.py | 8 +- backend/api/routes/calc.py | 16 +- backend/api/routes/colg.py | 2 +- backend/api/routes/info.py | 42 +- backend/api/routes/openapi.py | 35 +- backend/api/types/colg.py | 3 - backend/api/utils/skill_detail.py | 60 +++ backend/core/abstract/character.py | 9 +- backend/core/basic/character/base.py | 32 +- backend/core/basic/character/calc_equip.py | 97 ++-- backend/core/basic/equipment.py | 22 +- backend/core/basic/property.py | 2 +- backend/core/basic/roleinfo.py | 22 +- backend/core/basic/skill.py | 125 +++-- backend/core/character/GF/archer/hunter.py | 8 +- backend/core/character/GF/thief/rogue.py | 8 +- backend/core/equipment/version_0/oath.py | 1 - backend/core/equipment/version_0/register.py | 7 +- backend/core/open/helper.py | 11 +- backend/core/util/host.py | 26 +- backend/database/__init__.py | 2 +- backend/database/connect.py | 22 +- backend/database/models.py | 3 +- backend/openapi | 2 +- backend/pyproject.toml | 5 + backend/ruff.toml | 10 +- backend/uv.lock | 35 +- frontendNew/src/api/info/index.ts | 10 +- .../Dcalc/Character/CalcDetail/index.vue | 2 +- .../Character/EquipmentDictionary/Index.vue | 115 ++++- .../src/pages/Dcalc/Character/Forge/index.vue | 477 ++++++++++++++++-- .../src/pages/Dcalc/Character/Index.vue | 232 ++++++++- .../pages/Dcalc/Character/Result/Index.vue | 253 +++------- .../src/pages/Dcalc/Character/Skill/index.vue | 107 +++- .../src/pages/Dcalc/DnfHelper/list.vue | 11 +- frontendNew/src/pages/Dcalc/Home/Index.vue | 4 +- 38 files changed, 1304 insertions(+), 532 deletions(-) create mode 100644 backend/api/utils/skill_detail.py diff --git a/backend/api/core/Auth.py b/backend/api/core/Auth.py index fa8d1d73..798d8da3 100644 --- a/backend/api/core/Auth.py +++ b/backend/api/core/Auth.py @@ -1,9 +1,8 @@ import base64 -import hmac import json import time from core.basic.character import Character -from fastapi import Header +from fastapi import Header from api.core.exception import ResponseException @@ -36,7 +35,7 @@ def dict2obj(dictObj): 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)}) token = base64.urlsafe_b64encode(payload.encode('utf-8')).decode('utf-8') # character = createCharacter(alter) @@ -51,7 +50,7 @@ def readToken(token: str, redis) -> AlterState: info = dict2obj(json.loads(redis.get(f'token:{token}'))) except Exception as ex: print(ex) - ResponseException('登录过期或无效Token,请刷新后重试') + raise ResponseException('登录过期或无效Token,请刷新后重试') from ex if info is None: raise ResponseException('登录过期或无效Token,请刷新后重试') return info @@ -67,4 +66,3 @@ def uid(access_uid: str | None = Header(None)): return int(access_uid) except Exception: return -1 - diff --git a/backend/api/core/Redis.py b/backend/api/core/Redis.py index 456f9cb8..698c9bed 100644 --- a/backend/api/core/Redis.py +++ b/backend/api/core/Redis.py @@ -47,4 +47,4 @@ def get_redis_info(redis, key, fun, expire = 0, without_gzip=False): redis.set(key, gzip_zip(json.dumps(info))) if expire > 0: redis.expire(key, expire) - return info \ No newline at end of file + return info diff --git a/backend/api/main.py b/backend/api/main.py index bb2b1432..f7dd6c54 100644 --- a/backend/api/main.py +++ b/backend/api/main.py @@ -6,7 +6,7 @@ from api.routes import openapi from api.routes import colg api_router = APIRouter() -api_router.include_router(info.router,prefix="/api",tags=["info"]) -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(colg.router,prefix="/api/open",tags=["colg"]) +api_router.include_router(info.router, prefix="/api", tags=["info"]) +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(colg.router, prefix="/api/open", tags=["colg"]) diff --git a/backend/api/routes/calc.py b/backend/api/routes/calc.py index 2aff9706..edf6b435 100644 --- a/backend/api/routes/calc.py +++ b/backend/api/routes/calc.py @@ -1,17 +1,15 @@ -from api.core.Auth import createToken from api.core.Gzip import register_gzip_request -import json -from fastapi import APIRouter, Body,Request -from api.core.Response import Return, response -from api.core.Redis import get_redis_info -from api.dp import RedisDep,AltersDep +from fastapi import APIRouter, Body +from api.core.Response import response +from api.dp import AltersDep from core.basic.character import createCharacter router = APIRouter() register_gzip_request(router) + @router.post('/calc') -async def calc(request: Request,state:AltersDep,config=Body(None)): - character = createCharacter(state.alter, state.equVersion) - return response(data=character.calc(config)) +async def calc(state: AltersDep, config=Body(None)): + character = createCharacter(state.alter, state.equVersion) + return response(data=character.calc(config)) diff --git a/backend/api/routes/colg.py b/backend/api/routes/colg.py index f11c5a83..e62359ef 100644 --- a/backend/api/routes/colg.py +++ b/backend/api/routes/colg.py @@ -65,7 +65,7 @@ async def get_real_time_gold(redis: RedisDep) -> Response[list[RealTimeGold]]: resp = response.json().get('data', {}) res.append({'updataTime': get_date_str(resp['x'][-1]), 'region': region_name, 'goldPrice': resp['y'][-1]}) return res - except Exception as e: + except Exception: # print(f"File not found: {e}") return response(data=[]) # 设置半小时过期一次 diff --git a/backend/api/routes/info.py b/backend/api/routes/info.py index 8535fb1c..c7a5d8dc 100644 --- a/backend/api/routes/info.py +++ b/backend/api/routes/info.py @@ -1,11 +1,8 @@ -import asyncio -import re -from api.core.Auth import createToken from api.core.Gzip import register_gzip_request import json -from fastapi import APIRouter, Request, Depends, Body -from typing import Annotated -from api.core.Response import Return, response +from fastapi import APIRouter, Body, Request +from api.core.Auth import createToken +from api.core.Response import response from api.core.Redis import get_redis_info, get_redis_info_async from api.dp import RedisDep, AltersDep 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.skillTree import get_skill_tree_info from api.core.exception import ResponseException +from api.utils.skill_detail import normalize_skill_detail, skill_detail_for_level router = APIRouter() register_gzip_request(router) -def replace_placeholders(template): - # 匹配 、、、 等 - 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') async def get_adventure_info(request: Request, redis: RedisDep, name: str = None): 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 with open(f'./openapi/data/{jobId}/{jobGrowId}/cn/skillDetail/{skillId}.json', encoding='utf-8') as 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: - skill_data['levelInfo']['optionDesc'] = replace_placeholders(skill_data['levelInfo']['optionDesc']) - return skill_data + return normalize_skill_detail(skill_data) skill_info = get_redis_info(redis, key, get_skill_info) - if level is not None: - 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'] + skill_info = skill_detail_for_level(skill_info, level) except FileNotFoundError: return response(code=404, message=f'技能信息未找到: {jobId} {jobGrowId} {skillId}', data=None) return response(data=skill_info) diff --git a/backend/api/routes/openapi.py b/backend/api/routes/openapi.py index a219e1ba..df9b2d95 100644 --- a/backend/api/routes/openapi.py +++ b/backend/api/routes/openapi.py @@ -4,6 +4,7 @@ from fastapi import APIRouter, Path, Query from api.core.Response import response from api.core.Redis import get_redis_info from api.dp import RedisDep +from api.utils.skill_detail import normalize_skill_detail, skill_detail_for_level import re from typing import Annotated @@ -23,19 +24,6 @@ router = APIRouter() register_gzip_request(router) -def replace_placeholders(template): - # 匹配 、、、 等 - 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) async def get_skills_list( redis: RedisDep, @@ -151,27 +139,10 @@ async def get_cn_skill_info( encoding='utf-8', ) as 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: - skill_data['levelInfo']['optionDesc'] = replace_placeholders(skill_data['levelInfo']['optionDesc']) - return skill_data + return normalize_skill_detail(skill_data) skill_info = get_redis_info(redis, key, get_skill_info) - if level is not None: - 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'] + skill_info = skill_detail_for_level(skill_info, level) except FileNotFoundError: return response( code=404, diff --git a/backend/api/types/colg.py b/backend/api/types/colg.py index 508e8540..dd2c14df 100644 --- a/backend/api/types/colg.py +++ b/backend/api/types/colg.py @@ -1,7 +1,4 @@ -from enum import Enum from pydantic import BaseModel, Field -from typing import Generic, TypeVar -from api.core.Response import Return as Response class RealTimeGold(BaseModel): diff --git a/backend/api/utils/skill_detail.py b/backend/api/utils/skill_detail.py new file mode 100644 index 00000000..9e16c1ca --- /dev/null +++ b/backend/api/utils/skill_detail.py @@ -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 diff --git a/backend/core/abstract/character.py b/backend/core/abstract/character.py index 040a1c36..79287743 100644 --- a/backend/core/abstract/character.py +++ b/backend/core/abstract/character.py @@ -1,5 +1,6 @@ from abc import ABC, abstractmethod -from typing import Any, Literal, Mapping +from collections.abc import Mapping +from typing import Any, Literal class CharacterProperty(ABC): @@ -202,17 +203,17 @@ class CharacterProperty(ABC): pass @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 @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""" pass @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恢复""" pass diff --git a/backend/core/basic/character/base.py b/backend/core/basic/character/base.py index 1a329239..79b43763 100644 --- a/backend/core/basic/character/base.py +++ b/backend/core/basic/character/base.py @@ -1,5 +1,4 @@ from collections.abc import Mapping -import sys from typing import Any, Literal from core.abstract.character import CharacterProperty @@ -15,8 +14,11 @@ class CharacterBase(CharacterProperty): BindAwake: int """绑定觉醒等级""" - skills: list['Skill'] = [] - skills_dict: dict[str, 'Skill'] = {} + skills: list['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' """装备版本""" charEquipInfo: dict[str, 'CharacterEquipInfo'] @@ -33,9 +35,9 @@ class CharacterBase(CharacterProperty): """誓约套装信息""" oathSkillInfo: list['OathSuitSkill'] """誓约技能信息""" - equ_effect: list['EquEffect'] = [] + equ_effect: list['EquEffect'] """装备效果列表""" - equ_options: dict[str, int] = {} + equ_options: dict[str, int] """装备选项列表""" EquEffectRatio = 1.0 """装备效果倍率""" @@ -148,6 +150,9 @@ class CharacterBase(CharacterProperty): self.max_point = 0 self.skills = [] self.skills_dict = {} + self.skills_id_dict = {} + self.skills_uuid_dict = {} + self.skill_names_cache = {} self.load_skills() # region 角色属性设置 @@ -285,7 +290,7 @@ class CharacterBase(CharacterProperty): value = int(value * (1 + self.jade_effect.ElementIncrease)) 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 主动 @@ -296,13 +301,13 @@ class CharacterBase(CharacterProperty): if (skillType == 'all' or skill.type == skillType) and skill.lv > 0 and skill.name not in exceptSkills: 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""" for skill in self.skills: if min <= skill.learnLv <= max and (skill.type == 'active' or skill.damage) and skill.learnLv not in exclude: 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恢复""" for skill in self.skills: 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': """通过技能ID获取技能""" - skill = list(filter(lambda x: str(x.id) == id, self.skills)) - return skill[0] if len(skill) > 0 else None + return self.skills_id_dict.get(str(id), None) def GetSkillByUUID(self, uuid: str) -> 'Skill': """通过技能UUID获取技能""" - skill = list(filter(lambda x: str(x.uuid) == uuid, self.skills)) - return skill[0] if len(skill) > 0 else None + return self.skills_uuid_dict.get(str(uuid), None) 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]: """获取武器类型""" diff --git a/backend/core/basic/character/calc_equip.py b/backend/core/basic/character/calc_equip.py index 4c0764dc..850277d8 100644 --- a/backend/core/basic/character/calc_equip.py +++ b/backend/core/basic/character/calc_equip.py @@ -1,4 +1,5 @@ import sys +from functools import cache from uuid import uuid1 from core.basic.equipment import OathSuit, Suit, get_equipment @@ -7,6 +8,16 @@ from ..roleinfo import CharacterEmblemInfo, CharacterEquipInfo, CharacterOathInf 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): """装备计算、角色初始化相关方法""" @@ -22,11 +33,12 @@ class CalcEquipMixin(CharacterBase): info['emblems'] = {} if info.get('oaths', None) is None: info['oaths'] = {} + equs = get_equipment(self.equVersion) 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']: # 导入誓约打造信息、誓约信息 - self.charOathInfo.append(CharacterOathInfo(info['oaths'][key], self.equVersion, key)) + self.charOathInfo.append(CharacterOathInfo(info['oaths'][key], self.equVersion, key, equs)) try: # 异常处理 彩色等级自动取最小值 minEmblemLv = min([info['emblems'][key]['lv'] for key in ['红色', '蓝色', '绿色', '黄色']], default=1) @@ -36,7 +48,7 @@ class CalcEquipMixin(CharacterBase): info['emblems'][key]['part'] = key self.charEmblemInfo.append(CharacterEmblemInfo(info['emblems'][key])) pass - except Exception as e: + except Exception: pass self.charOathSkillId = info.get('oathSkill', 1) @@ -59,13 +71,12 @@ class CalcEquipMixin(CharacterBase): def load_skills(self) -> None: """加载技能""" - module = sys.modules[self.moduleName] - for name in dir(module): - memberClass = getattr(module, name) - if isinstance(memberClass, type) and memberClass.__name__.startswith('Skill'): - skill = memberClass(char=self) - self.skills_dict[skill.name] = skill - self.skills.append(skill) + for memberClass in get_skill_classes(self.moduleName): + skill = memberClass(char=self) + self.skills_dict[skill.name] = skill + self.skills_id_dict[str(skill.id)] = skill + self.skills_uuid_dict[str(skill.uuid)] = skill + self.skills.append(skill) # endregion @@ -77,27 +88,27 @@ class CalcEquipMixin(CharacterBase): oathSuitInfo = {} correction = 0 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 + if partEqu is None: + continue # partFusion = item.fusionInfo - if partEqu is not None: - for suit in partEqu.suit: - if suitInfo.get(suit, None) is None: - suitInfo[suit] = {'point': 0, 'count': 0} - if partEqu.Point == 0: - suitInfo[suit]['count'] += 1 - else: - suitInfo[suit]['point'] += partEqu.Point + for suit in partEqu.suit: + suit_item = suitInfo.setdefault(suit, {'point': 0, 'count': 0}) + if partEqu.Point == 0: + suit_item['count'] += 1 + else: + suit_item['point'] += partEqu.Point for item in self.charOathInfo: partOath = item.oathInfo - if partOath is not None: - for suit in partOath.suit: - if oathSuitInfo.get(suit, None) is None: - oathSuitInfo[suit] = {'point': 0, 'count': 0} - if partOath.Point == 0: - oathSuitInfo[suit]['count'] += 1 - else: - oathSuitInfo[suit]['point'] += partOath.Point + if partOath is None: + continue + for suit in partOath.suit: + oath_suit_item = oathSuitInfo.setdefault(suit, {'point': 0, 'count': 0}) + if partOath.Point == 0: + oath_suit_item['count'] += 1 + else: + 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_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] @@ -140,17 +151,19 @@ class CalcEquipMixin(CharacterBase): oathSuitList += equs.get_oath_suit_info(suit, oathSuitInfo[suit]['point'], 0) if len(oathSuitList) == 0: continue + oath_suit_by_suit_id = {item.suitId: item for item in oathSuitList} for suit in suitList: i = suit.id 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) - if suit is not None: - filtered_dict = {k: v for k, v in suit.__dict__.items() if k[0].isupper()} - self.SetStatus(**filtered_dict) + oathEffect = oath_suit_by_suit_id.get(suit.suitId, None) + filtered_dict = {k: v for k, v in suit.__dict__.items() if k[0].isupper()} + self.SetStatus(**filtered_dict) if func is not None and oathEffect is None: # print(f'套装 {suit.name} 生效') 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) try: self.oath_max_point = oathSuitInfo.get(max_point_oath_suit, {}).get('point', 0) - except: + except Exception: self.oath_max_point = 0 self.oathSuitInfo = [] self.oathSkillInfo = [] # 当套装点数达到太初时,誓约技能生效 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: - 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) # 誓约套装效果生效 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 # 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())]: - equ = detail[0] + for item in self.charEquipInfo.values(): + equ = item.equInfo if equ is None or equ.itemType == '副武器': continue - enchat = detail[1] + enchat = item.enchant fun = effects.execture(f'equ_{equ.id}') if fun is None: continue @@ -316,20 +329,20 @@ class CalcEquipMixin(CharacterBase): def calc_basic(self): """计算基础属性:防具精通、增幅、强化等""" + char_equip_info = self.charEquipInfo for part in ['上衣', '头肩', '下装', '腰带', '鞋']: - cur = self.charEquipInfo[part] + cur = char_equip_info[part] if cur.equInfo is None: continue equ = cur.equInfo - reinforce = self.charEquipInfo[part].reinforce + reinforce = cur.reinforce if equ is None: continue for key in self.防具精通属性: value = 精通计算(115, equ.rarity, reinforce, equ.itemDetailType, self.防具类型, self.buffer, key) key = get_key_by_value(key) setattr(self, key, getattr(self, key) + value) - for part in self.charEquipInfo.keys(): - cur = self.charEquipInfo[part] + for part, cur in char_equip_info.items(): if cur.equInfo is None: continue if cur.reinforceType == 1: diff --git a/backend/core/basic/equipment.py b/backend/core/basic/equipment.py index 0a535928..4143b3e5 100644 --- a/backend/core/basic/equipment.py +++ b/backend/core/basic/equipment.py @@ -1,18 +1,21 @@ 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 -def parse_to_number_list(info: str, default: list[float] = [0]) -> list[float]: - return default if not info else [float(i) for i in info.split(',')] +def parse_to_number_list(info: str, default: Sequence[float] = (0,)) -> list[float]: + 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]: - return default if not info else [i for i in info.split(',')] +def parse_to_string_list(info: str, default: Sequence[str] = ('',)) -> list[str]: + 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]: - return default if not info else [int(i) for i in info.split(',')] +def parse_to_int_list(info: str, default: Sequence[int] = (0,)) -> list[int]: + return list(default) if not info else [int(i) for i in info.split(',')] class Equ: @@ -89,6 +92,7 @@ class Equipments: self.emblems = [] self.suits: list[Suit] = [] self.suit_dict: dict[str, Suit] = {} + self.suit_id_dict: dict[str, Suit] = {} self.oath_suits: list[OathSuit] = [] self.oath_suit_dict: dict[str, OathSuit] = {} self.init_equs() @@ -99,7 +103,6 @@ class Equipments: self.init_jades() self.init_oaths() self.init_oath_suits() - self.engine.dispose() def init_equs(self): """从数据库中获取所有装备信息""" @@ -166,6 +169,7 @@ class Equipments: suit_obj = Suit(**suit) self.suits.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 @@ -279,6 +283,7 @@ class Equipments: funs = importlib.import_module('core.equipment.version_0') return funs + @cache def get_suit_info(self, suitId: str | int, point: int = 0, count: int = 0) -> list[Suit]: """根据套装点数返回对应适用的套装属性\n 新套装只需要传入suitID和point即可,count默认为0\n @@ -295,6 +300,7 @@ class Equipments: result[suit.count] = suit return list(result.values()) + @cache def get_oath_suit_info(self, suitId: str | int, point: int = 0, count: int = 0) -> list['OathSuit']: """根据套装点数返回对应适用的誓约套装属性\n 新套装只需要传入suitID和point即可,count默认为0\n diff --git a/backend/core/basic/property.py b/backend/core/basic/property.py index be5e323a..30ec5d81 100644 --- a/backend/core/basic/property.py +++ b/backend/core/basic/property.py @@ -39,7 +39,7 @@ class CharacterInfo(enum.Enum): 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")}%' diff --git a/backend/core/basic/roleinfo.py b/backend/core/basic/roleinfo.py index c9252ad8..27f61b6d 100644 --- a/backend/core/basic/roleinfo.py +++ b/backend/core/basic/roleinfo.py @@ -32,12 +32,15 @@ class CharacterEquipInfo: 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['precision'] = info.get('precision', 0) # info['weaponFusion'] = info.get('weaponFusion', None) 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) if equ is not None and (equ.itemType == part or equ.itemDetailType == part or (equ.itemType == '武器' and part == '副武器')): self.equInfo = equ.adapt(self.adaptation) @@ -60,10 +63,13 @@ class CharacterOathInfo: 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.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: self.oathInfo = oath.adapt(self.adaptation) self.adaptation = min(self.adaptation, self.oathInfo.max_adaptation) @@ -79,7 +85,9 @@ class CharacterEmblemInfo: lv: int """星徽等级""" - def __init__(self, info={}) -> None: + def __init__(self, info=None) -> None: + if info is None: + info = {} self.__dict__.update(info) @@ -89,7 +97,9 @@ class CharacterWeaponInfo(CharacterEquipInfo): upgradeDetail: dict """贴膜详情""" - def __init__(self, info={}) -> None: + def __init__(self, info=None) -> None: + if info is None: + info = {} self.__dict__.update(info) diff --git a/backend/core/basic/skill.py b/backend/core/basic/skill.py index 3a0e7ebf..e8d132c9 100644 --- a/backend/core/basic/skill.py +++ b/backend/core/basic/skill.py @@ -1,50 +1,77 @@ -from copy import deepcopy +from copy import copy +from collections.abc import Callable from functools import cache import json import re -from typing import Literal, TYPE_CHECKING +from typing import Any, Literal, TYPE_CHECKING 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 if TYPE_CHECKING: from core.basic.character import Character characterLv = 115 + 5 +OPENAPI_DIR = BACKEND_DIR / 'openapi' + + +def _identity(value: Any) -> Any: + return value @cache -def get_data(prefix: str, key: int | str, func = lambda x: x): - redis = next(get_redis()) +def _get_skill_data_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('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(): - 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) return custom_cn + def get_skill_info(): - # This function should retrieve the skill info based on job and jobGrow - # For now, we return a placeholder dictionary - with open(f'./openapi/data/{prefix}.json', encoding='utf-8') as f: + with (OPENAPI_DIR / 'data' / f'{prefix}.json').open(encoding='utf-8') as f: skill_data = json.load(f) 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: 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] elif key == "vps": data = [ {"name":x["name"],"desc":x["desc"]} for x in skill_data.get("evolution",[])] 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) - except Exception as e: - # print(f"get skill {prefix}/{key} data error",e) + except Exception: + # print(f"get skill {prefix}/{key} data error") 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 - 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) @@ -174,65 +201,76 @@ class Skill: self._lv = value def effect(self, old: int, new: int): + weapon_type = self.char.GetWeaponType()[0] + weapon_options = self.char.武器选项 for assoc in self.associate: type = assoc.get('type', '*skillDamage') skills = assoc.get('skills', '*') data = assoc.get('data', [0] * self.maxLv) ratio = assoc.get('ratio', 100) - weapon = assoc.get('weapon', self.char.武器选项) + weapon = assoc.get('weapon', weapon_options) exceptSkills = assoc.get('exceptSkills', []) 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) continue 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): """effect的前置条件""" 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: - 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 if type.startswith('$*'): 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: - value = (1 + data[new] / ratio) / (1 + data[old] / ratio) - eval(f'self.char.SetStatus({type[2:]}={value})') + value = (1 + self.get_data_value(data, new) / ratio) / (1 + self.get_data_value(data, old) / ratio) + self.char.SetStatus(**{type[2:]: value}) elif type.startswith('$+'): - value = data[new] / ratio - data[old] / ratio - eval(f'self.char.SetStatus({type[2:]}={value})') + value = self.get_data_value(data, new) / ratio - self.get_data_value(data, old) / ratio + self.char.SetStatus(**{type[2:]: value}) else: if skills == '*': skills = self.char.GetSkillNames('all', True) + exceptSkills = set(exceptSkills) for name in skills: if name in exceptSkills: continue skill = self.char.GetSkillByName(name) 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: # print(f"Error applying association {type} for skill {self.name}: {e}") raise e - def update_skill_attribute(self, skill, type, old, new, data, ratio=100, weapon=[]): - if (self.char.GetWeaponType()[0] not in weapon) and not (self.char.GetWeaponType()[0] is None and len(weapon) == len(self.char.武器选项)): + def update_skill_attribute(self, skill, type, old, new, data, ratio=100, weapon=(), weapon_type=None, weapon_options=()): + if (weapon_type not in weapon) and not (weapon_type is None and len(weapon) == len(weapon_options)): return + old_value = self.get_data_value(data, old) + new_value = self.get_data_value(data, new) if type.startswith('*'): 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: - 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('+'): if type[1:] == 'lv' and self.char.setInfo.get('skills', {}).get(str(skill.id),{}).get('lv',0) == 0: return 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('='): - 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): pass @@ -252,8 +290,7 @@ class ActiveSkill(Skill): def __init__(self, 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 keys: + for i in _get_skill_data_suffixes(self.__class__): power = getattr(self, f'power{i}', None) if power is None: setattr(self, f'power{i}', 1) @@ -262,7 +299,12 @@ class ActiveSkill(Skill): setattr(self, f'hit{i}', 0) 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() if mode is not None: basic.currentMode = mode @@ -298,8 +340,7 @@ class ActiveSkill(Skill): def getSkillData(self, lv: int = 0): res = 0 - keys = [key.replace('data', '') for key in dir(self) if key.startswith('data') and not key.startswith('dataplus')] - for i in keys: + for i in _get_skill_damage_suffixes(self.__class__): data = getattr(self, f'data{i}', []) plus = getattr(self, f'plus{i}', 0) hit = getattr(self, f'hit{i}', 0) @@ -309,8 +350,7 @@ class ActiveSkill(Skill): hit = getattr(self, f'hit{i}', 0) power = getattr(self, f'power{i}', 1) res += hit * power * (data[lv] + plus) - keys = [key.replace('dataplus', '') for key in dir(self) if key.startswith('dataplus')] - for i in keys: + for i in _get_skill_dataplus_suffixes(self.__class__): data = getattr(self, f'dataplus{i}', 0) hit = getattr(self, f'hitplus{i}', 1) power = getattr(self, f'powerplus{i}', 1) @@ -397,8 +437,7 @@ class BuffSkill(Skill): def __init__(self, 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 keys: + for i in _get_skill_data_suffixes(self.__class__): power = getattr(self, f'power{i}', None) if power is None: setattr(self, f'power{i}', 1) diff --git a/backend/core/character/GF/archer/hunter.py b/backend/core/character/GF/archer/hunter.py index 8755239a..7a07333c 100644 --- a/backend/core/character/GF/archer/hunter.py +++ b/backend/core/character/GF/archer/hunter.py @@ -1,5 +1,5 @@ # 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 prefix = 'archer/hunter/cn/skillDetail' @@ -12,8 +12,7 @@ class HunterActiveSkill(ActiveSkill): # 标记消耗,大于0为消耗,小于0为生成 def getSkillData(self, lv: int = 0): res = 0 - keys = [key.replace('data', '') for key in dir(self) if key.startswith('data') and not key.startswith('dataplus')] - for i in keys: + for i in _get_skill_damage_suffixes(self.__class__): data = getattr(self, f'data{i}', []) plus = getattr(self, f'plus{i}', 0) hit = getattr(self, f'hit{i}', 0) @@ -24,8 +23,7 @@ class HunterActiveSkill(ActiveSkill): power = getattr(self, f'power{i}', 1) cost = getattr(self, f'cost{i}', 0) 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 keys: + for i in _get_skill_dataplus_suffixes(self.__class__): data = getattr(self, f'dataplus{i}', 0) hit = getattr(self, f'hitplus{i}', 1) power = getattr(self, f'powerplus{i}', 1) diff --git a/backend/core/character/GF/thief/rogue.py b/backend/core/character/GF/thief/rogue.py index 91f24589..a5af3fa7 100644 --- a/backend/core/character/GF/thief/rogue.py +++ b/backend/core/character/GF/thief/rogue.py @@ -1,5 +1,5 @@ # 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 prefix = 'thief/rogue/cn/skillDetail' @@ -14,8 +14,7 @@ class ActiveSkill(ActiveSkill): def getSkillData(self, lv: int = 0): res = 0 - keys = [key.replace('data', '') for key in dir(self) if key.startswith('data') and not key.startswith('dataplus')] - for i in keys: + for i in _get_skill_damage_suffixes(self.__class__): data = getattr(self, f'data{i}', []) plus = getattr(self, f'plus{i}', 0) if len(data) == 0: @@ -25,8 +24,7 @@ class ActiveSkill(ActiveSkill): power = getattr(self, f'power{i}', 1) group = getattr(self, f'group{i}', 'normal') 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 keys: + for i in _get_skill_dataplus_suffixes(self.__class__): data = getattr(self, f'dataplus{i}', 0) hit = getattr(self, f'hitplus{i}', 1) power = getattr(self, f'powerplus{i}', 1) diff --git a/backend/core/equipment/version_0/oath.py b/backend/core/equipment/version_0/oath.py index 4b7db291..6b424b43 100644 --- a/backend/core/equipment/version_0/oath.py +++ b/backend/core/equipment/version_0/oath.py @@ -1,4 +1,3 @@ -from core.basic.equipment import EquEffect from core.abstract.character import CharacterProperty from .register import register diff --git a/backend/core/equipment/version_0/register.py b/backend/core/equipment/version_0/register.py index 0abc294a..43736acc 100644 --- a/backend/core/equipment/version_0/register.py +++ b/backend/core/equipment/version_0/register.py @@ -1,11 +1,10 @@ registry = {} + def register(func): registry[func.__name__] = func return func + def execture(name): - if name in registry: - return registry[name] - else: - return None + return registry.get(name, None) diff --git a/backend/core/open/helper.py b/backend/core/open/helper.py index 87c432cd..8e661ba7 100644 --- a/backend/core/open/helper.py +++ b/backend/core/open/helper.py @@ -117,12 +117,11 @@ def trans_to_dcalc_set(detail, alter: AltersDep): if job != alter.origin: raise ResponseException('未能匹配到当前职业对应的角色信息,请确认后重试') basic = get_equipment('0') - basicAvatars = basic.funs.get_dress_list([]) - equips = {} - avatar = {} - avatarDisplay = {} - emblems = {} - oaths = {} + basicAvatars = basic.funs.get_dress_list([]) + equips = {} + avatar = {} + emblems = {} + oaths = {} for equ in detail.get('equs', []): info = { 'id': '0', diff --git a/backend/core/util/host.py b/backend/core/util/host.py index b409e092..d177d609 100644 --- a/backend/core/util/host.py +++ b/backend/core/util/host.py @@ -1,6 +1,5 @@ -import os -import platform -import re +import platform +import re host_window = r'C:\Windows\System32\drivers\etc\hosts' host_mac = r'/etc/hosts' @@ -14,23 +13,22 @@ def search_host(hostvalue): host_path = host_mac else: host_path = host_window - hostfile = open(host_path, 'r', errors='ignore') - each_line = hostfile.readlines() - hostfile.close() + with open(host_path, errors='ignore') as hostfile: + each_line = hostfile.readlines() findresult = re.findall(hostvalue, ''.join(each_line)) return findresult def write_host(hostvalue): if platform.system().lower() != 'windows': - output = open(host_mac, 'a') - else: - output = open(host_window, 'a') - - for insid in hostvalue: - output.write(insid) - output.write('\n') - output.close() + output = open(host_mac, 'a') + else: + output = open(host_window, 'a') + + with output: + for insid in hostvalue: + output.write(insid) + output.write('\n') def host_init(): diff --git a/backend/database/__init__.py b/backend/database/__init__.py index 95964647..236cc9e2 100644 --- a/backend/database/__init__.py +++ b/backend/database/__init__.py @@ -1,3 +1,3 @@ import redis -pool = redis.ConnectionPool(host='localhost', port=6379, decode_responses=True) \ No newline at end of file +pool = redis.ConnectionPool(host='localhost', port=6379, decode_responses=True) diff --git a/backend/database/connect.py b/backend/database/connect.py index fae3d60e..35a897ec 100644 --- a/backend/database/connect.py +++ b/backend/database/connect.py @@ -1,22 +1,28 @@ -import os -import sys +from functools import cache +from pathlib import Path import redis from sqlalchemy import create_engine from sqlalchemy.orm import Session from config.main import config 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(): - with redis.Redis(connection_pool=RedisPool) as redis_conn: + with get_redis_client() as redis_conn: yield redis_conn + +@cache def get_db_engine(version='0'): - try: - os.chdir(os.path.dirname(sys.argv[0])) - except Exception: - pass - return create_engine(f'sqlite:///{os.path.join("./", "dataFiles", f"dcalc_{version}.db")}') + db_path = BACKEND_DIR / 'dataFiles' / f'dcalc_{version}.db' + return create_engine(f'sqlite:///{db_path}', connect_args={'check_same_thread': False}) + def get_db(): with Session(get_db_engine()) as session: diff --git a/backend/database/models.py b/backend/database/models.py index 6b246922..33224a7a 100644 --- a/backend/database/models.py +++ b/backend/database/models.py @@ -1,8 +1,7 @@ # import os # import sys from sqlalchemy import Column, Integer, Text, Float -from sqlalchemy import create_engine -from sqlalchemy.orm import Session, declarative_base +from sqlalchemy.orm import declarative_base Base = declarative_base() metadata = Base.metadata diff --git a/backend/openapi b/backend/openapi index 0c2c03e9..d85ff080 160000 --- a/backend/openapi +++ b/backend/openapi @@ -1 +1 @@ -Subproject commit 0c2c03e94869e2aea1d7667c9f3e4e73b9657ba3 +Subproject commit d85ff08062e94b7080ea3361311445a65d23901e diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 9a5f2760..5cc61cec 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -12,3 +12,8 @@ dependencies = [ "requests>=2.32.3", "sqlalchemy>=2.0.38", ] + +[dependency-groups] +dev = [ + "ruff>=0.15.15", +] diff --git a/backend/ruff.toml b/backend/ruff.toml index 0a81f2be..84e8dcbe 100644 --- a/backend/ruff.toml +++ b/backend/ruff.toml @@ -1,9 +1,17 @@ # 限制每行最多20个字符 line-length = 320 +extend-exclude = [ + "api/core/zipfile.py", + "core/basic/character_old.py", +] 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] # 使用单引号 quote-style = "single" # 缩进使用两个空格 -indent-style = "space" \ No newline at end of file +indent-style = "space" diff --git a/backend/uv.lock b/backend/uv.lock index 7c2e9390..16b48ad8 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.14'", @@ -544,6 +544,11 @@ dependencies = [ { name = "sqlalchemy" }, ] +[package.dev-dependencies] +dev = [ + { name = "ruff" }, +] + [package.metadata] requires-dist = [ { name = "aiohttp", specifier = ">=3.13.3" }, @@ -554,6 +559,9 @@ requires-dist = [ { name = "sqlalchemy", specifier = ">=2.0.38" }, ] +[package.metadata.requires-dev] +dev = [{ name = "ruff", specifier = ">=0.15.15" }] + [[package]] name = "detect-installer" 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" }, ] +[[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]] name = "secretstorage" version = "3.5.0" diff --git a/frontendNew/src/api/info/index.ts b/frontendNew/src/api/info/index.ts index 444ee00c..c3a0bc8b 100644 --- a/frontendNew/src/api/info/index.ts +++ b/frontendNew/src/api/info/index.ts @@ -1,5 +1,5 @@ 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) => { return { @@ -7,6 +7,10 @@ export default defineRequest((request) => { const res = await request.get("/adventure"); return res.data; }, + async alterByName(name: string) { + const res = await request.get("/adventure", { params: { name } }); + return res.data; + }, async createToken(alter: string, equVersion?: string) { const res = await request.get(`/token/get/${alter}`, { params: { equVersion } }); return res.data; @@ -23,6 +27,10 @@ export default defineRequest((request) => { const res = await request.get(`/dnfhelper/${uid}/`); return res.data; }, + async getConfigFromHelperAlter(uid: string, alter: string) { + const res = await request.get(`/dnfhelper/${uid}/${alter}/`); + return res.data; + }, async getSkillFromCode(code: string) { const res = await request.post(`/skillTree/`, { code }); return res.data; diff --git a/frontendNew/src/pages/Dcalc/Character/CalcDetail/index.vue b/frontendNew/src/pages/Dcalc/Character/CalcDetail/index.vue index 31923322..4b16dded 100644 --- a/frontendNew/src/pages/Dcalc/Character/CalcDetail/index.vue +++ b/frontendNew/src/pages/Dcalc/Character/CalcDetail/index.vue @@ -572,7 +572,7 @@ const leftPanelOpenSections = ["equipped", "stats", "suit", "equipped-list"];
diff --git a/frontendNew/src/pages/Dcalc/Character/EquipmentDictionary/Index.vue b/frontendNew/src/pages/Dcalc/Character/EquipmentDictionary/Index.vue index f07d4cbf..fc62bc77 100644 --- a/frontendNew/src/pages/Dcalc/Character/EquipmentDictionary/Index.vue +++ b/frontendNew/src/pages/Dcalc/Character/EquipmentDictionary/Index.vue @@ -6,12 +6,21 @@ import { ChevronRight, ChevronsLeft, ChevronsRight, + Settings2, Sparkles, } from "lucide-vue-next"; import { useConfigStore, useInfoStore } from "@/stores"; import type { IEquipment, ISuit } from "@/api/info/type"; import { Button } from "@/components/ui/button"; 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 TabsList from "@/components/ui/tabs/TabsList.vue"; import TabsTrigger from "@/components/ui/tabs/TabsTrigger.vue"; @@ -54,7 +63,8 @@ const suitId = ref(""); const oathSuitId = ref(""); const suitRarity = ref(""); const oathSuitRarity = ref(""); -const sidePanelTab = ref("detail"); +const sidePanelTab = ref("equipped"); +const triggerOptionsOpen = ref(false); const equippedPart = ref(""); const equippedOathPart = ref("0"); const previewEquipKey = ref(""); @@ -75,7 +85,8 @@ const panelLayoutVars = computed(() => ({ const iconWrapStyle = computed(() => ({ ...panelLayoutVars.value, - width: "var(--equip-grid-width)", + maxWidth: "var(--equip-grid-width)", + width: "100%", })); const isOathEquipment = (equip?: IEquipment) => @@ -805,25 +816,29 @@ const toggleOathRarity = (value: unknown) => { const next = normalizeRarityValue(value); oathSuitRarity.value = !next || oathSuitRarity.value === next ? "" : next; }; + +const setTriggerOption = (id: number, value: string) => { + configStore.config.options[id.toString()] = Number.parseInt(value, 10); +};