feat: 角色天赋图鉴(支持自定义等级)

This commit is contained in:
bietiaop 2024-09-02 16:22:23 +08:00
parent 4f02e6b2ce
commit 15b14eece4
23 changed files with 937 additions and 124 deletions

2
.gitignore vendored
View file

@ -8,4 +8,4 @@ data/**/*.*
!data/.gitkeep
resources/images/**/*.*
!resources/images/.gitkeep
resources/data/**/*.*

View file

@ -1,3 +1,6 @@
# 1.5
* 新增角色天赋,发送 `%帮助` 查看如何使用
# 1.4.2
* 版本机制

View file

@ -161,6 +161,13 @@ const helpData = [
needSK: false,
commands: ['更新+角色名+攻略[+0~7]'],
},
{
title: '角色天赋图鉴',
desc: '查看角色天赋默认等级为12级核心技等级为F你可以在指令后面加上自定义等级以英文句号点分隔顺序依次为普通攻击、闪避、支援技、特殊技、连携技、核心技其中除核心技等级为0和AF表示外其他等级为116的数字。例如%猫又天赋6.12.11.10.9.F',
needCK: false,
needSK: false,
commands: ['角色名+天赋[+等级]'],
},
],
},
{

View file

@ -1,10 +1,13 @@
import fs from 'fs';
import {
getRoleImage,
getRoleCircleImage,
getSmallSquareAvatar,
getSquareAvatar,
getSuitImage,
getWeaponImage,
getHakushCharacter,
getHakushWeapon,
} from '../../lib/download.js';
import { char } from '../../lib/convert.js';
import { getAllEquipID } from '../../lib/convert/equip.js';
@ -17,30 +20,49 @@ export async function downloadAll() {
const equipSprites = getAllEquipID();
const weaponSprites = getAllWeaponID();
const result = {
char: {
success: 0,
failed: 0,
total: charIDs.length,
images: {
char: {
success: 0,
failed: 0,
total: charIDs.length,
},
charSmallSquare: {
success: 0,
failed: 0,
total: charIDs.length,
},
charCircle: {
success: 0,
failed: 0,
total: charIDs.length,
},
charSquare: {
success: 0,
failed: 0,
total: charIDs.length,
},
equip: {
success: 0,
failed: 0,
total: equipSprites.length,
},
weapon: {
success: 0,
failed: 0,
total: weaponSprites.length,
},
},
charSmallSquare: {
success: 0,
failed: 0,
total: charIDs.length,
},
charSquare: {
success: 0,
failed: 0,
total: charIDs.length,
},
equip: {
success: 0,
failed: 0,
total: equipSprites.length,
},
weapon: {
success: 0,
failed: 0,
total: weaponSprites.length,
hakush: {
char: {
success: 0,
failed: 0,
total: charIDs.length,
},
equip: {
success: 0,
failed: 0,
total: equipSprites.length,
},
},
};
await this.reply(
@ -51,51 +73,75 @@ export async function downloadAll() {
for (const id of charIDs) {
try {
await getSquareAvatar(id);
result.charSquare.success++;
result.images.charSquare.success++;
} catch (error) {
logger.error('getSquareAvatar', id, error);
result.charSquare.failed++;
result.images.charSquare.failed++;
}
try {
await getSmallSquareAvatar(id);
result.charSmallSquare.success++;
result.images.charSmallSquare.success++;
} catch (error) {
logger.error('getSmallSquareAvatar', id, error);
result.charSmallSquare.failed++;
result.images.charSmallSquare.failed++;
}
try {
await getRoleImage(id);
result.char.success++;
result.images.char.success++;
} catch (error) {
logger.error('getRoleImage', id, error);
result.char.failed++;
result.images.char.failed++;
}
try {
await getRoleCircleImage(id);
result.images.charCircle.success++;
} catch (error) {
logger.error('getRoleCircleImage', id, error);
result.images.charCircle.failed++;
}
try {
await getHakushCharacter(id);
result.hakush.char.success++;
} catch (error) {
logger.error('getHakushCharacter', id, error);
result.hakush.char.failed++;
}
}
for (const sprite of equipSprites) {
try {
await getSuitImage(sprite);
result.equip.success++;
result.images.equip.success++;
} catch (error) {
logger.error('getSuitImage', sprite, error);
result.equip.failed++;
result.images.equip.failed++;
}
}
for (const sprite of weaponSprites) {
try {
await getWeaponImage(sprite);
result.weapon.success++;
result.images.weapon.success++;
} catch (error) {
logger.error('getWeaponImage', sprite, error);
result.weapon.failed++;
result.images.weapon.failed++;
}
try {
await getHakushWeapon(sprite);
result.hakush.equip.success++;
} catch (error) {
logger.error('getHakushWeapon', sprite, error);
result.hakush.equip.failed++;
}
}
const messages = [
'资源下载完成(成功的包含先前下载的图片)',
`角色图需下载${charIDs.length}张,成功${result.char.success}张,失败${result.char.failed}`,
`角色头像图需下载${charIDs.length}张,成功${result.charSquare.success}张,失败${result.charSquare.failed}`,
`角色头像图(练度统计)需下载${charIDs.length}张,成功${result.charSmallSquare.success}张,失败${result.charSmallSquare.failed}`,
`驱动盘套装图需下载${equipSprites.length}张,成功${result.equip.success}张,失败${result.equip.failed}`,
`武器图需下载${weaponSprites.length}张,成功${result.weapon.success}张,失败${result.weapon.failed}`,
'资源下载完成(成功的包含先前下载的资源)',
`角色图需下载${charIDs.length}张,成功${result.images.char.success}张,失败${result.images.char.failed}`,
`角色头像图需下载${charIDs.length}张,成功${result.images.charSquare.success}张,失败${result.images.charSquare.failed}`,
`角色圆形图需下载${charIDs.length}张,成功${result.images.charCircle.success}张,失败${result.images.charCircle.failed}`,
`角色头像图(练度统计)需下载${charIDs.length}张,成功${result.images.charSmallSquare.success}张,失败${result.images.charSmallSquare.failed}`,
`驱动盘套装图需下载${equipSprites.length}张,成功${result.images.equip.success}张,失败${result.images.equip.failed}`,
`武器图需下载${weaponSprites.length}张,成功${result.images.weapon.success}张,失败${result.images.weapon.failed}`,
`Hakush角色数据需下载${charIDs.length}个,成功${result.hakush.char.success}张,失败${result.hakush.char.failed}`,
`Hakush驱动盘数据需下载${equipSprites.length}个,成功${result.hakush.equip.success}张,失败${result.hakush.equip.failed}`,
];
await this.reply(messages.join('\n'));
}

94
apps/wiki.js Normal file
View file

@ -0,0 +1,94 @@
import { ZZZPlugin } from '../lib/plugin.js';
import settings from '../lib/settings.js';
import _ from 'lodash';
import { rulePrefix } from '../lib/common.js';
import { getHakushCharacterData, isSkillLevelLegal } from '../lib/hakush.js';
const displays = [
{
key: 'Basic',
name: '普通攻击',
icon: 'basic',
},
{
key: 'Dodge',
name: '闪避',
icon: 'dodge',
},
{
key: 'Assist',
name: '支援技',
icon: 'assist',
},
{
key: 'Special',
name: '特殊技',
icon: 'special',
},
{
key: 'Chain',
name: '连携技',
icon: 'chain',
},
];
export class Abyss extends ZZZPlugin {
constructor() {
super({
name: '[ZZZ-Plugin]wiki',
dsc: 'zzzWiki',
event: 'message',
priority: _.get(settings.getConfig('priority'), 'wiki', 70),
rule: [
{
reg: `${rulePrefix}(.*)天赋(.*)$`,
fnc: 'skills',
},
],
});
}
async skills() {
const reg = new RegExp(`${rulePrefix}(.*)天赋(.*)$`);
const charname = this.e.msg.match(reg)[4];
if (!charname) return false;
const levelsChar = this.e.msg.match(reg)[5];
const [
BasicLevel = 12,
DodgeLevel = 12,
AssistLevel = 12,
SpecialLevel = 12,
ChainLevel = 12,
CoreLevel = 6,
] = levelsChar.split('.').map(x => {
const _x = Number(x.trim());
if (!_.isNaN(_x)) return _x;
if (_.isString(x)) return x.charCodeAt(0) - 64;
return null;
});
if (
!isSkillLevelLegal('BasicLevel', BasicLevel) ||
!isSkillLevelLegal('DodgeLevel', DodgeLevel) ||
!isSkillLevelLegal('AssistLevel', AssistLevel) ||
!isSkillLevelLegal('SpecialLevel', SpecialLevel) ||
!isSkillLevelLegal('ChainLevel', ChainLevel) ||
!isSkillLevelLegal('CoreLevel', CoreLevel)
) {
await this.reply(`${charname}天赋等级参数不合法`);
return false;
}
const charData = await getHakushCharacterData(charname);
charData.Skill.getAllSkillData({
BasicLevel,
DodgeLevel,
AssistLevel,
SpecialLevel,
ChainLevel,
});
charData.Passive.getPassiveData(CoreLevel);
await charData.get_assets();
const finalData = {
charData,
displays,
};
await this.render('skills/index.html', finalData);
}
}

View file

@ -9,7 +9,7 @@ const PartnerId2SpriteId = getMapData('PartnerId2Data');
* @param {string | number} id
* @param {boolean} full 显示全称
* @param {boolean} en 是否为英文
* @returns string | null
* @returns {string | null}
*/
export const IDToCharName = (id, full = true, en = false) => {
const data = PartnerId2SpriteId?.[id];
@ -22,7 +22,7 @@ export const IDToCharName = (id, full = true, en = false) => {
/**
*
* @param {string | number} id
* @returns string | null
* @returns {string | null}
*/
export const IDToCharSprite = id => {
const data = PartnerId2SpriteId?.[id];
@ -32,7 +32,7 @@ export const IDToCharSprite = id => {
/**
* @param {string} name
* @returns number | null
* @returns {number | null}
*/
export const charNameToID = name => {
for (const [id, data] of Object.entries(PartnerId2SpriteId)) {
@ -43,7 +43,7 @@ export const charNameToID = name => {
/**
* @param {string} name
* @returns string | null
* @returns {string | null}
*/
export const charNameToSprite = name => {
for (const [_id, data] of Object.entries(PartnerId2SpriteId)) {
@ -53,8 +53,8 @@ export const charNameToSprite = name => {
};
/**
* @param {string} alias
* @returns string | null
* @param {string} _alias
* @returns {string | null}
*/
export const aliasToName = _alias => {
const alias = settings.getConfig('alias');
@ -67,7 +67,7 @@ export const aliasToName = _alias => {
/**
* @param {string} _alias
* @returns string | null
* @returns {string | null}
*/
export const aliasToSprite = _alias => {
const name = aliasToName(_alias);
@ -76,7 +76,7 @@ export const aliasToSprite = _alias => {
/**
* @param {string} name
* @returns number | null
* @returns {number | null}
*/
export const aliasToID = name => {
const _name = aliasToName(name);
@ -86,7 +86,7 @@ export const aliasToID = name => {
/**
* 获取所有角色ID
* @returns string[]
* @returns {string[]}
*/
export const getAllCharactersID = () => {
return Object.keys(PartnerId2SpriteId);

View file

@ -135,7 +135,7 @@ export const getSuit3DImage = async suitId => {
/**
* 获取Hakush角色数据
* @param {string} charId
* @returns {Promise<string>}
* @returns {Promise<object | null>} 文件内容JSON
*/
export const getHakushCharacter = async charId => {
const filename = `${charId}.json`;

View file

@ -1,5 +1,5 @@
import path from 'path';
import { imageResourcesPath } from '../path.js';
import { imageResourcesPath, dataResourcesPath } from '../path.js';
export const ZZZ_SQUARE_AVATAR_PATH = path.join(
imageResourcesPath,
@ -18,7 +18,7 @@ export const ZZZ_SQUARE_AVATAR_PATH = path.join(
// const ZZZ_GUIDES_PATH = path.join(imageResourcesPath, 'guides');
export const HAKUSH_CHARACTER_DATA_PATH = path.join(
imageResourcesPath,
dataResourcesPath,
'hakush/data/character'
),
HAKUSH_WEAPON_DATA_PATH = path.join(imageResourcesPath, 'hakush/data/weapon');
HAKUSH_WEAPON_DATA_PATH = path.join(dataResourcesPath, 'hakush/data/weapon');

View file

@ -1,4 +1,5 @@
import path from 'path';
import fs from 'fs';
import { checkFile } from './core.js';
import { getResourceRemotePath } from '../assets.js';
import * as MysURL from '../assets/mysurl.js';
@ -10,6 +11,7 @@ import * as LocalURI from './const.js';
* @param {keyof LocalURI} localBase 本地地址
* @param {string} filename 文件名
* @param {keyof MysURL} newBase 新远程地址
* @returns {Promise<string | null>} 保存路径
*/
export const downloadMysImage = async (
base,
@ -38,6 +40,7 @@ export const downloadMysImage = async (
* @param {keyof LocalURI} localBase 本地地址
* @param {string} filename 文件名
* @param {string} replaceFilename 替换文件名(如果资源不存在)
* @returns {Promise<string | null>} 保存路径
*/
export const downloadResourceImage = async (
remoteLabel,
@ -62,6 +65,7 @@ export const downloadResourceImage = async (
* @param {keyof HakushURL} base 远程地址
* @param {keyof LocalURI} localBase 本地地址
* @param {string} filename 文件名
* @returns {Promise<object | null>} 文件内容JSON
*/
export const downloadHakushFile = async (base, localBase, filename = '') => {
base = HakushURL[base];
@ -71,5 +75,17 @@ export const downloadHakushFile = async (base, localBase, filename = '') => {
if (filename) {
url += `/${filename}`;
}
return checkFile(url, finalPath);
const filepath = await checkFile(url, finalPath);
if (filepath) {
// 打开文件
const file = fs.openSync(filepath, 'r');
// 读取文件内容
const content = fs.readFileSync(file);
// 关闭文件
fs.closeSync(file);
// 返回文件内容
return JSON.parse(content.toString());
} else {
return null;
}
};

19
lib/hakush.js Normal file
View file

@ -0,0 +1,19 @@
import { Character } from '../model/hakush/character.js';
import * as convert from './convert.js';
import { getHakushCharacter } from './download.js';
export const getHakushCharacterData = async alias => {
const name = convert.char.aliasToName(alias);
const id = convert.char.charNameToID(name);
if (!id) return null;
const data = await getHakushCharacter(id);
if (!data) return null;
const result = new Character(data);
return result;
};
export const isSkillLevelLegal = (key, level) => {
if (key === 'CoreLevel') {
return !!level && level >= 0 && level <= 6;
}
return !!level && level >= 1 && level <= 12;
};

View file

@ -21,6 +21,8 @@ export const resourcesPath = path.join(pluginPath, 'resources');
export const imageResourcesPath = path.join(resourcesPath, 'images');
export const dataResourcesPath = path.join(resourcesPath, 'data');
export const mapResourcesPath = path.join(resourcesPath, 'map');
// config 路径

View file

@ -1,3 +1,4 @@
import { getSquareAvatar } from '../../lib/download.js';
/**
* @typedef {Object} StatsData
* @property {number} Armor
@ -141,7 +142,7 @@ class Level {
/**
* @typedef {Object} ExtraLevelData
* @property {number} MaxLevel
* @property {Object.<string, Object.<string, string|number|float>>} Extra
* @property {Record<string, Record<string, string|number|float>>} Extra
*/
class ExtraLevel {
@ -154,11 +155,60 @@ class ExtraLevel {
}
}
/**
* @typedef {Object} PartnerInfoData
* @property {string} Birthday
* @property {string} FullName
* @property {string} Gender
* @property {string} IconPath
* @property {string} ImpressionF
* @property {string} ImpressionM
* @property {string} Name
* @property {string} OutlookDesc
* @property {string} ProfileDesc
* @property {string} Race
* @property {string} RoleIcon
* @property {string} Stature
* @property {string[]} UnlockCondition
*/
class PartnerInfo {
/**
* @param {PartnerInfoData} data
*/
constructor(data) {
this.Birthday = data.Birthday;
this.FullName = data.FullName;
this.Gender = data.Gender;
this.IconPath = data.IconPath;
this.ImpressionF = data.ImpressionF;
this.ImpressionM = data.ImpressionM;
this.Name = data.Name;
this.OutlookDesc = data.OutlookDesc;
this.ProfileDesc = data.ProfileDesc;
this.Race = data.Race;
this.RoleIcon = data.RoleIcon;
this.Stature = data.Stature;
this.UnlockCondition = data.UnlockCondition;
}
}
/**
* @typedef {Object} SkillValueData
* @property {number} Main
* @property {number} Growth
* @property {string} Format
* @property {number[]} AttackData
* @property {number} AttributeInfliction
* @property {number} FeverRecovery
* @property {number} FeverRecoveryGrowth
* @property {number} SpConsume
* @property {number} SpRecovery
* @property {number} SpRecoveryGrowth
* @property {number} StunRatio
* @property {number} StunRatioGrowth
* @property {number} DamagePercentage
* @property {number} DamagePercentageGrowth
*/
class SkillValue {
@ -169,6 +219,17 @@ class SkillValue {
this.Main = data.Main;
this.Growth = data.Growth;
this.Format = data.Format;
this.AttackData = data.AttackData;
this.AttributeInfliction = data.AttributeInfliction;
this.DamagePercentage = data.DamagePercentage;
this.DamagePercentageGrowth = data.DamagePercentageGrowth;
this.FeverRecovery = data.FeverRecovery;
this.FeverRecoveryGrowth = data.FeverRecoveryGrowth;
this.SpConsume = data.SpConsume;
this.SpRecovery = data.SpRecovery;
this.SpRecoveryGrowth = data.SpRecoveryGrowth;
this.StunRatio = data.StunRatio;
this.StunRatioGrowth = data.StunRatioGrowth;
}
}
@ -176,7 +237,7 @@ class SkillValue {
* @typedef {Object} SkillParamData
* @property {string} Name
* @property {string} Desc
* @property {Object.<string, SkillValueData>} Param
* @property {Record<string, SkillValueData>} Param
*/
class SkillParam {
@ -186,9 +247,11 @@ class SkillParam {
constructor(data) {
this.Name = data.Name;
this.Desc = data.Desc;
this.Param = {};
for (const [key, value] of Object.entries(data.Param)) {
this.Param[key] = new SkillValue(value);
if (data.Param) {
this.Param = {};
for (const [key, value] of Object.entries(data.Param)) {
this.Param[key] = new SkillValue(value);
}
}
}
}
@ -206,13 +269,27 @@ class SkillDescription {
constructor(data) {
this.Name = data.Name;
this.Desc = data.Desc;
/** @type {string} */
this.description =
'<div class="line">' +
this.Desc.replace(
/<IconMap:Icon_(\w+)>/g,
'<span class="skill-icon $1"></span>'
)
.replace(
/<color=#(\w+?)>(.+?)<\/color>/g,
'<span style="color:#$1"><strong>$2</strong></span>'
)
.split('\n')
.join('</div><div class="line">') +
'</div>';
}
}
/**
* @typedef {Object} SkillDescription2Data
* @property {string} Name
* @property {SkillParamData} Param
* @property {SkillParamData[]} Param
*/
class SkillDescription2 {
@ -221,14 +298,14 @@ class SkillDescription2 {
*/
constructor(data) {
this.Name = data.Name;
this.Param = new SkillParam(data.Param);
this.Param = data.Param.map(param => new SkillParam(param));
}
}
/**
* @typedef {Object} SkillDetailData
* @property {(SkillDescriptionData|SkillDescription2Data)[]} Description
* @property {Object.<string, Object.<string, number>>} Material
* @property {Recordstring, Record<string, number>>} Material
*/
class SkillDetail {
@ -241,15 +318,71 @@ class SkillDetail {
);
this.Material = data.Material;
}
/**
* 获取技能详情数据
* @param {number} level
* @returns {Record<string, string|number>}
*/
getDetailData(level = 12) {
this.level = level;
const rate = [];
for (const desc of this.Description) {
if (desc.Param) {
const itemData = {
rate: [],
details: [],
};
for (const param of desc.Param) {
if (!!param.Param) {
const value = Object.values(param.Param)[0];
let final = value.Main + value.Growth * (level - 1);
if (value.Format === '%') {
final = `${final / 100}%`;
}
itemData['rate'].push({
label: param.Name,
value: final,
});
itemData['details'].push({
A: (value.Main + value.Growth * (level - 1)) / 100,
B: (value.StunRatio + value.StunRatioGrowth * (level - 1)) / 100,
C:
(value.SpRecovery + value.SpRecoveryGrowth * (level - 1)) /
10000,
D:
(value.FeverRecovery +
value.FeverRecoveryGrowth * (level - 1)) /
10000,
E: value.AttributeInfliction / 100,
F: 0,
G: 0,
});
} else {
itemData['rate'].push({
label: param.Name,
value: param.Desc,
});
}
}
rate.push({
name: desc.Name,
data: itemData,
});
}
}
this.rate = rate;
return rate;
}
}
/**
* @typedef {Object} SkillData
* @property {Object.<string, SkillDetailData>} Basic
* @property {Object.<string, SkillDetailData>} Dodge
* @property {Object.<string, SkillDetailData>} Special
* @property {Object.<string, SkillDetailData>} Chain
* @property {Object.<string, SkillDetailData>} Assist
* @property {SkillDetailData} Basic
* @property {SkillDetailData} Dodge
* @property {SkillDetailData} Special
* @property {SkillDetailData>} Chain
* @property {SkillDetailData>} Assist
*/
class Skill {
@ -257,27 +390,43 @@ class Skill {
* @param {SkillData} data
*/
constructor(data) {
this.Basic = {};
this.Dodge = {};
this.Special = {};
this.Chain = {};
this.Assist = {};
this.Basic = new SkillDetail(data.Basic);
this.Dodge = new SkillDetail(data.Dodge);
this.Special = new SkillDetail(data.Special);
this.Chain = new SkillDetail(data.Chain);
this.Assist = new SkillDetail(data.Assist);
}
for (const [key, value] of Object.entries(data.Basic)) {
this.Basic[key] = new SkillDetail(value);
}
for (const [key, value] of Object.entries(data.Dodge)) {
this.Dodge[key] = new SkillDetail(value);
}
for (const [key, value] of Object.entries(data.Special)) {
this.Special[key] = new SkillDetail(value);
}
for (const [key, value] of Object.entries(data.Chain)) {
this.Chain[key] = new SkillDetail(value);
}
for (const [key, value] of Object.entries(data.Assist)) {
this.Assist[key] = new SkillDetail(value);
}
/**
* 获取技能数据
* @param {string} skill
* @param {number} level
* @returns {Record<string, string|number>}
*/
getSkillData(skill, level = 12) {
return this[skill].getDetailData(level);
}
/**
* 获取所有技能数据
* @param {Record<string, number>} levels
* @returns {Record<string, Record<'BasicLevel'|'DodgeLevel'|'AssistLevel'|'SpecialLevel'|'ChainLevel', number>>}
*/
getAllSkillData(levels) {
const {
BasicLevel = 12,
DodgeLevel = 12,
AssistLevel = 12,
SpecialLevel = 12,
ChainLevel = 12,
} = levels;
return {
Basic: this.getSkillData('Basic', BasicLevel),
Dodge: this.getSkillData('Dodge', DodgeLevel),
Assist: this.getSkillData('Assist', AssistLevel),
Special: this.getSkillData('Special', SpecialLevel),
Chain: this.getSkillData('Chain', ChainLevel),
};
}
}
@ -298,13 +447,31 @@ class PassiveLevel {
this.Id = data.Id;
this.Name = data.Name;
this.Desc = data.Desc;
/** @type {string[]} */
this.description = data.Desc.map(
item =>
'<div class="line">' +
item
.replace(
/<IconMap:Icon_(\w+)>/g,
'<span class="skill-icon $1"></span>'
)
.replace(
/<color=#(\w+?)>(.+?)<\/color>/g,
'<span style="color:#$1"><strong>$2</strong></span>'
)
.split('\n')
.join('</div><div class="line">') +
'</div>'
);
}
}
/**
* @typedef {Object} PassiveData
* @property {Object.<number, PassiveLevelData>} Level
* @property {Object.<string, Object.<string, number>>} Materials
* @property {Record<number, PassiveLevelData>} Level
* @property {Record<string, Record<string, number>>} Materials
*/
class Passive {
@ -319,6 +486,13 @@ class Passive {
this.Level[key] = new PassiveLevel(value);
}
}
/** @type {PassiveLevel} */
getPassiveData(level = 1) {
this._level = level;
this.currentLevel = this.Level[level];
return this.currentLevel;
}
}
/**
@ -341,30 +515,6 @@ class TalentLevel {
}
}
/**
* @typedef {Object} TalentData
* @property {TalentLevelData} Heroism
* @property {TalentLevelData} YouthfulArrogance
* @property {TalentLevelData} Insensitive
* @property {TalentLevelData} OriginalAspiration
* @property {TalentLevelData} LongingDistance
* @property {TalentLevelData} Idealism
*/
class Talent {
/**
* @param {TalentData} data
*/
constructor(data) {
this.Heroism = new TalentLevel(data.Heroism);
this.YouthfulArrogance = new TalentLevel(data.YouthfulArrogance);
this.Insensitive = new TalentLevel(data.Insensitive);
this.OriginalAspiration = new TalentLevel(data.OriginalAspiration);
this.LongingDistance = new TalentLevel(data.LongingDistance);
this.Idealism = new TalentLevel(data.Idealism);
}
}
/**
* @typedef {Object} CharacterData
* @property {number} Id
@ -372,21 +522,21 @@ class Talent {
* @property {string} Name
* @property {string} CodeName
* @property {number} Rarity
* @property {Object.<string, string>} WeaponType
* @property {Object.<string, string>} ElementType
* @property {Object.<string, string>} HitType
* @property {Object.<string, string>} Camp
* @property {Record<string, string>} WeaponType
* @property {Record<string, string>} ElementType
* @property {Record<string, string>} HitType
* @property {Record<string, string>} Camp
* @property {number} Gender
* @property {Object} PartnerInfo
* @property {PartnerInfoData} PartnerInfo
* @property {StatsData} Stats
* @property {Object.<('1'|'2'|'3'|'4'|'5'|'6'), LevelData>} Level
* @property {Object.<('1'|'2'|'3'|'4'|'5'|'6'), ExtraLevelData>} ExtraLevel
* @property {Record<('1'|'2'|'3'|'4'|'5'|'6'), LevelData>} Level
* @property {Record<('1'|'2'|'3'|'4'|'5'|'6'), ExtraLevelData>} ExtraLevel
* @property {SkillData} Skill
* @property {PassiveData} Passive
* @property {TalentData} Talent
* @property {Record<('1'|'2'|'3'|'4'|'5'|'6'),TalentLevel>} Talent
*/
class Character {
export class Character {
/**
* @param {CharacterData} data
*/
@ -401,10 +551,11 @@ class Character {
this.HitType = data.HitType;
this.Camp = data.Camp;
this.Gender = data.Gender;
this.PartnerInfo = data.PartnerInfo;
this.PartnerInfo = new PartnerInfo(data.PartnerInfo);
this.Stats = new Stats(data.Stats);
this.Level = {};
this.ExtraLevel = {};
this.Talent = {};
for (const [key, value] of Object.entries(data.Level)) {
this.Level[key] = new Level(value);
@ -414,6 +565,14 @@ class Character {
}
this.Skill = new Skill(data.Skill);
this.Passive = new Passive(data.Passive);
this.Talent = new Talent(data.Talent);
for (const [key, value] of Object.entries(data.Talent)) {
this.Talent[key] = new TalentLevel(value);
}
}
async get_assets() {
const result = await getSquareAvatar(this.Id);
this.square_icon = result;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -165,6 +165,31 @@
background-image: url("../images/prop/IconSupport.png");
}
.skill-icon {
aspect-ratio: 1;
background-size: contain;
background-position: center;
background-repeat: no-repeat;
}
.skill-icon.assist, .skill-icon.Switch, .skill-icon.switch {
background-image: url("../images/skills/assist.webp");
}
.skill-icon.basic, .skill-icon.Normal, .skill-icon.normal {
background-image: url("../images/skills/basic.webp");
}
.skill-icon.chain, .skill-icon.Ultimate, .skill-icon.UltimateReady, .skill-icon.ultimateready {
background-image: url("../images/skills/chain.webp");
}
.skill-icon.core, .skill-icon.Core, .skill-icon.CoreSkill, .skill-icon.coreskill {
background-image: url("../images/skills/core.webp");
}
.skill-icon.dodge, .skill-icon.Evade, .skill-icon.evade {
background-image: url("../images/skills/dodge.webp");
}
.skill-icon.special, .skill-icon.Special, .skill-icon.SpecialReady, .skill-icon.specialready {
background-image: url("../images/skills/special.webp");
}
.special-title {
width: 100%;
background-size: contain;

View file

@ -182,6 +182,46 @@
}
}
.skill-icon {
aspect-ratio: 1;
background-size: contain;
background-position: center;
background-repeat: no-repeat;
&.assist,
&.Switch,
&.switch {
background-image: url('../images/skills/assist.webp');
}
&.basic,
&.Normal,
&.normal {
background-image: url('../images/skills/basic.webp');
}
&.chain,
&.Ultimate,
&.UltimateReady,
&.ultimateready {
background-image: url('../images/skills/chain.webp');
}
&.core,
&.Core,
&.CoreSkill,
&.coreskill {
background-image: url('../images/skills/core.webp');
}
&.dodge,
&.Evade,
&.evade {
background-image: url('../images/skills/dodge.webp');
}
&.special,
&.Special,
&.SpecialReady,
&.specialready {
background-image: url('../images/skills/special.webp');
}
}
.special-title {
width: 100%;
background-size: contain;

140
resources/skills/index.css Normal file
View file

@ -0,0 +1,140 @@
.char-info {
display: flex;
align-items: flex-start;
padding: 1em;
gap: 1em;
}
.char-info .avatar {
width: 5em;
aspect-ratio: 1;
flex-grow: 0;
flex-shrink: 0;
background-color: white;
border-radius: 50%;
overflow: hidden;
}
.char-info .avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.char-info .info {
display: flex;
flex-direction: column;
gap: 0.5em;
}
.char-info .info .name {
display: flex;
align-items: flex-end;
gap: 0.5em;
}
.char-info .info .name .simple {
font-size: 1.5em;
}
.char-info .info .description {
font-size: 0.8em;
}
.skills {
display: flex;
flex-direction: column;
gap: 0.5em;
margin: 0.5em;
}
.skills .skill {
background-color: rgba(0, 0, 0, 0.3);
padding: 0.5em;
border-radius: 0.5em;
backdrop-filter: blur(5px);
}
.skills .skill .description {
display: flex;
gap: 0.5em;
}
.skills .skill .description .icon {
width: 4em;
aspect-ratio: 1;
flex-grow: 0;
flex-shrink: 0;
}
.skills .skill .description .icon .skill-icon {
width: 100%;
}
.skills .skill .description .info .name {
font-size: 1.4em;
color: rgb(246, 202, 69);
margin: 0.5em 0;
}
.skills .skill .description .info .item {
margin-bottom: 0.3em;
}
.skills .skill .description .info .item .title {
font-size: 1.2em;
}
.skills .skill .description .info .item .content .line .skill-icon {
width: 1.2em;
margin-bottom: -0.2em;
display: inline-block;
}
.skills .skill .detail .title {
font-size: 1.1em;
color: rgb(246, 202, 69);
margin: 0.5em 0;
}
.skills .skill .detail .title .level {
font-size: 0.8em;
color: white;
background-color: rgb(246, 202, 69);
padding: 0.1em 0.3em;
border-radius: 0.3em;
margin-left: 0.5em;
text-shadow: 0 0 0.1em rgba(0, 0, 0, 0.4);
}
.skills .skill .detail .item .rate {
display: grid;
text-align: center;
}
.skills .skill .detail .item .rate .tb-tr {
display: grid;
grid-template-columns: 1fr 1fr;
border-bottom: 0.1em solid rgba(255, 255, 255, 0.2);
}
.skills .skill .detail .item .rate .tb-tr:first-child {
border-top: 0.1em solid rgba(255, 255, 255, 0.2);
}
.skills .skill .detail .item .rate .tb-tr:nth-child(odd) {
background-color: rgba(255, 255, 255, 0.1);
}
.skills .skill .detail .item .rate .tb-td {
padding: 0.2em 0.5em;
font-size: 0.9em;
border-right: 0.1em solid rgba(255, 255, 255, 0.2);
}
.skills .skill .detail .item .rate .tb-td:last-child {
border-right: none;
}
.skills .skill .detail .item .detail {
display: grid;
text-align: center;
}
.skills .skill .detail .item .detail .tb-tr {
display: grid;
grid-template-columns: repeat(8, 1fr);
border-bottom: 0.1em solid rgba(255, 255, 255, 0.2);
}
.skills .skill .detail .item .detail .tb-tr:first-child {
border-top: 0.1em solid rgba(255, 255, 255, 0.2);
}
.skills .skill .detail .item .detail .tb-tr:nth-child(odd) {
background-color: rgba(255, 255, 255, 0.1);
}
.skills .skill .detail .item .detail .tb-td {
padding: 0.2em 0.5em;
font-size: 0.9em;
border-right: 0.1em solid rgba(255, 255, 255, 0.2);
}
.skills .skill .detail .item .detail .tb-td:last-child {
border-right: none;
}
/*# sourceMappingURL=index.css.map */

112
resources/skills/index.html Normal file
View file

@ -0,0 +1,112 @@
{{extend defaultLayout}}
{{block 'css'}}
<link rel="stylesheet" href="{{@sys.currentPath}}/index.css">
{{/block}}
{{block 'main'}}
<div class="char-info">
<div class="avatar">
<img src="{{charData.square_icon}}" alt="Avatar">
</div>
<div class="info">
<div class="name">
<div class="simple">{{charData.PartnerInfo.Name}}</div>
<div class="full">{{charData.PartnerInfo.FullName}}</div>
</div>
<div class="description no-zzz-font">
<div class="f">{{@charData.PartnerInfo.ImpressionF}}</div>
<div class="m">{{@charData.PartnerInfo.ImpressionM}}</div>
</div>
</div>
</div>
<div class="skills">
{{each displays display}}
<div class="skill">
<div class="description">
<div class="icon">
<div class="skill-icon {{display.icon}}"></div>
</div>
<div class="info">
<div class="name">{{display.name}}</div>
{{each charData.Skill[display.key].Description skill}}
{{if !!skill.description}}
<div class="item">
<div class="title">{{skill.Name}}</div>
<div class="content no-zzz-font">{{@skill.description}}</div>
</div>
{{/if}}
{{/each}}
</div>
</div>
<div class="detail">
<div class="title">详细属性<span class="level">Lv.{{charData.Skill[display.key].level}}</span></div>
{{if !!charData.Skill[display.key].rate}}
{{each charData.Skill[display.key].rate rate}}
<div class="item">
<div class="name">{{rate.name}}</div>
<div class="rate no-zzz-font">
{{each rate.data.rate rt}}
<div class="tb-tr">
<div class="tb-td">{{rt.label}}</div>
<div class="tb-td">{{rt.value}}</div>
</div>
{{/each}}
</div>
<div class="detail no-zzz-font">
<div class="tb-tr">
<div class="tb-td">#</div>
<div class="tb-td">A</div>
<div class="tb-td">B</div>
<div class="tb-td">C</div>
<div class="tb-td">D</div>
<div class="tb-td">E</div>
<div class="tb-td">F</div>
<div class="tb-td">G</div>
</div>
{{each rate.data.details detail i}}
<div class="tb-tr">
<div class="tb-td">{{i}}</div>
<div class="tb-td">{{detail.A}}</div>
<div class="tb-td">{{detail.B}}</div>
<div class="tb-td">{{detail.C}}</div>
<div class="tb-td">{{detail.D}}</div>
<div class="tb-td">{{detail.E}}</div>
<div class="tb-td">{{detail.F}}</div>
<div class="tb-td"></div>
</div>
{{/each}}
</div>
</div>
{{/each}}
{{/if}}
</div>
</div>
{{/each}}
{{if !!charData.Passive.currentLevel}}
<div class="skill">
<div class="description">
<div class="icon">
<div class="skill-icon core"></div>
</div>
<div class="info">
<div class="name">核心技</div>
<div class="detail">
<div class="title">详细属性<span class="level">Lv.{{charData.Passive._level}}</span></div>
</div>
{{if !!charData.Passive.currentLevel.description}}
{{each charData.Passive.currentLevel.description skill i}}
<div class="item">
<div class="title">{{charData.Passive.currentLevel.Name[i]}}</div>
<div class="content no-zzz-font">{{@skill}}</div>
</div>
{{/each}}
{{/if}}
</div>
</div>
</div>
{{/if}}
</div>
<div style="text-align: center; font-size: 1em; color: #666; margin: 2em 0;">数据来源于Hakush</div>
{{/block}}

150
resources/skills/index.scss Normal file
View file

@ -0,0 +1,150 @@
.char-info {
display: flex;
align-items: flex-start;
padding: 1em;
gap: 1em;
.avatar {
width: 5em;
aspect-ratio: 1;
flex-grow: 0;
flex-shrink: 0;
background-color: white;
border-radius: 50%;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.info {
display: flex;
flex-direction: column;
gap: 0.5em;
.name {
display: flex;
align-items: flex-end;
gap: 0.5em;
.simple {
font-size: 1.5em;
}
}
.description {
font-size: 0.8em;
}
}
}
.skills {
display: flex;
flex-direction: column;
gap: 0.5em;
margin: 0.5em;
.skill {
background-color: rgba(0, 0, 0, 0.3);
padding: 0.5em;
border-radius: 0.5em;
backdrop-filter: blur(5px);
.description {
display: flex;
gap: 0.5em;
.icon {
width: 4em;
aspect-ratio: 1;
flex-grow: 0;
flex-shrink: 0;
.skill-icon {
width: 100%;
}
}
.info {
.name {
font-size: 1.4em;
color: rgb(246, 202, 69);
margin: 0.5em 0;
}
.item {
margin-bottom: 0.3em;
.title {
font-size: 1.2em;
}
.content {
.line {
.skill-icon {
width: 1.2em;
margin-bottom: -0.2em;
display: inline-block;
}
}
}
}
}
}
.detail {
.title {
font-size: 1.1em;
color: rgb(246, 202, 69);
margin: 0.5em 0;
.level {
font-size: 0.8em;
color: white;
background-color: rgb(246, 202, 69);
padding: 0.1em 0.3em;
border-radius: 0.3em;
margin-left: 0.5em;
text-shadow: 0 0 0.1em rgba(0, 0, 0, 0.4);
}
}
.item {
.rate {
display: grid;
text-align: center;
.tb-tr {
display: grid;
grid-template-columns: 1fr 1fr;
border-bottom: 0.1em solid rgba(255, 255, 255, 0.2);
&:first-child {
border-top: 0.1em solid rgba(255, 255, 255, 0.2);
}
&:nth-child(odd) {
background-color: rgba(255, 255, 255, 0.1);
}
}
.tb-td {
padding: 0.2em 0.5em;
font-size: 0.9em;
border-right: 0.1em solid rgba(255, 255, 255, 0.2);
&:last-child {
border-right: none;
}
}
}
.detail {
display: grid;
text-align: center;
.tb-tr {
display: grid;
grid-template-columns: repeat(8, 1fr);
border-bottom: 0.1em solid rgba(255, 255, 255, 0.2);
&:first-child {
border-top: 0.1em solid rgba(255, 255, 255, 0.2);
}
&:nth-child(odd) {
background-color: rgba(255, 255, 255, 0.1);
}
}
.tb-td {
padding: 0.2em 0.5em;
font-size: 0.9em;
border-right: 0.1em solid rgba(255, 255, 255, 0.2);
&:last-child {
border-right: none;
}
}
}
}
}
}
}