1 #1
@@ -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
@@ -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"])
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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=[])
|
||||||
# 设置半小时过期一次
|
# 设置半小时过期一次
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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]:
|
||||||
"""获取武器类型"""
|
"""获取武器类型"""
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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")}%'
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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', []):
|
||||||
|
|||||||
@@ -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():
|
||||||
|
|||||||
@@ -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,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
|
||||||
|
|||||||
+1
-1
Submodule backend/openapi updated: 0c2c03e948...d85ff08062
@@ -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",
|
||||||
|
]
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Generated
+34
-1
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user