UPDATE: monster bar
Some checks failed
Gitea Auto Deploy / Deploy-Container (push) Failing after 1m36s

This commit is contained in:
2025-07-25 09:20:39 +07:00
parent 604cf1ceec
commit 487c29def1
133 changed files with 841207 additions and 16695 deletions

3
.gitignore vendored
View File

@@ -1,6 +1,7 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
.history/
/node_modules
/.pnp
.pnp.*
@@ -9,7 +10,7 @@
!.yarn/plugins
!.yarn/releases
!.yarn/versions
test1.json
# testing
/coverage

View File

@@ -4,8 +4,11 @@
"": {
"name": "firefly-tools",
"dependencies": {
"@tanstack/react-query": "^5.83.0",
"axios": "^1.10.0",
"framer-motion": "^12.12.1",
"lodash": "^4.17.21",
"lucide-react": "^0.525.0",
"next": "15.3.2",
"next-intl": "^4.1.0",
"react": "^19.0.0",
@@ -20,6 +23,7 @@
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4",
"@types/jest": "^29.5.14",
"@types/lodash": "^4.17.20",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
@@ -264,6 +268,10 @@
"@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.7", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.7", "@tailwindcss/oxide": "4.1.7", "postcss": "^8.4.41", "tailwindcss": "4.1.7" } }, "sha512-88g3qmNZn7jDgrrcp3ZXEQfp9CVox7xjP1HN2TFKI03CltPVd/c61ydn5qJJL8FYunn0OqBaW5HNUga0kmPVvw=="],
"@tanstack/query-core": ["@tanstack/query-core@5.83.0", "", {}, "sha512-0M8dA+amXUkyz5cVUm/B+zSk3xkQAcuXuz5/Q/LveT4ots2rBpPTZOzd7yJa2Utsf8D2Upl5KyjhHRY+9lB/XA=="],
"@tanstack/react-query": ["@tanstack/react-query@5.83.0", "", { "dependencies": { "@tanstack/query-core": "5.83.0" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-/XGYhZ3foc5H0VM2jLSD/NyBRIOK4q9kfeml4+0x2DlL6xVuAcVEW+hTlTapAmejObg0i3eNqhkr2dT+eciwoQ=="],
"@tybys/wasm-util": ["@tybys/wasm-util@0.9.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw=="],
"@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="],
@@ -280,6 +288,8 @@
"@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="],
"@types/lodash": ["@types/lodash@4.17.20", "", {}, "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA=="],
"@types/node": ["@types/node@20.17.50", "", { "dependencies": { "undici-types": "~6.19.2" } }, "sha512-Mxiq0ULv/zo1OzOhwPqOA13I81CV/W3nvd3ChtQZRT5Cwz3cr0FKo/wMSsbTqL3EXpaBAEQhva2B8ByRkOIh9A=="],
"@types/parse-json": ["@types/parse-json@4.0.2", "", {}, "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="],
@@ -832,6 +842,8 @@
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
"lucide-react": ["lucide-react@0.525.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Tm1txJ2OkymCGkvwoHt33Y2JpN5xucVq1slHcgE6Lk0WjDfjgKWor5CdVER8U6DvcfMwh4M8XxmpTiyzfmfDYQ=="],
"magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],

19909
data/as.cn.json Normal file

File diff suppressed because it is too large Load Diff

19909
data/as.en.json Normal file

File diff suppressed because it is too large Load Diff

19909
data/as.jp.json Normal file

File diff suppressed because it is too large Load Diff

19909
data/as.kr.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -21622,6 +21622,253 @@
}
]
},
"22005": {
"Name": "永远的迷境饭",
"Desc": "轰隆隆——\\n迷境之外风雨无休无止。\\n咕噜噜——\\n妖精们的神奇大煮锅火焰不熄。\\n\\n<i>「撒上特制香料……」</i>\\n<i>「放入新鲜的蔬菜!」</i>\\n<i>「这一次,一定能研发成功!」</i>\\n\\n大煮锅又咕噜噜了起来妖精们眼巴巴地望着。\\n<i>「要来了要来了!」</i>\\n一束强烈的亮光自锅底生出——\\n\\n看大家都围了过来\\n\\n……\\n\\n在那香气四溢的宴会上来客举杯共祝奇妙的岁月——\\n\\n愿这梦中的乐土永远温暖永远欢乐",
"Rarity": "CombatPowerLightconeRarity4",
"BaseType": "Shaman",
"Refinements": {
"Name": "真香",
"Desc": "使装备者的攻击力提高<color=#f29e38ff><unbreak>#1[i]%</unbreak></color>。装备者施放战技后,攻击力提高<color=#f29e38ff><unbreak>#2[i]%</unbreak></color>,该效果最多叠加<unbreak>#3[i]</unbreak>层。",
"Level": {
"1": {
"ParamList": [
0.1600000001490116,
0.0800000000745058,
3
]
},
"2": {
"ParamList": [
0.20000000018626451,
0.10000000009313226,
3
]
},
"3": {
"ParamList": [
0.24000000022351742,
0.12000000011175871,
3
]
},
"4": {
"ParamList": [
0.2800000002607703,
0.14000000013038516,
3
]
},
"5": {
"ParamList": [
0.3200000002980232,
0.1600000001490116,
3
]
}
}
},
"Stats": [
{
"EquipmentID": 22005,
"PromotionCostList": [
{
"$type": "ItemConfigRow",
"ItemID": 2,
"ItemNum": 4000,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 115001,
"ItemNum": 5,
"Rarity": "NotNormal"
}
],
"PlayerLevelRequire": 15,
"MaxLevel": 20,
"BaseHP": 43.200000000186265,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 21.600000000558794,
"BaseAttackAdd": 3.2400000002235174,
"BaseDefence": 15,
"BaseDefenceAdd": 2.2500000002328306
},
{
"EquipmentID": 22005,
"Promotion": 1,
"PromotionCostList": [
{
"$type": "ItemConfigRow",
"ItemID": 2,
"ItemNum": 8000,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 110231,
"ItemNum": 3,
"Rarity": "NotNormal"
},
{
"$type": "ItemConfigRow",
"ItemID": 115001,
"ItemNum": 10,
"Rarity": "NotNormal"
}
],
"WorldLevelRequire": 1,
"MaxLevel": 30,
"BaseHP": 95.04000000003725,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 47.52000000048429,
"BaseAttackAdd": 3.2400000002235174,
"BaseDefence": 33,
"BaseDefenceAdd": 2.2500000002328306
},
{
"EquipmentID": 22005,
"Promotion": 2,
"PromotionCostList": [
{
"$type": "ItemConfigRow",
"ItemID": 2,
"ItemNum": 16000,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 110232,
"ItemNum": 3,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 115002,
"ItemNum": 6,
"Rarity": "Rare"
}
],
"WorldLevelRequire": 2,
"MaxLevel": 40,
"BaseHP": 164.160000000149,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 82.0800000000745,
"BaseAttackAdd": 3.2400000002235174,
"BaseDefence": 57,
"BaseDefenceAdd": 2.2500000002328306
},
{
"EquipmentID": 22005,
"Promotion": 3,
"PromotionCostList": [
{
"$type": "ItemConfigRow",
"ItemID": 2,
"ItemNum": 40000,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 110232,
"ItemNum": 6,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 115002,
"ItemNum": 9,
"Rarity": "Rare"
}
],
"WorldLevelRequire": 3,
"MaxLevel": 50,
"BaseHP": 233.28000000026077,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 116.64000000059605,
"BaseAttackAdd": 3.2400000002235174,
"BaseDefence": 81,
"BaseDefenceAdd": 2.2500000002328306
},
{
"EquipmentID": 22005,
"Promotion": 4,
"PromotionCostList": [
{
"$type": "ItemConfigRow",
"ItemID": 2,
"ItemNum": 80000,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 110233,
"ItemNum": 4,
"Rarity": "VeryRare"
},
{
"$type": "ItemConfigRow",
"ItemID": 115003,
"ItemNum": 5,
"Rarity": "VeryRare"
}
],
"WorldLevelRequire": 4,
"MaxLevel": 60,
"BaseHP": 302.40000000037253,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 151.20000000018626,
"BaseAttackAdd": 3.2400000002235174,
"BaseDefence": 105,
"BaseDefenceAdd": 2.2500000002328306
},
{
"EquipmentID": 22005,
"Promotion": 5,
"PromotionCostList": [
{
"$type": "ItemConfigRow",
"ItemID": 2,
"ItemNum": 160000,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 110233,
"ItemNum": 8,
"Rarity": "VeryRare"
},
{
"$type": "ItemConfigRow",
"ItemID": 115003,
"ItemNum": 7,
"Rarity": "VeryRare"
}
],
"WorldLevelRequire": 5,
"MaxLevel": 70,
"BaseHP": 371.5200000004843,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 185.7600000007078,
"BaseAttackAdd": 3.2400000002235174,
"BaseDefence": 129,
"BaseDefenceAdd": 2.2500000002328306
},
{
"EquipmentID": 22005,
"Promotion": 6,
"PromotionCostList": [],
"WorldLevelRequire": 5,
"MaxLevel": 80,
"BaseHP": 440.64000000059605,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 220.32000000029802,
"BaseAttackAdd": 3.2400000002235174,
"BaseDefence": 153,
"BaseDefenceAdd": 2.2500000002328306
}
]
},
"23000": {
"Name": "银河铁道之夜",
"Desc": "若是担心脚下,只需再度抬头凝望,\\n当繁星温柔地注视心已乘风而上。\\n每一个念想每一声低语\\n都化作生命中的承载再不会失去。",
@@ -33069,7 +33316,7 @@
"BaseType": "Warrior",
"Refinements": {
"Name": "骑士之王",
"Desc": "使装备者的暴击伤害提高<color=#f29e38ff><unbreak>#1[i]%</unbreak></color>。施放终结技时,使装备者的攻击力提高<color=#f29e38ff><unbreak>#6[i]%</unbreak></color>,若装备者的能量上限大于等于<unbreak>#3[i]</unbreak>点,为装备者固定恢复等同于装备者能量上限<unbreak>#5[i]%</unbreak>的能量,并使装备者的攻击力提高<color=#f29e38ff><unbreak>#2[i]%</unbreak></color>,持续<unbreak>#4[i]</unbreak>回合。",
"Desc": "使装备者的暴击伤害提高<color=#f29e38ff><unbreak>#1[i]%</unbreak></color>。施放终结技时,使装备者的攻击力提高<color=#f29e38ff><unbreak>#6[i]%</unbreak></color>,若装备者的能量上限大于等于<unbreak>#3[i]</unbreak>点,为装备者固定恢复等同于装备者能量上限<unbreak>#5[i]%</unbreak>的能量,并再次使装备者的攻击力提高<color=#f29e38ff><unbreak>#2[i]%</unbreak></color>,持续<unbreak>#4[i]</unbreak>回合。",
"Level": {
"1": {
"ParamList": [
@@ -33581,6 +33828,530 @@
}
]
},
"23047": {
"Name": "海洋为何而歌",
"Desc": "浪花奔涌,涛声响彻天际。\\n鱼儿洄游在那空无一物的海旧日的泡沫翻腾那一曲乐声似乎仍在回荡。\\n\\n<i>「尊贵的公主,与我们共飨盛宴吧!」</i>\\n为了回应这深海的呼唤她随那黑潮起舞直至盛大的舞台上只剩她一人。\\n\\n<i>「海洋的剑士,你为何沉默,为何而歌?」</i>\\n行于大地的每一步她忍受痛苦鲜少歌唱只有那血色的锋刃奏出生命的悲鸣。\\n\\n<i>「剑旗爵,随我一同,征服星海!」</i>\\n当她寻到这炙热的灯火她终于执起琴弓以为那就是欢宴的开始。\\n\\n……\\n\\n<i>「海列屈拉…海列屈拉…一曲唱毕,你还要游向哪里?」</i>\\n\\n当虚妄的希望碎裂她总是以一曲孤独的旋律回答人们的诘问却使所有听者心醉魂迷——\\n\\n又或许…无论在人世或深海 真正能听到的,唯有奏者自己。",
"Rarity": "CombatPowerLightconeRarity5",
"BaseType": "Warlock",
"Refinements": {
"Name": "独奏",
"Desc": "使装备者的效果命中提高<color=#f29e38ff><unbreak>#1[i]%</unbreak></color>,当有敌方目标陷入装备者施加的负面效果时,有<unbreak>#2[i]%</unbreak>的基础概率使其陷入【魂迷】状态,持续<unbreak>#3[i]</unbreak>回合同类效果无法叠加。【魂迷】状态下每有1个装备者施加的负面效果受到的持续伤害提高<color=#f29e38ff><unbreak>#4[f1]%</unbreak></color>,该效果最多叠加<unbreak>#5[i]</unbreak>层,受到我方目标攻击时,使攻击者速度提高<color=#f29e38ff><unbreak>#6[f1]%</unbreak></color>,持续<unbreak>#7[i]</unbreak>回合。",
"Level": {
"1": {
"ParamList": [
0.40000000037252903,
0.8000000007450581,
3,
0.05000000004656613,
6,
0.10000000009313226,
3
]
},
"2": {
"ParamList": [
0.45000000041909516,
0.8000000007450581,
3,
0.06250000023283064,
6,
0.12499999976716936,
3
]
},
"3": {
"ParamList": [
0.5000000004656613,
0.8000000007450581,
3,
0.07499999972060323,
6,
0.1500000001396984,
3
]
},
"4": {
"ParamList": [
0.5500000005122274,
0.8000000007450581,
3,
0.08749999990686774,
6,
0.17499999981373549,
3
]
},
"5": {
"ParamList": [
0.6000000005587935,
0.8000000007450581,
3,
0.10000000009313226,
6,
0.20000000018626451,
3
]
}
}
},
"Stats": [
{
"EquipmentID": 23047,
"PromotionCostList": [
{
"$type": "ItemConfigRow",
"ItemID": 2,
"ItemNum": 5000,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 115001,
"ItemNum": 8,
"Rarity": "NotNormal"
}
],
"PlayerLevelRequire": 15,
"MaxLevel": 20,
"BaseHP": 43.200000000186265,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 28.800000000745058,
"BaseAttackAdd": 4.320000000298023,
"BaseDefence": 21,
"BaseDefenceAdd": 3.1500000001396984
},
{
"EquipmentID": 23047,
"Promotion": 1,
"PromotionCostList": [
{
"$type": "ItemConfigRow",
"ItemID": 2,
"ItemNum": 10000,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 110221,
"ItemNum": 4,
"Rarity": "NotNormal"
},
{
"$type": "ItemConfigRow",
"ItemID": 115001,
"ItemNum": 12,
"Rarity": "NotNormal"
}
],
"WorldLevelRequire": 1,
"MaxLevel": 30,
"BaseHP": 95.04000000003725,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 63.360000000335276,
"BaseAttackAdd": 4.320000000298023,
"BaseDefence": 46.200000000186265,
"BaseDefenceAdd": 3.1500000001396984
},
{
"EquipmentID": 23047,
"Promotion": 2,
"PromotionCostList": [
{
"$type": "ItemConfigRow",
"ItemID": 2,
"ItemNum": 20000,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 110222,
"ItemNum": 4,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 115002,
"ItemNum": 8,
"Rarity": "Rare"
}
],
"WorldLevelRequire": 2,
"MaxLevel": 40,
"BaseHP": 164.160000000149,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 109.44000000040978,
"BaseAttackAdd": 4.320000000298023,
"BaseDefence": 79.80000000074506,
"BaseDefenceAdd": 3.1500000001396984
},
{
"EquipmentID": 23047,
"Promotion": 3,
"PromotionCostList": [
{
"$type": "ItemConfigRow",
"ItemID": 2,
"ItemNum": 50000,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 110222,
"ItemNum": 8,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 115002,
"ItemNum": 12,
"Rarity": "Rare"
}
],
"WorldLevelRequire": 3,
"MaxLevel": 50,
"BaseHP": 233.28000000026077,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 155.5200000004843,
"BaseAttackAdd": 4.320000000298023,
"BaseDefence": 113.40000000037253,
"BaseDefenceAdd": 3.1500000001396984
},
{
"EquipmentID": 23047,
"Promotion": 4,
"PromotionCostList": [
{
"$type": "ItemConfigRow",
"ItemID": 2,
"ItemNum": 100000,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 110223,
"ItemNum": 5,
"Rarity": "VeryRare"
},
{
"$type": "ItemConfigRow",
"ItemID": 115003,
"ItemNum": 6,
"Rarity": "VeryRare"
}
],
"WorldLevelRequire": 4,
"MaxLevel": 60,
"BaseHP": 302.40000000037253,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 201.6000000005588,
"BaseAttackAdd": 4.320000000298023,
"BaseDefence": 147,
"BaseDefenceAdd": 3.1500000001396984
},
{
"EquipmentID": 23047,
"Promotion": 5,
"PromotionCostList": [
{
"$type": "ItemConfigRow",
"ItemID": 2,
"ItemNum": 200000,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 110223,
"ItemNum": 10,
"Rarity": "VeryRare"
},
{
"$type": "ItemConfigRow",
"ItemID": 115003,
"ItemNum": 8,
"Rarity": "VeryRare"
}
],
"WorldLevelRequire": 5,
"MaxLevel": 70,
"BaseHP": 371.5200000004843,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 247.6800000006333,
"BaseAttackAdd": 4.320000000298023,
"BaseDefence": 180.6000000005588,
"BaseDefenceAdd": 3.1500000001396984
},
{
"EquipmentID": 23047,
"Promotion": 6,
"PromotionCostList": [],
"WorldLevelRequire": 5,
"MaxLevel": 80,
"BaseHP": 440.64000000059605,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 293.7600000007078,
"BaseAttackAdd": 4.320000000298023,
"BaseDefence": 214.20000000018626,
"BaseDefenceAdd": 3.1500000001396984
}
]
},
"23048": {
"Name": "金血铭刻的时代",
"Desc": "<i>「千年后,历史将如何铭记刻律德菈?」</i>\\n她曾如此被人问道。\\n\\n有人心中她是赫赫威名的「凯撒」。\\n那冰冷残酷的暴君那野心勃勃的「燃冕者」那以血与火烧尽旧时代的女皇……\\n\\n有人反驳她只是一枚可怜的「棋子」。\\n这众叛亲离的君王不过是野心的囚徒在那场与神明对决的棋局中她注定只能满盘皆输。\\n\\n也有人说在那比海更幽暗的内心深处她只是一个名为「刻律德菈」的凡人——\\n她也会为死难的同伴哀悼也会在沉重的使命前犹疑畏惧止步不前……\\n\\n<i>「呵,无需历史将我铭记……」</i>\\n\\n她咽下心头苦涩继续行在那由牺牲铺就的逐火之路——\\n\\n<i>「是吾等燃烧的金血,铭刻了它!」</i>",
"Rarity": "CombatPowerLightconeRarity5",
"BaseType": "Shaman",
"Refinements": {
"Name": "征服",
"Desc": "使装备者的攻击力提高<color=#f29e38ff><unbreak>#1[i]%</unbreak></color>。施放终结技攻击后回复<unbreak>#3[i]</unbreak>个战技点,装备者对我方单体角色施放战技后,使目标造成的战技伤害提高<color=#f29e38ff><unbreak>#4[f1]%</unbreak></color>,持续<unbreak>#5[i]</unbreak>回合。",
"Level": {
"1": {
"ParamList": [
0.6400000005960464,
1,
1,
0.5400000005029142,
3
]
},
"2": {
"ParamList": [
0.8000000007450581,
1,
1,
0.6750000002793968,
3
]
},
"3": {
"ParamList": [
0.9600000008940697,
1,
1,
0.8100000007543713,
3
]
},
"4": {
"ParamList": [
1.1200000001117587,
1,
1,
0.9450000005308539,
3
]
},
"5": {
"ParamList": [
1.2800000002607703,
1,
1,
1.0800000000745058,
3
]
}
}
},
"Stats": [
{
"EquipmentID": 23048,
"PromotionCostList": [
{
"$type": "ItemConfigRow",
"ItemID": 2,
"ItemNum": 5000,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 115011,
"ItemNum": 8,
"Rarity": "NotNormal"
}
],
"PlayerLevelRequire": 15,
"MaxLevel": 20,
"BaseHP": 43.200000000186265,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 28.800000000745058,
"BaseAttackAdd": 4.320000000298023,
"BaseDefence": 21,
"BaseDefenceAdd": 3.1500000001396984
},
{
"EquipmentID": 23048,
"Promotion": 1,
"PromotionCostList": [
{
"$type": "ItemConfigRow",
"ItemID": 2,
"ItemNum": 10000,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 110231,
"ItemNum": 4,
"Rarity": "NotNormal"
},
{
"$type": "ItemConfigRow",
"ItemID": 115011,
"ItemNum": 12,
"Rarity": "NotNormal"
}
],
"WorldLevelRequire": 1,
"MaxLevel": 30,
"BaseHP": 95.04000000003725,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 63.360000000335276,
"BaseAttackAdd": 4.320000000298023,
"BaseDefence": 46.200000000186265,
"BaseDefenceAdd": 3.1500000001396984
},
{
"EquipmentID": 23048,
"Promotion": 2,
"PromotionCostList": [
{
"$type": "ItemConfigRow",
"ItemID": 2,
"ItemNum": 20000,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 110232,
"ItemNum": 4,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 115012,
"ItemNum": 8,
"Rarity": "Rare"
}
],
"WorldLevelRequire": 2,
"MaxLevel": 40,
"BaseHP": 164.160000000149,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 109.44000000040978,
"BaseAttackAdd": 4.320000000298023,
"BaseDefence": 79.80000000074506,
"BaseDefenceAdd": 3.1500000001396984
},
{
"EquipmentID": 23048,
"Promotion": 3,
"PromotionCostList": [
{
"$type": "ItemConfigRow",
"ItemID": 2,
"ItemNum": 50000,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 110232,
"ItemNum": 8,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 115012,
"ItemNum": 12,
"Rarity": "Rare"
}
],
"WorldLevelRequire": 3,
"MaxLevel": 50,
"BaseHP": 233.28000000026077,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 155.5200000004843,
"BaseAttackAdd": 4.320000000298023,
"BaseDefence": 113.40000000037253,
"BaseDefenceAdd": 3.1500000001396984
},
{
"EquipmentID": 23048,
"Promotion": 4,
"PromotionCostList": [
{
"$type": "ItemConfigRow",
"ItemID": 2,
"ItemNum": 100000,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 110233,
"ItemNum": 5,
"Rarity": "VeryRare"
},
{
"$type": "ItemConfigRow",
"ItemID": 115013,
"ItemNum": 6,
"Rarity": "VeryRare"
}
],
"WorldLevelRequire": 4,
"MaxLevel": 60,
"BaseHP": 302.40000000037253,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 201.6000000005588,
"BaseAttackAdd": 4.320000000298023,
"BaseDefence": 147,
"BaseDefenceAdd": 3.1500000001396984
},
{
"EquipmentID": 23048,
"Promotion": 5,
"PromotionCostList": [
{
"$type": "ItemConfigRow",
"ItemID": 2,
"ItemNum": 200000,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 110233,
"ItemNum": 10,
"Rarity": "VeryRare"
},
{
"$type": "ItemConfigRow",
"ItemID": 115013,
"ItemNum": 8,
"Rarity": "VeryRare"
}
],
"WorldLevelRequire": 5,
"MaxLevel": 70,
"BaseHP": 371.5200000004843,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 247.6800000006333,
"BaseAttackAdd": 4.320000000298023,
"BaseDefence": 180.6000000005588,
"BaseDefenceAdd": 3.1500000001396984
},
{
"EquipmentID": 23048,
"Promotion": 6,
"PromotionCostList": [],
"WorldLevelRequire": 5,
"MaxLevel": 80,
"BaseHP": 440.64000000059605,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 293.7600000007078,
"BaseAttackAdd": 4.320000000298023,
"BaseDefence": 214.20000000018626,
"BaseDefenceAdd": 3.1500000001396984
}
]
},
"24000": {
"Name": "记一位星神的陨落",
"Desc": "从一道光开始。\\n祂们坠落陨灭的威胁居高临下。\\n祂们不得不停止自我复制争先恐后拥抱彼此\\n试图用繁殖的权利为代价换取生存的可能。\\n祂们携手从未如此团结\\n——但命途戛然而止\\n祂们趋向真正的死亡。",

View File

@@ -18210,7 +18210,7 @@
"Rarity": "CombatPowerLightconeRarity4",
"BaseType": "Knight",
"Refinements": {
"Name": "甘い夢",
"Name": "{RUBY_B#あま}甘{RUBY_E#}い{RUBY_B#ゆめ}夢{RUBY_E#}",
"Desc": "装備キャラが付与するバリアの耐久値+<color=#f29e38ff><unbreak>#1[i]%</unbreak></color>。味方がバリアを持つ時、与ダメージ+<color=#f29e38ff><unbreak>#2[i]%</unbreak></color>。",
"Level": {
"1": {
@@ -18452,7 +18452,7 @@
"Rarity": "CombatPowerLightconeRarity4",
"BaseType": "Memory",
"Refinements": {
"Name": "る",
"Name": "{RUBY_B#つづ}綴{RUBY_E#}る",
"Desc": "装備キャラの最大HP+<color=#f29e38ff><unbreak>#1[i]%</unbreak></color>。装備キャラの記憶の精霊が攻撃した後、装備キャラおよびその記憶の精霊の治癒量+<color=#f29e38ff><unbreak>#2[i]%</unbreak></color>、<unbreak>#3[i]</unbreak>ターン継続。",
"Level": {
"1": {
@@ -18699,7 +18699,7 @@
"Rarity": "CombatPowerLightconeRarity4",
"BaseType": "Priest",
"Refinements": {
"Name": "れ",
"Name": "{RUBY_B#わか}別{RUBY_E#}れ",
"Desc": "装備キャラの治癒量+<color=#f29e38ff><unbreak>#1[i]%</unbreak></color>。味方の残りHPが<unbreak>#2[i]%</unbreak>以上の場合、与ダメージ+<color=#f29e38ff><unbreak>#3[i]%</unbreak></color>。",
"Level": {
"1": {
@@ -19183,7 +19183,7 @@
"Rarity": "CombatPowerLightconeRarity4",
"BaseType": "Memory",
"Refinements": {
"Name": "一蓮托生",
"Name": "{RUBY_B#いちれんたくしょう}一蓮托生{RUBY_E#}",
"Desc": "装備キャラの会心ダメージ+<color=#f29e38ff><unbreak>#1[i]%</unbreak></color>。さらに装備キャラの記憶の精霊の会心ダメージ+<color=#f29e38ff><unbreak>#2[i]%</unbreak></color>。",
"Level": {
"1": {
@@ -19425,8 +19425,8 @@
"Rarity": "CombatPowerLightconeRarity4",
"BaseType": "Warrior",
"Refinements": {
"Name": "殺し合い",
"Desc": "装備キャラの会心率+<color=#f29e38ff><unbreak>#1[i]%</unbreak></color>。装備キャラによる戦闘スキルダメージと必殺技ダメージ+<color=#f29e38ff><unbreak>#2[i]%</unbreak></color>。",
"Name": "{RUBY_B#ころ}殺{RUBY_E#}し{RUBY_B#あ}合{RUBY_E#}い",
"Desc": "装備キャラの会心率+<color=#f29e38ff><unbreak>#1[i]%</unbreak></color>。装備キャラ戦闘スキルダメージと必殺技ダメージ+<color=#f29e38ff><unbreak>#2[i]%</unbreak></color>。",
"Level": {
"1": {
"ParamList": [
@@ -19667,8 +19667,8 @@
"Rarity": "CombatPowerLightconeRarity4",
"BaseType": "Mage",
"Refinements": {
"Name": "れ",
"Desc": "装備キャラの会心率+<color=#f29e38ff><unbreak>#1[i]%</unbreak></color>。装備キャラによる必殺技ダメージと追加攻撃ダメージ+<color=#f29e38ff><unbreak>#2[i]%</unbreak></color>。",
"Name": "{RUBY_B#あこが}憧{RUBY_E#}れ",
"Desc": "装備キャラの会心率+<color=#f29e38ff><unbreak>#1[i]%</unbreak></color>。装備キャラ必殺技ダメージと追加攻撃ダメージ+<color=#f29e38ff><unbreak>#2[i]%</unbreak></color>。",
"Level": {
"1": {
"ParamList": [
@@ -19905,11 +19905,11 @@
},
"21061": {
"Name": "{RUBY_B#きゅうじつ}休日{RUBY_E#}のバルネア{RUBY_B#だいぼうけん}大冒険{RUBY_E#}",
"Desc": "<i>「ヒアンシーちゃん、一緒に新ちくオープンちたバルネアに行ってみない?」</i>\\n\\n少女はオクヘイマでの診療を終えた後、好奇心旺盛な子供のように次々とピュエロスを試して回った。\\n\\n<i>「フルーツジュースのマッサージ、試ちてみる?」</i>\\n真っ赤に染まった両手を見た少女は、イカルンを抱きしめながら後ずさった――\\n<i>「わたしはやっぱり…飲むほうが好きですね……」</i>\\n\\n<i>「大地獣の背中ふみふみマッサージ、割引中だよ!」</i>\\n大地獣は頭を下げ、その鼻息で少女の髪を揺らした。\\n<i>「ま…また今度にします!」</i>\\n\\n<i>「薬草のピュエロスなので健康に良いですよ。何名様でご利用になられますか?」</i>\\n香ばしい薬草の匂いを嗅ぎながら、少女はゆっくりと中に入ってみた。\\n<i>「あ…すごく健康に良さそうです……」</i>\\n\\n<i>「ドゥ、ドゥドゥ!」</i>\\nイカルンも嬉しそうに泳いでいる。\\n<i>「ここ数日でかなり疲れが溜まってたみたいですね……」</i>\\n\\nこの貴重な休日に、少女はブドウジュースを飲みながら、赤毛の女の子と「ぐっすり眠るコツ」や、「スイーツ作りの秘訣」などを思う存分話し合った。\\n\\n<i>「大変、イ…イカルン!」</i>\\nその時、少女の叫び声がバルネアの静寂を破った。\\n\\n<i>「ドゥ……」\\n「イカルン、しっかり」</i>",
"Desc": "<i>「ヒアンシーちゃん、一緒に新ちくオープンちたバルネアに行ってみない?」</i>\\n\\n少女はオクヘイマでの診療を終えた後、好奇心旺盛な子供のように次々とピュエロスを試して回った。\\n\\n<i>「フルーツジュースのマッサージ、試ちてみる?」</i>\\n真っ赤に染まった両手を見た少女は、イカルンを抱きしめながら後ずさった――\\n<i>「わたしはやっぱり…飲むほうが好きですね……」</i>\\n\\n<i>「大地獣の背中ふみふみマッサージ、割引中だよ!」</i>\\n大地獣は頭を下げ、その鼻息で少女の髪を揺らした。\\n<i>「ま…また今度にします!」</i>\\n\\n<i>「薬草のピュエロスなので健康に良いですよ。何名様でご利用になられますか?」</i>\\n香ばしい薬草の匂いを嗅ぎながら、少女はゆっくりと中に入ってみた。\\n<i>「あ…すごく健康に良さそうです……」</i>\\n\\n<i>「ブル、プー!」</i>\\nイカルンも嬉しそうに泳いでいる。\\n<i>「ここ数日でかなり疲れが溜まってたみたいですね……」</i>\\n\\nこの貴重な休日に、少女はブドウジュースを飲みながら、赤毛の女の子と「ぐっすり眠るコツ」や、「スイーツ作りの秘訣」などを思う存分話し合った。\\n\\n<i>「大変、イ…イカルン!」</i>\\nその時、少女の叫び声がバルネアの静寂を破った。\\n\\n<i>「ブルル……」\\n「イカルン、しっかり」</i>",
"Rarity": "CombatPowerLightconeRarity4",
"BaseType": "Warlock",
"Refinements": {
"Name": "落ち着く",
"Name": "{RUBY_B#お}落{RUBY_E#}ち{RUBY_B#つ}着{RUBY_E#}く",
"Desc": "装備キャラの与ダメージ+<color=#f29e38ff><unbreak>#1[i]%</unbreak></color>。装備キャラが攻撃を行った後、<unbreak>#2[i]%</unbreak>の基礎確率で攻撃を受けた敵を被ダメージアップ状態にする。状態中、敵の受けるダメージ+<color=#f29e38ff><unbreak>#3[i]%</unbreak></color>、<unbreak>#4[i]</unbreak>ターン継続。同系統のスキルは累積できない。",
"Level": {
"1": {
@@ -20161,8 +20161,8 @@
"Rarity": "CombatPowerLightconeRarity4",
"BaseType": "Rogue",
"Refinements": {
"Name": "執着",
"Desc": "装備キャラの会心ダメージ+<color=#f29e38ff><unbreak>#1[i]%</unbreak></color>。装備キャラによる戦闘スキルダメージと追加攻撃ダメージ+<color=#f29e38ff><unbreak>#2[i]%</unbreak></color>。",
"Name": "{RUBY_B#しゅうじゃく}執着{RUBY_E#}",
"Desc": "装備キャラの会心ダメージ+<color=#f29e38ff><unbreak>#1[i]%</unbreak></color>。装備キャラ戦闘スキルダメージと追加攻撃ダメージ+<color=#f29e38ff><unbreak>#2[i]%</unbreak></color>。",
"Level": {
"1": {
"ParamList": [
@@ -21622,6 +21622,253 @@
}
]
},
"22005": {
"Name": "永遠の迷境ごはん",
"Desc": "ゴロゴロ——\\n迷境の外で嵐が荒れ狂っている。\\nグツグツ——\\n妖精たちの不思議な大鍋が音を立てて煮えたぎる。\\n\\n<i>「特製スパイスを振って……」</i>\\n<i>「新鮮な野菜を投入!」</i>\\n<i>「今回こそ、きっと成功するはず!」</i>\\n\\n大鍋が再びグツグツと音を立て、妖精たちは目を輝かせながらそれを見守る。\\n<i>「くるぞ…きたーっ!」</i>\\n大鍋の底から、まばゆい光が溢れ出した——\\n\\nその瞬間、鍋のまわりにみんなが集まってくる\\n\\n……\\n\\nいい匂いが漂う宴の席で、来客たちは杯を掲げ、この不思議なひと時を祝福する——\\n\\n願わくば、この夢の楽園がいつまでも温もりに満ち、いつまでも笑顔で溢れますように",
"Rarity": "CombatPowerLightconeRarity4",
"BaseType": "Shaman",
"Refinements": {
"Name": "いい匂い!",
"Desc": "装備キャラの攻撃力+<color=#f29e38ff><unbreak>#1[i]%</unbreak></color>。装備キャラが戦闘スキルを発動した後、攻撃力+<color=#f29e38ff><unbreak>#2[i]%</unbreak></color>、この効果は最大で<unbreak>#3[i]</unbreak>層累積できる。",
"Level": {
"1": {
"ParamList": [
0.1600000001490116,
0.0800000000745058,
3
]
},
"2": {
"ParamList": [
0.20000000018626451,
0.10000000009313226,
3
]
},
"3": {
"ParamList": [
0.24000000022351742,
0.12000000011175871,
3
]
},
"4": {
"ParamList": [
0.2800000002607703,
0.14000000013038516,
3
]
},
"5": {
"ParamList": [
0.3200000002980232,
0.1600000001490116,
3
]
}
}
},
"Stats": [
{
"EquipmentID": 22005,
"PromotionCostList": [
{
"$type": "ItemConfigRow",
"ItemID": 2,
"ItemNum": 4000,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 115001,
"ItemNum": 5,
"Rarity": "NotNormal"
}
],
"PlayerLevelRequire": 15,
"MaxLevel": 20,
"BaseHP": 43.200000000186265,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 21.600000000558794,
"BaseAttackAdd": 3.2400000002235174,
"BaseDefence": 15,
"BaseDefenceAdd": 2.2500000002328306
},
{
"EquipmentID": 22005,
"Promotion": 1,
"PromotionCostList": [
{
"$type": "ItemConfigRow",
"ItemID": 2,
"ItemNum": 8000,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 110231,
"ItemNum": 3,
"Rarity": "NotNormal"
},
{
"$type": "ItemConfigRow",
"ItemID": 115001,
"ItemNum": 10,
"Rarity": "NotNormal"
}
],
"WorldLevelRequire": 1,
"MaxLevel": 30,
"BaseHP": 95.04000000003725,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 47.52000000048429,
"BaseAttackAdd": 3.2400000002235174,
"BaseDefence": 33,
"BaseDefenceAdd": 2.2500000002328306
},
{
"EquipmentID": 22005,
"Promotion": 2,
"PromotionCostList": [
{
"$type": "ItemConfigRow",
"ItemID": 2,
"ItemNum": 16000,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 110232,
"ItemNum": 3,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 115002,
"ItemNum": 6,
"Rarity": "Rare"
}
],
"WorldLevelRequire": 2,
"MaxLevel": 40,
"BaseHP": 164.160000000149,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 82.0800000000745,
"BaseAttackAdd": 3.2400000002235174,
"BaseDefence": 57,
"BaseDefenceAdd": 2.2500000002328306
},
{
"EquipmentID": 22005,
"Promotion": 3,
"PromotionCostList": [
{
"$type": "ItemConfigRow",
"ItemID": 2,
"ItemNum": 40000,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 110232,
"ItemNum": 6,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 115002,
"ItemNum": 9,
"Rarity": "Rare"
}
],
"WorldLevelRequire": 3,
"MaxLevel": 50,
"BaseHP": 233.28000000026077,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 116.64000000059605,
"BaseAttackAdd": 3.2400000002235174,
"BaseDefence": 81,
"BaseDefenceAdd": 2.2500000002328306
},
{
"EquipmentID": 22005,
"Promotion": 4,
"PromotionCostList": [
{
"$type": "ItemConfigRow",
"ItemID": 2,
"ItemNum": 80000,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 110233,
"ItemNum": 4,
"Rarity": "VeryRare"
},
{
"$type": "ItemConfigRow",
"ItemID": 115003,
"ItemNum": 5,
"Rarity": "VeryRare"
}
],
"WorldLevelRequire": 4,
"MaxLevel": 60,
"BaseHP": 302.40000000037253,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 151.20000000018626,
"BaseAttackAdd": 3.2400000002235174,
"BaseDefence": 105,
"BaseDefenceAdd": 2.2500000002328306
},
{
"EquipmentID": 22005,
"Promotion": 5,
"PromotionCostList": [
{
"$type": "ItemConfigRow",
"ItemID": 2,
"ItemNum": 160000,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 110233,
"ItemNum": 8,
"Rarity": "VeryRare"
},
{
"$type": "ItemConfigRow",
"ItemID": 115003,
"ItemNum": 7,
"Rarity": "VeryRare"
}
],
"WorldLevelRequire": 5,
"MaxLevel": 70,
"BaseHP": 371.5200000004843,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 185.7600000007078,
"BaseAttackAdd": 3.2400000002235174,
"BaseDefence": 129,
"BaseDefenceAdd": 2.2500000002328306
},
{
"EquipmentID": 22005,
"Promotion": 6,
"PromotionCostList": [],
"WorldLevelRequire": 5,
"MaxLevel": 80,
"BaseHP": 440.64000000059605,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 220.32000000029802,
"BaseAttackAdd": 3.2400000002235174,
"BaseDefence": 153,
"BaseDefenceAdd": 2.2500000002328306
}
]
},
"23000": {
"Name": "{RUBY_B#ぎんがてつどう}銀河鉄道{RUBY_E#}の{RUBY_B#よる}夜{RUBY_E#}",
"Desc": "足元が心配なら、もう一度頭を上げて見渡せばいい、\\n星々が優しく注目する時、心は既に風に乗り飛び上がっている。\\n全ての想念、全ての囁きが、\\n命の支えとなり、いつまでも付き添ってくれる。",
@@ -32288,12 +32535,12 @@
},
"23042": {
"Name": "{RUBY_B#そら}空{RUBY_E#}の{RUBY_B#にじ}虹{RUBY_E#}が{RUBY_B#き}消{RUBY_E#}えぬように",
"Desc": "<i>「感情が雲なら、たちはそれを背負う空のようなものです。空が曇っていれば気持ちも不安定になります」</i>\\n医者は訪ねてきた者の苦痛に耳を傾け、癒しの囁きを与える。\\n<i>「目を閉じて、ゆっくりと息を吸って…自分が暖かく柔らかい場所にいる想像をしてください」</i>\\n\\nその言葉には魔力が宿っているようで、翼獣の翼の下に虹色の光が降り注ぐ。\\n<i>「野原からくそよ風が花の香りを運び、漂うしっとりした雲は陽の光に満ち溢れている……」</i>\\n訪問者は徐々に美し夢に包まれ、落ち着いた呼吸音だけが返ってくる。\\n\\n最後の患者を診療した後、医者はそっと病室のドアを閉めた。\\n\\n遠くではまだ果てしない永夜が渦巻き、数え切れないほどの廃墟が傷跡のように大地に残されていく。\\n\\n<i>「来世では、黎明がすべての人々に降り注ぐといいな……」</i>\\n旅立つ前に、彼女は空に向かって願いことをした——\\n<i>「どうか、空に架かる虹が、消えませんように」</i>",
"Desc": "<i>「感情が雲なら、わたしたちはそれを背負う空のようなものです。空が曇っていれば気持ちも不安定になります」</i>\\n医者は訪ねてきた者の苦痛に耳を傾け、癒しの囁きを与える。\\n<i>「目を閉じて、ゆっくりと息を吸って…自分が暖かく柔らかい場所にいる想像をしてください」</i>\\n\\nその言葉には魔力が宿っているようで、翼獣の翼の下に虹色の光が降り注ぐ。\\n<i>「野原からくそよ風が花の香りを運び、漂うしっとりした雲は陽の光に満ち溢れている……」</i>\\n訪問者は徐々に美し夢に包まれ、落ち着いた呼吸音だけが返ってくる。\\n\\n最後の患者を診療した後、医者はそっと病室のドアを閉めた。\\n\\n遠くではまだ果てしない永夜が渦巻き、数え切れないほどの廃墟が傷跡のように大地に残されていく。\\n\\n<i>「来世では、黎明がすべての人々に降り注ぐといいな……」</i>\\n旅立つ前に、彼女は空に向かって願いをした——\\n<i>「どうか、空に架かる虹が、消えませんように」</i>",
"Rarity": "CombatPowerLightconeRarity5",
"BaseType": "Memory",
"Refinements": {
"Name": "{RUBY_B#ほうよう}包容{RUBY_E#}",
"Desc": "装備キャラの速度+<color=#f29e38ff><unbreak>#1[i]%</unbreak></color>。装備キャラが通常攻撃、戦闘スキルまたは必殺技を発動する時、味方それぞれの残りHP<color=#f29e38ff><unbreak>#2[f1]%</unbreak></color>分のHPを消費し、装備キャラの記憶の精霊が次の攻撃を行った後に消費したHP<color=#f29e38ff><unbreak>#6[f1]%</unbreak></color>分の付加ダメージを1回与える。なお、この付加ダメージの属性は装備キャラの記憶の精霊と同じものになる。その後、消費したHPのカウントはクリアされる。装備キャラの記憶の精霊が精霊スキルを発動する時、敵全体の受けるダメージ+<color=#f29e38ff><unbreak>#4[f1]%</unbreak></color>、<unbreak>#5[i]</unbreak>ターン継続。同系統のスキルは累積できない。",
"Desc": "装備キャラの速度+<color=#f29e38ff><unbreak>#1[i]%</unbreak></color>。装備キャラが通常攻撃、戦闘スキルまたは必殺技を発動する時、味方それぞれが自身の残りHP<color=#f29e38ff><unbreak>#2[f1]%</unbreak></color>分のHPを消費し、装備キャラの記憶の精霊が次の攻撃を行った後に消費したHP<color=#f29e38ff><unbreak>#6[f1]%</unbreak></color>分の付加ダメージを1回与える。なお、この付加ダメージの属性は装備キャラの記憶の精霊と同じものになる。その後、消費したHPのカウントはクリアされる。装備キャラの記憶の精霊が精霊スキルを発動する時、敵全体の受けるダメージ+<color=#f29e38ff><unbreak>#4[f1]%</unbreak></color>、<unbreak>#5[i]</unbreak>ターン継続。同系統のスキルは累積できない。",
"Level": {
"1": {
"ParamList": [
@@ -32821,8 +33068,8 @@
"Rarity": "CombatPowerLightconeRarity5",
"BaseType": "Warrior",
"Refinements": {
"Name": "喪失",
"Desc": "装備キャラの基礎速度+<color=#f29e38ff><unbreak>#1[i]</unbreak></color>、ダメージを与え時、敵の防御力を<color=#f29e38ff><unbreak>#2[i]%</unbreak></color>無視する。装備キャラが必殺技を発動した後、「烈日」を獲得する。この効果はターンが回ってきた時に解除される。「烈日」を所持している場合、装備キャラの与ダメージ+<color=#f29e38ff><unbreak>#3[i]%</unbreak></color>。",
"Name": "{RUBY_B#そうしつ}喪失{RUBY_E#}",
"Desc": "装備キャラの基礎速度+<color=#f29e38ff><unbreak>#1[i]</unbreak></color>、ダメージを与え時、敵の防御力を<color=#f29e38ff><unbreak>#2[i]%</unbreak></color>無視する。装備キャラが必殺技を発動した後、「烈日」を獲得する。この効果はターンが回ってきた時に解除される。「烈日」を所持している場合、装備キャラの与ダメージ+<color=#f29e38ff><unbreak>#3[i]%</unbreak></color>。",
"Level": {
"1": {
"ParamList": [
@@ -33068,8 +33315,8 @@
"Rarity": "CombatPowerLightconeRarity5",
"BaseType": "Warrior",
"Refinements": {
"Name": "騎士王",
"Desc": "装備キャラの会心ダメージ+<color=#f29e38ff><unbreak>#1[i]%</unbreak></color>。必殺技を発動した時、装備キャラの攻撃力+<color=#f29e38ff><unbreak>#6[i]%</unbreak></color>。なお、装備キャラの最大EPが<unbreak>#3[i]</unbreak>以上の場合、固定で最大EP<unbreak>#5[i]%</unbreak>分回復し、さらに装備キャラの攻撃力+<color=#f29e38ff><unbreak>#2[i]%</unbreak></color>、<unbreak>#4[i]</unbreak>ターン継続。",
"Name": "{RUBY_B#きしおう}騎士王{RUBY_E#}",
"Desc": "装備キャラの会心ダメージ+<color=#f29e38ff><unbreak>#1[i]%</unbreak></color>。必殺技を発動する時、装備キャラの攻撃力+<color=#f29e38ff><unbreak>#6[i]%</unbreak></color>、<unbreak>#4[i]</unbreak>ターン継続。なお、装備キャラの最大EPが<unbreak>#3[i]</unbreak>以上の場合、自身の最大EP<unbreak>#5[i]%</unbreak>分のEPを回復し、追加で装備キャラの攻撃力+<color=#f29e38ff><unbreak>#2[i]%</unbreak></color>、<unbreak>#4[i]</unbreak>ターン継続。",
"Level": {
"1": {
"ParamList": [
@@ -33330,7 +33577,7 @@
"Rarity": "CombatPowerLightconeRarity5",
"BaseType": "Rogue",
"Refinements": {
"Name": "赤原猟兵",
"Name": "{RUBY_B#フルンディング}赤原猟兵{RUBY_E#}",
"Desc": "装備キャラの会心率+<color=#f29e38ff><unbreak>#1[i]%</unbreak></color>。戦闘に入る時、味方の最大SPが<unbreak>#2[i]</unbreak>以上の場合、装備キャラの攻撃力+<color=#f29e38ff><unbreak>#3[i]%</unbreak></color>。装備キャラが戦闘スキルを発動するたびに、装備キャラの攻撃力+<color=#f29e38ff><unbreak>#4[i]%</unbreak></color>、最大<unbreak>#5[i]</unbreak>層まで累積できる。",
"Level": {
"1": {
@@ -33581,6 +33828,530 @@
}
]
},
"23047": {
"Name": "海の歌は何がため",
"Desc": "ほとばしる波の咆哮が空をも震わせる。\\n魚は何もない海を回遊する。旧き日の泡が渦巻く中、かつての旋律が今もなおこだまし続けているようだ。\\n\\n<i></i><i>「高貴なる姫様、私たちと一緒に宴を楽しみましょう!」</i>\\n深海からの呼び声に応え、彼女は暗黒の潮と共に舞い踊った——やがて、盛大なる舞台に独り取り残されるその時まで。\\n\\n<i>「海洋の剣士よ、あなたは何故沈黙し、その歌は何がためのものなのか?」</i>\\n大地を歩む彼女は、痛みに耐え続け、ほとんど歌うこともなくなった。もし彼女が奏でる音があるとすれば、それは血に染まった刃が響かせる、命の悲鳴だけだろう。\\n\\n<i>「剣旗卿、僕についてこい。共に星海を征服しようではないか!」</i>\\nその熱い灯火を見つけた時、ついに饗宴が始まるのだと確信し、彼女は琴を手に取った。\\n\\n……\\n\\n<i>「ヘレクトラ…ヘレクトラ…曲は終わった。次はどこへ向かうのだ?」</i>\\n\\n偽りの希望が砕け散るたびに、彼女はいつも同じ孤独に満ちた旋律で人々の詰問に答え、聴く者たちを心酔させた——\\n\\nあるいは…人間の世界でも深海でも、本当にその旋律を聞くことができるのは、奏でる者ただ1人だけなのかもしれない。",
"Rarity": "CombatPowerLightconeRarity5",
"BaseType": "Warlock",
"Refinements": {
"Name": "独奏",
"Desc": "装備キャラの効果命中+<color=#f29e38ff><unbreak>#1[i]%</unbreak></color>。敵が装備キャラによりデバフが付与された時、<unbreak>#2[i]%</unbreak>の基礎確率でその敵を「魂迷」状態にする。<unbreak>#3[i]</unbreak>ターン継続。なお、同系統のスキルは累積できない。「魂迷」状態の敵は、装備キャラが付与したデバフ1つにつき、受ける持続ダメージ+<color=#f29e38ff><unbreak>#4[f1]%</unbreak></color>、この効果は最大で<unbreak>#5[i]</unbreak>層累積できる。味方が「魂迷」状態の敵を攻撃した時、その味方の速度+<color=#f29e38ff><unbreak>#6[f1]%</unbreak></color>、<unbreak>#7[i]</unbreak>ターン継続。",
"Level": {
"1": {
"ParamList": [
0.40000000037252903,
0.8000000007450581,
3,
0.05000000004656613,
6,
0.10000000009313226,
3
]
},
"2": {
"ParamList": [
0.45000000041909516,
0.8000000007450581,
3,
0.06250000023283064,
6,
0.12499999976716936,
3
]
},
"3": {
"ParamList": [
0.5000000004656613,
0.8000000007450581,
3,
0.07499999972060323,
6,
0.1500000001396984,
3
]
},
"4": {
"ParamList": [
0.5500000005122274,
0.8000000007450581,
3,
0.08749999990686774,
6,
0.17499999981373549,
3
]
},
"5": {
"ParamList": [
0.6000000005587935,
0.8000000007450581,
3,
0.10000000009313226,
6,
0.20000000018626451,
3
]
}
}
},
"Stats": [
{
"EquipmentID": 23047,
"PromotionCostList": [
{
"$type": "ItemConfigRow",
"ItemID": 2,
"ItemNum": 5000,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 115001,
"ItemNum": 8,
"Rarity": "NotNormal"
}
],
"PlayerLevelRequire": 15,
"MaxLevel": 20,
"BaseHP": 43.200000000186265,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 28.800000000745058,
"BaseAttackAdd": 4.320000000298023,
"BaseDefence": 21,
"BaseDefenceAdd": 3.1500000001396984
},
{
"EquipmentID": 23047,
"Promotion": 1,
"PromotionCostList": [
{
"$type": "ItemConfigRow",
"ItemID": 2,
"ItemNum": 10000,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 110221,
"ItemNum": 4,
"Rarity": "NotNormal"
},
{
"$type": "ItemConfigRow",
"ItemID": 115001,
"ItemNum": 12,
"Rarity": "NotNormal"
}
],
"WorldLevelRequire": 1,
"MaxLevel": 30,
"BaseHP": 95.04000000003725,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 63.360000000335276,
"BaseAttackAdd": 4.320000000298023,
"BaseDefence": 46.200000000186265,
"BaseDefenceAdd": 3.1500000001396984
},
{
"EquipmentID": 23047,
"Promotion": 2,
"PromotionCostList": [
{
"$type": "ItemConfigRow",
"ItemID": 2,
"ItemNum": 20000,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 110222,
"ItemNum": 4,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 115002,
"ItemNum": 8,
"Rarity": "Rare"
}
],
"WorldLevelRequire": 2,
"MaxLevel": 40,
"BaseHP": 164.160000000149,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 109.44000000040978,
"BaseAttackAdd": 4.320000000298023,
"BaseDefence": 79.80000000074506,
"BaseDefenceAdd": 3.1500000001396984
},
{
"EquipmentID": 23047,
"Promotion": 3,
"PromotionCostList": [
{
"$type": "ItemConfigRow",
"ItemID": 2,
"ItemNum": 50000,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 110222,
"ItemNum": 8,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 115002,
"ItemNum": 12,
"Rarity": "Rare"
}
],
"WorldLevelRequire": 3,
"MaxLevel": 50,
"BaseHP": 233.28000000026077,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 155.5200000004843,
"BaseAttackAdd": 4.320000000298023,
"BaseDefence": 113.40000000037253,
"BaseDefenceAdd": 3.1500000001396984
},
{
"EquipmentID": 23047,
"Promotion": 4,
"PromotionCostList": [
{
"$type": "ItemConfigRow",
"ItemID": 2,
"ItemNum": 100000,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 110223,
"ItemNum": 5,
"Rarity": "VeryRare"
},
{
"$type": "ItemConfigRow",
"ItemID": 115003,
"ItemNum": 6,
"Rarity": "VeryRare"
}
],
"WorldLevelRequire": 4,
"MaxLevel": 60,
"BaseHP": 302.40000000037253,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 201.6000000005588,
"BaseAttackAdd": 4.320000000298023,
"BaseDefence": 147,
"BaseDefenceAdd": 3.1500000001396984
},
{
"EquipmentID": 23047,
"Promotion": 5,
"PromotionCostList": [
{
"$type": "ItemConfigRow",
"ItemID": 2,
"ItemNum": 200000,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 110223,
"ItemNum": 10,
"Rarity": "VeryRare"
},
{
"$type": "ItemConfigRow",
"ItemID": 115003,
"ItemNum": 8,
"Rarity": "VeryRare"
}
],
"WorldLevelRequire": 5,
"MaxLevel": 70,
"BaseHP": 371.5200000004843,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 247.6800000006333,
"BaseAttackAdd": 4.320000000298023,
"BaseDefence": 180.6000000005588,
"BaseDefenceAdd": 3.1500000001396984
},
{
"EquipmentID": 23047,
"Promotion": 6,
"PromotionCostList": [],
"WorldLevelRequire": 5,
"MaxLevel": 80,
"BaseHP": 440.64000000059605,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 293.7600000007078,
"BaseAttackAdd": 4.320000000298023,
"BaseDefence": 214.20000000018626,
"BaseDefenceAdd": 3.1500000001396984
}
]
},
"23048": {
"Name": "黄金の血で刻む時代",
"Desc": "<i>「千年後、ケリュドラはどのような姿で歴史に刻まれるのか?」</i>\\n誰かが彼女にそう問いかけた。\\n\\nある者にとって彼女は誰もが知る「カイザー」——\\n冷酷な暴君にして野心に満ちた「炎冠」、血と炎で旧き時代を焼き払った女帝である……\\n\\n一方で、別の者は異を唱える。彼女は哀れな「駒」にすぎないと。\\n裏切られた王は所詮、野望の囚人。神との対局において、敗北は初めから決まっていたのだと。\\n\\nまた、こう語る者もいた。海よりもなお深く暗いその心の奥底では、彼女はただ「ケリュドラ」という名の凡庸な人にすぎないと——\\n犠牲になった仲間を悼み、重すぎる使命の前で怯え、迷い、そして立ちすくむ……\\n\\n<i>「ふっ、歴史に刻まれるだと?」</i>\\n\\n彼女は苦しみを飲み込みながら、仲間たちの犠牲によって敷かれた火を追う旅路を歩み続ける——\\n\\n<i>「逆だ。僕たちの燃える黄金の血が、歴史を刻むのだ!」</i>",
"Rarity": "CombatPowerLightconeRarity5",
"BaseType": "Shaman",
"Refinements": {
"Name": "征服",
"Desc": "装備キャラの攻撃力+<color=#f29e38ff><unbreak>#1[i]%</unbreak></color>。必殺技を発動して攻撃を行った後、SPを<unbreak>#3[i]</unbreak>回復する。装備キャラが味方単体キャラに戦闘スキルを発動した後、その味方の戦闘スキルダメージ+<color=#f29e38ff><unbreak>#4[f1]%</unbreak></color>、<unbreak>#5[i]</unbreak>ターン継続。",
"Level": {
"1": {
"ParamList": [
0.6400000005960464,
1,
1,
0.5400000005029142,
3
]
},
"2": {
"ParamList": [
0.8000000007450581,
1,
1,
0.6750000002793968,
3
]
},
"3": {
"ParamList": [
0.9600000008940697,
1,
1,
0.8100000007543713,
3
]
},
"4": {
"ParamList": [
1.1200000001117587,
1,
1,
0.9450000005308539,
3
]
},
"5": {
"ParamList": [
1.2800000002607703,
1,
1,
1.0800000000745058,
3
]
}
}
},
"Stats": [
{
"EquipmentID": 23048,
"PromotionCostList": [
{
"$type": "ItemConfigRow",
"ItemID": 2,
"ItemNum": 5000,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 115011,
"ItemNum": 8,
"Rarity": "NotNormal"
}
],
"PlayerLevelRequire": 15,
"MaxLevel": 20,
"BaseHP": 43.200000000186265,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 28.800000000745058,
"BaseAttackAdd": 4.320000000298023,
"BaseDefence": 21,
"BaseDefenceAdd": 3.1500000001396984
},
{
"EquipmentID": 23048,
"Promotion": 1,
"PromotionCostList": [
{
"$type": "ItemConfigRow",
"ItemID": 2,
"ItemNum": 10000,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 110231,
"ItemNum": 4,
"Rarity": "NotNormal"
},
{
"$type": "ItemConfigRow",
"ItemID": 115011,
"ItemNum": 12,
"Rarity": "NotNormal"
}
],
"WorldLevelRequire": 1,
"MaxLevel": 30,
"BaseHP": 95.04000000003725,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 63.360000000335276,
"BaseAttackAdd": 4.320000000298023,
"BaseDefence": 46.200000000186265,
"BaseDefenceAdd": 3.1500000001396984
},
{
"EquipmentID": 23048,
"Promotion": 2,
"PromotionCostList": [
{
"$type": "ItemConfigRow",
"ItemID": 2,
"ItemNum": 20000,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 110232,
"ItemNum": 4,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 115012,
"ItemNum": 8,
"Rarity": "Rare"
}
],
"WorldLevelRequire": 2,
"MaxLevel": 40,
"BaseHP": 164.160000000149,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 109.44000000040978,
"BaseAttackAdd": 4.320000000298023,
"BaseDefence": 79.80000000074506,
"BaseDefenceAdd": 3.1500000001396984
},
{
"EquipmentID": 23048,
"Promotion": 3,
"PromotionCostList": [
{
"$type": "ItemConfigRow",
"ItemID": 2,
"ItemNum": 50000,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 110232,
"ItemNum": 8,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 115012,
"ItemNum": 12,
"Rarity": "Rare"
}
],
"WorldLevelRequire": 3,
"MaxLevel": 50,
"BaseHP": 233.28000000026077,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 155.5200000004843,
"BaseAttackAdd": 4.320000000298023,
"BaseDefence": 113.40000000037253,
"BaseDefenceAdd": 3.1500000001396984
},
{
"EquipmentID": 23048,
"Promotion": 4,
"PromotionCostList": [
{
"$type": "ItemConfigRow",
"ItemID": 2,
"ItemNum": 100000,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 110233,
"ItemNum": 5,
"Rarity": "VeryRare"
},
{
"$type": "ItemConfigRow",
"ItemID": 115013,
"ItemNum": 6,
"Rarity": "VeryRare"
}
],
"WorldLevelRequire": 4,
"MaxLevel": 60,
"BaseHP": 302.40000000037253,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 201.6000000005588,
"BaseAttackAdd": 4.320000000298023,
"BaseDefence": 147,
"BaseDefenceAdd": 3.1500000001396984
},
{
"EquipmentID": 23048,
"Promotion": 5,
"PromotionCostList": [
{
"$type": "ItemConfigRow",
"ItemID": 2,
"ItemNum": 200000,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 110233,
"ItemNum": 10,
"Rarity": "VeryRare"
},
{
"$type": "ItemConfigRow",
"ItemID": 115013,
"ItemNum": 8,
"Rarity": "VeryRare"
}
],
"WorldLevelRequire": 5,
"MaxLevel": 70,
"BaseHP": 371.5200000004843,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 247.6800000006333,
"BaseAttackAdd": 4.320000000298023,
"BaseDefence": 180.6000000005588,
"BaseDefenceAdd": 3.1500000001396984
},
{
"EquipmentID": 23048,
"Promotion": 6,
"PromotionCostList": [],
"WorldLevelRequire": 5,
"MaxLevel": 80,
"BaseHP": 440.64000000059605,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 293.7600000007078,
"BaseAttackAdd": 4.320000000298023,
"BaseDefence": 214.20000000018626,
"BaseDefenceAdd": 3.1500000001396984
}
]
},
"24000": {
"Name": "とある{RUBY_B#アイオーン}星神{RUBY_E#}の{RUBY_B#うんらく}殞落{RUBY_E#}を{RUBY_B#しる}記{RUBY_E#}す",
"Desc": "一筋の光から始まった。\\n其らは墜ちる、消滅の脅威が見下す。\\n其らは自己複製を止め、先を争ってお互いを抱擁せざるを得なかった、\\n繁殖の権利を対価に、生存の可能性を掴もうと試みた。\\n其らは手を取り合い、今までにない程に団結した。\\n——しかし、運命は突然途絶え、\\n其らは、真の死へと向かった。",

View File

@@ -18206,7 +18206,7 @@
},
"21053": {
"Name": "언제나 여정이 평탄하기를",
"Desc": "소년은 별처럼 쏟아지는 빗속을 달리며, 오랜 가뭄 끝에 내린 단비에 환호했다.\\n\\n그 비는 아주 오랫동안 내렸고, 수레는 초록빛으로 뒤덮인 땅에 멈춰 섰다.\\n<i>「엄마, 누나, 이제 더 이상 굶거나 추위에 떨지 않아도 돼!」\\n</i>에브긴인들의 모닥불은 긴 밤을 따뜻하게 밝혔고, 소년은 누나의 이야기를 들으며 아침을 기다렸다.\\n\\n<i>「카카바샤, 너도 지모신께서 주신 행운으로 우리가 할 수 없는 일을 해낼 거야…. 언제나 여정이 평탄하며, 영원히 계략을 들키는 일이 없기를……」</i>\\n\\n황금빛 햇살이 꿈에서 새어나와 현실 거처를 비추자, 거래와 숫자가 또다시 시야에 들어왔다.\\n그는 움켜쥐었던 손을 풀었다——\\n\\n마치 아무것도 잡지 못한 것처럼, 마치 아직 온기가 남아 있는 것처럼",
"Desc": "소년은 별처럼 쏟아지는 빗속을 달리며, 오랜 가뭄 끝에 내린 단비에 환호했다.\\n\\n그 비는 아주 오랫동안 내렸고, 수레는 초록빛으로 뒤덮인 땅에 멈춰 섰다.\\n<i>「엄마, 누나, 이제 더 이상 굶거나 추위에 떨지 않아도 돼!」\\n</i>에브긴인들의 모닥불은 긴 밤을 따뜻하게 밝혔고, 소년은 누나의 이야기를 들으며 아침을 기다렸다.\\n\\n<i>「카카바샤, 너도 지모신께서 주신 행운으로 우리가 할 수 없는 일을 해낼 거야…. 너의 언제나 여정이 평탄하며, 영원히 계략을 들키는 일이 없기를……」</i>\\n\\n황금빛 햇살이 꿈에서 새어나와 현실 거처를 비추자, 거래와 숫자가 또다시 시야에 들어왔다.\\n그는 움켜쥐었던 손을 풀었다——\\n\\n마치 아무것도 잡지 못한 것처럼, 마치 온기가 아직 남아 있는 것처럼",
"Rarity": "CombatPowerLightconeRarity4",
"BaseType": "Knight",
"Refinements": {
@@ -18695,7 +18695,7 @@
},
"21055": {
"Name": "내일의 내일이 올 때까지",
"Desc": "<i>「트리스비오스, 내일 봐……」</i>\\n\\n어머니가 먼 길을 떠날 때 소녀는 그녀의 얼굴에 흐르는 눈물을 닦았다.\\n\\n그녀는 자신이 어머니처럼 때까지 수많은 내일을 기다렸다.\\n그녀는 불씨를 움켜쥔 채 문 앞에 섰다. 그러자 귓가에 어머니의 말이 들려왔다.\\n\\n<i>「그곳에는 눈보라도, 추위도, 폭우도 없을 거란다……」</i>\\n\\n<i>「하지만 내일 만나지 못한다면……」</i>\\n\\n그녀는 유성이 하늘을 가르듯 어린아이의 모습만을 남긴 채 땅으로 추락했다.\\n\\n<i>「내일의 내일 보자!」</i>",
"Desc": "<i>「트리스비오스, 내일 봐……」</i>\\n\\n어머니가 먼 길을 떠날 때 소녀는 그녀의 얼굴에 흐르는 눈물을 닦았다.\\n\\n그녀는 자신이 어머니처럼 성장할 때까지 수많은 내일을 기다렸다.\\n그녀는 불씨를 움켜쥔 채 문 앞에 섰다. 그러자 귓가에 어머니의 말이 들려왔다.\\n\\n<i>「그곳에는 눈보라도, 추위도, 폭우도 없을 거란다……」</i>\\n\\n<i>「하지만 내일 만나지 못한다면……」</i>\\n\\n그녀는 유성이 하늘을 가르듯 어린아이의 모습만을 남긴 채 대지로 떨어졌다.\\n\\n<i>「내일의 내일 보자!」</i>",
"Rarity": "CombatPowerLightconeRarity4",
"BaseType": "Priest",
"Refinements": {
@@ -18946,7 +18946,7 @@
"Rarity": "CombatPowerLightconeRarity4",
"BaseType": "Shaman",
"Refinements": {
"Name": "당황",
"Name": "허둥지둥",
"Desc": "전투 진입 후 모든 아군이 가하는 격파 피해가 <color=#f29e38ff><unbreak>#1[i]%</unbreak></color> 증가한다. 같은 유형의 스킬 효과는 중복 적용되지 않는다",
"Level": {
"1": {
@@ -19421,7 +19421,7 @@
},
"21058": {
"Name": "과거의 핏자국",
"Desc": "폭풍이 먼 곳에서 일렁이고 저승의 바다에서 손이 튀어나와 수상한 생명체를 놀리는 듯다.\\n바다 괴물의 잔해에서 하늘로 솟구치는 파도 돌멩이처럼 해연으로 추락다.\\n\\n들은 싸우는 포효가 열 낮과 밤 이어지고, 천둥소리를 넘었다고 다.\\n들은 만 개의 목숨이 있어도 죽음의 바다에 맞설 수 없다고 다.\\n\\n용맹함으로 미숙함을 씻어내고 거만함으로 나약함을 씻어낸 후——\\n\\n왜소한 그 몸은 해안가에 비틀거리며 올라왔다. 당시 해가 떠오르고 바닷물은 지난 날에 흘린 피와 같았다",
"Desc": "먼 곳에서 폭풍이 형성되고 있는 가운데, 저승의 바다에서 튀어나온 촉수는 표류하는 나약한 생명체를 괴롭히려는 듯다.\\n바다 괴물의 잘린 촉수는 하늘로 솟구치는 파도를 내려치더니 돌멩이처럼 부서지고 해연으로 추락다.\\n\\n사람들은 포효가 열 차례의 밤낮 동안 이어지고, 천둥소리를 뒤덮을 정도로 컸다고 다.\\n사람들은 만 개의 목숨이 있어도 저승의 바다에 맞설 수 없다고 다.\\n\\n용맹함으로 미숙함을 씻어내고, 거만함으로 나약함을 씻어내고 나서야——\\n\\n왜소한 그 몸은 비틀거리며 해안가에 올라왔다. 해가 이제 막 떠오르고, 바닷물은 마치 지난날에 흘린 피와 같았다",
"Rarity": "CombatPowerLightconeRarity4",
"BaseType": "Warrior",
"Refinements": {
@@ -19905,7 +19905,7 @@
},
"21061": {
"Name": "휴일의 목욕탕 대모험",
"Desc": "<i>「히아, 우리 새로 생긴 특색 목욕탕에 가보자!」</i>\\n\\n오크마에서 진료를 마친 소녀는 호기심에 가득 찬 채로 아이와 함께 각양각색의 목욕탕에 들어갔다.\\n\\n<i>「과일즙 안마, 한번 받아 보실래요?」</i>\\n소녀는 붉은 두 손을 보곤, 의욕 넘치는 이카를 껴안은 채 몇걸음 뒤로 물러났다——\\n<i>「과일즙은… 마시는 걸 더 선호해요……」</i>\\n\\n<i>「드로마스가 등 밟아드립니다! 기간 한정 할인 중이에요!」</i>\\n호기심 많은 드로마스가 고개를 숙이자, 콧바람이 소녀의 머리카락을 스쳤다.\\n<i>「다, 다음에 꼭 해볼게요!」</i>\\n\\n<i>「몸이 건강해지는 약초탕입니다. 몇 분이세요?」</i>\\n소녀는 향긋한 약초 향을 맡으며 조심스레 탕 안으로 발을 들였다.\\n<i>「몸에 아주 좋을 것 같아요……」</i>\\n\\n<i>「두, 두두!」</i>\\n이카가 물속에서 신나게 배영을 하기 시작했다.\\n<i>「이카도 많이 지쳤나보네……」</i>\\n\\n모처럼의 휴가에 소녀는 포도즙을 마시며 적발의 아이와 잠을 잘 자는 비결부터 디저트를 만드는 방법까지 다양한 이야기를 나눴다.\\n\\n<i>「큰일이야… 이, 이카가!」</i>\\n놀란 아이의 외침이 목욕탕의 고요함을 깨뜨렸다.\\n\\n<i>「두……」\\n「이카, 조금만 버텨!」</i>",
"Desc": "<i>「히아, 우리 새로 생긴 특색 목욕탕에 가보자!」</i>\\n\\n오크마에서 진료를 마친 소녀는 호기심에 가득 찬 채로 아이와 함께 각양각색의 목욕탕에 들어갔다.\\n\\n<i>「과일즙 안마, 한번 받아 보실래요?」</i>\\n소녀는 붉은 두 손을 보곤, 의욕 넘치는 이카를 껴안은 채 몇걸음 뒤로 물러났다——\\n<i>「과일즙은… 마시는 걸 더 선호해요……」</i>\\n\\n<i>「드로마스가 등 밟아드립니다! 기간 한정 할인 중이에요!」</i>\\n호기심 많은 드로마스가 고개를 숙이자, 콧바람이 소녀의 머리카락을 스쳤다.\\n<i>「다, 다음에 꼭 해볼게요!」</i>\\n\\n<i>「몸이 건강해지는 약초탕입니다. 몇 분이세요?」</i>\\n소녀는 향긋한 약초 향을 맡으며 조심스레 탕 안으로 발을 들였다.\\n<i>「몸에 아주 좋을 것 같아요……」</i>\\n\\n<i>「두, 두두!」</i>\\n이카가 물속에서 신나게 배영을 하기 시작했다.\\n<i>「이카도 많이 지쳤나보네……」</i>\\n\\n모처럼의 휴가에 소녀는 포도즙을 마시며 적발의 아이와 잠을 잘 자는 비결부터 디저트를 만드는 방법까지 다양한 이야기를 나눴다.\\n\\n<i>「큰일이야… 이, 이카가!」</i>\\n놀란 아이의 외침이 목욕탕의 고요함을 깨뜨렸다.\\n\\n<i>「두……」\\n「이카, 조금만 버텨!」</i>",
"Rarity": "CombatPowerLightconeRarity4",
"BaseType": "Warlock",
"Refinements": {
@@ -20157,7 +20157,7 @@
},
"21062": {
"Name": "그 종착지에서 다시 만나자",
"Desc": "분의 눈물이 뺨을 타고 흘러내렸다. 파괴된 법진 중앙에 남은 것은 죽음의 그림자, 그리고 끝없는 고요함과 공허함이었다.\\n\\n그때, 어둠 속에서 탄식이 들려왔다. 그것은 「영혼」의 떨림이었다.\\n<i>「…누나?」</i>\\n소년은 손을 들어 올렸고, 그 눈동자에는 한 줄기 빛이 스쳤다. 가족의 모습은 아주 잠시 머물렀지만, 그 순간은 그의 마음속에서 천 년 동안 멈춰 있는 듯했다.\\n\\n<i>「알겠어…. 마지막으로 작별 인사를 하러 와줘서 고마워」</i>\\n그는 천천히 몸을 일으켰고, 첫 번째 아침 햇살이 단호한 얼굴과 마르지 않은 눈물 자국을 비췄다.\\n\\n<i>「탐구는 길고 고독한 여정이 될 테니, 우린 종착에서 다시 만나자……」</i>",
"Desc": "분의 눈물이 뺨을 타고 흘러내렸다. 파괴된 법진 중앙에 남은 것은 죽음의 그림자, 그리고 끝없는 고요함과 공허함이었다.\\n\\n그때, 어둠 속에서 탄식이 들려왔다. 그것은 「영혼」의 떨림이었다.\\n<i>「…누나?」</i>\\n소년은 손을 들어 올렸고, 그 눈동자에는 한 줄기 빛이 스쳤다. 가족의 모습은 아주 잠시 머물렀지만, 그 순간은 그의 마음속에서 천년 동안 멈춰 있는 듯했다.\\n\\n<i>「알겠어…. 마지막으로 작별 인사를 하러 와줘서 고마워」</i>\\n그는 천천히 몸을 일으켰고, 첫 번째 아침 햇살이 단호한 얼굴과 마르지 않은 눈물 자국을 비췄다.\\n\\n<i>「탐구는 길고 고독한 여정이 될 테니, 우린 종착에서 다시 만나자……」</i>",
"Rarity": "CombatPowerLightconeRarity4",
"BaseType": "Rogue",
"Refinements": {
@@ -21622,6 +21622,253 @@
}
]
},
"22005": {
"Name": "영원한 미궁의 식사",
"Desc": "우르릉 쾅쾅——\\n미궁 밖, 비바람이 끊임없이 휘몰아친다.\\n보글보글——\\n요정들의 신기한 거대 솥이 펄펄 끓는다.\\n\\n<i>「특제 향신료를 뿌리고…」</i>\\n<i>「신선한 채소를 넣자!」</i>\\n<i>「이번에는 분명 성공할 수 있을 거야!」</i>\\n\\n솥이 또다시 보글보글 끓었고, 요정들은 솥을 간절하게 바라봤다.\\n<i>「온다, 온다!」</i>\\n강렬한 빛이 솥 아래에서 뿜어져 나왔다——\\n\\n이에 모두가 모여들었다!\\n\\n……\\n\\n향기로운 연회에서 손님들은 잔을 들고 함께 기묘한 세월을 축하한다——\\n\\n이 꿈속의 낙원이 영원히 따뜻하고, 영원히 즐겁기를!",
"Rarity": "CombatPowerLightconeRarity4",
"BaseType": "Shaman",
"Refinements": {
"Name": "최고!",
"Desc": "장착한 캐릭터의 공격력이 <color=#f29e38ff><unbreak>#1[i]%</unbreak></color> 증가한다. 장착한 캐릭터가 전투 스킬 발동 후 공격력이 <color=#f29e38ff><unbreak>#2[i]%</unbreak></color> 증가한다, 해당 효과 최대 중첩수: <unbreak>#3[i]</unbreak>스택",
"Level": {
"1": {
"ParamList": [
0.1600000001490116,
0.0800000000745058,
3
]
},
"2": {
"ParamList": [
0.20000000018626451,
0.10000000009313226,
3
]
},
"3": {
"ParamList": [
0.24000000022351742,
0.12000000011175871,
3
]
},
"4": {
"ParamList": [
0.2800000002607703,
0.14000000013038516,
3
]
},
"5": {
"ParamList": [
0.3200000002980232,
0.1600000001490116,
3
]
}
}
},
"Stats": [
{
"EquipmentID": 22005,
"PromotionCostList": [
{
"$type": "ItemConfigRow",
"ItemID": 2,
"ItemNum": 4000,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 115001,
"ItemNum": 5,
"Rarity": "NotNormal"
}
],
"PlayerLevelRequire": 15,
"MaxLevel": 20,
"BaseHP": 43.200000000186265,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 21.600000000558794,
"BaseAttackAdd": 3.2400000002235174,
"BaseDefence": 15,
"BaseDefenceAdd": 2.2500000002328306
},
{
"EquipmentID": 22005,
"Promotion": 1,
"PromotionCostList": [
{
"$type": "ItemConfigRow",
"ItemID": 2,
"ItemNum": 8000,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 110231,
"ItemNum": 3,
"Rarity": "NotNormal"
},
{
"$type": "ItemConfigRow",
"ItemID": 115001,
"ItemNum": 10,
"Rarity": "NotNormal"
}
],
"WorldLevelRequire": 1,
"MaxLevel": 30,
"BaseHP": 95.04000000003725,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 47.52000000048429,
"BaseAttackAdd": 3.2400000002235174,
"BaseDefence": 33,
"BaseDefenceAdd": 2.2500000002328306
},
{
"EquipmentID": 22005,
"Promotion": 2,
"PromotionCostList": [
{
"$type": "ItemConfigRow",
"ItemID": 2,
"ItemNum": 16000,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 110232,
"ItemNum": 3,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 115002,
"ItemNum": 6,
"Rarity": "Rare"
}
],
"WorldLevelRequire": 2,
"MaxLevel": 40,
"BaseHP": 164.160000000149,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 82.0800000000745,
"BaseAttackAdd": 3.2400000002235174,
"BaseDefence": 57,
"BaseDefenceAdd": 2.2500000002328306
},
{
"EquipmentID": 22005,
"Promotion": 3,
"PromotionCostList": [
{
"$type": "ItemConfigRow",
"ItemID": 2,
"ItemNum": 40000,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 110232,
"ItemNum": 6,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 115002,
"ItemNum": 9,
"Rarity": "Rare"
}
],
"WorldLevelRequire": 3,
"MaxLevel": 50,
"BaseHP": 233.28000000026077,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 116.64000000059605,
"BaseAttackAdd": 3.2400000002235174,
"BaseDefence": 81,
"BaseDefenceAdd": 2.2500000002328306
},
{
"EquipmentID": 22005,
"Promotion": 4,
"PromotionCostList": [
{
"$type": "ItemConfigRow",
"ItemID": 2,
"ItemNum": 80000,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 110233,
"ItemNum": 4,
"Rarity": "VeryRare"
},
{
"$type": "ItemConfigRow",
"ItemID": 115003,
"ItemNum": 5,
"Rarity": "VeryRare"
}
],
"WorldLevelRequire": 4,
"MaxLevel": 60,
"BaseHP": 302.40000000037253,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 151.20000000018626,
"BaseAttackAdd": 3.2400000002235174,
"BaseDefence": 105,
"BaseDefenceAdd": 2.2500000002328306
},
{
"EquipmentID": 22005,
"Promotion": 5,
"PromotionCostList": [
{
"$type": "ItemConfigRow",
"ItemID": 2,
"ItemNum": 160000,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 110233,
"ItemNum": 8,
"Rarity": "VeryRare"
},
{
"$type": "ItemConfigRow",
"ItemID": 115003,
"ItemNum": 7,
"Rarity": "VeryRare"
}
],
"WorldLevelRequire": 5,
"MaxLevel": 70,
"BaseHP": 371.5200000004843,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 185.7600000007078,
"BaseAttackAdd": 3.2400000002235174,
"BaseDefence": 129,
"BaseDefenceAdd": 2.2500000002328306
},
{
"EquipmentID": 22005,
"Promotion": 6,
"PromotionCostList": [],
"WorldLevelRequire": 5,
"MaxLevel": 80,
"BaseHP": 440.64000000059605,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 220.32000000029802,
"BaseAttackAdd": 3.2400000002235174,
"BaseDefence": 153,
"BaseDefenceAdd": 2.2500000002328306
}
]
},
"23000": {
"Name": "은하철도의 밤",
"Desc": "발밑이 걱정된다면 다시 한번 하늘을 올려다 보라.\\n별들이 보내주는 부드러운 시선을 느끼면 마음은 바람을 타고 날아오를 테니까.\\n떠오르는 생각과 들려오는 속삭임은\\n모두 언제까지나 삶의 버팀목이 되어줄 것이다",
@@ -32550,7 +32797,7 @@
},
"23043": {
"Name": "바람에 흩날리는 거짓말",
"Desc": "고양이 귀 소녀가 지붕에서 몸을 쭉 펴고 야경 속으로 들어갔다.\\n천 년 동안 침묵했던 보물 창고에서 소녀는 가뿐하게 왔다갔다 움직이며, 보물을 품에 안고 소리 없이 도망쳤다.\\n<i>「어리석은 운명 따위 날 쫓아올 수 없지!」</i>\\n\\n폐허에 불어오는 황야의 냄새가 섞인 바람 소녀의 후드를 건드렸다.\\n소녀는 차가운 돌을 베고 누워 거친 식량을 삼키며 손끝에 있는 금화를 갖고 놀았다.\\n\\n먼 곳에서 나는 작은 소리가 그녀의 두 귀에 들려왔다. 여명의 빛이 비추는 거룩한 도시에서 북적거리는 사람들은 여전히 축제와 따뜻함을 즐기고 있었다.\\n<i>「얼마나 더 필요하지… 1년, 아니면 만 년?」</i>\\n\\n그녀는 기지개를 켜고 불안한 생각을 무시했다.\\n<i>「후후, 사이퍼라는 늘 마지막까지 웃을 테니까!」</i>\\n\\n금색 번개가 하늘을 갈랐고, 소녀의 선언이 여전히 바람 속에 흩날렸다.\\n——천 년 전과 다름없이 말이다",
"Desc": "고양이 귀 소녀가 지붕에서 몸을 쭉 펴고 야경 속으로 들어갔다.\\n천년 동안 침묵했던 보물 창고에서 소녀는 가뿐하게 왔다갔다 움직이며, 보물을 품에 안고 소리 없이 도망쳤다.\\n<i>「어리석은 운명 따위 날 쫓아올 수 없지!」</i>\\n\\n폐허에 불어오는 황야의 냄새가 섞인 바람 소녀의 후드를 건드렸다.\\n소녀는 차가운 돌을 베고 누워 거친 식량을 삼키며 손끝에 있는 금화를 갖고 놀았다.\\n\\n먼 곳에서 나는 작은 소리가 그녀의 두 귀에 들려왔다. 여명의 빛이 비추는 거룩한 도시에서 북적거리는 사람들은 여전히 축제와 따뜻함을 즐기고 있었다.\\n<i>「얼마나 더 필요하지… 1년, 아니면 만 년?」</i>\\n\\n그녀는 기지개를 켜고 불안한 생각을 무시했다.\\n<i>「후후, 사이퍼라는 늘 마지막까지 웃을 테니까!」</i>\\n\\n금색 번개가 하늘을 갈랐고, 소녀의 선언이 여전히 바람 속에 흩날렸다.\\n——천년 전과 다름없이 말이다",
"Rarity": "CombatPowerLightconeRarity5",
"BaseType": "Warlock",
"Refinements": {
@@ -32822,7 +33069,7 @@
"BaseType": "Warrior",
"Refinements": {
"Name": "상실",
"Desc": "장착한 캐릭터의 기본 속도가 <color=#f29e38ff><unbreak>#1[i]</unbreak></color> 증가하고, 피해를 가할 시 목표의 방어력을 <color=#f29e38ff><unbreak>#2[i]%</unbreak></color> 무시한다. 장착한 캐릭터가 필살기 발동 [뜨거운 태양]을 획득하며, 턴 시작 시 해제된다. [뜨거운 태양] 보유 시 장착한 캐릭터가 가하는 피해가 <color=#f29e38ff><unbreak>#3[i]%</unbreak></color> 증가한다",
"Desc": "장착한 캐릭터의 기본 속도가 <color=#f29e38ff><unbreak>#1[i]</unbreak></color> 증가하고, 피해를 가할 시 목표의 방어력을 <color=#f29e38ff><unbreak>#2[i]%</unbreak></color> 무시한다. 장착한 캐릭터가 필살기 발동하면 [뜨거운 태양]을 획득하며, 턴 시작 시 해제된다. [뜨거운 태양] 보유 시 장착한 캐릭터가 가하는 피해가 <color=#f29e38ff><unbreak>#3[i]%</unbreak></color> 증가한다",
"Level": {
"1": {
"ParamList": [
@@ -33069,7 +33316,7 @@
"BaseType": "Warrior",
"Refinements": {
"Name": "기사왕",
"Desc": "장착한 캐릭터의 치명타 피해가 <color=#f29e38ff><unbreak>#1[i]%</unbreak></color> 증가한다. 필살기 발동 시 장착한 캐릭터의 공격력이 <color=#f29e38ff><unbreak>#6[i]%</unbreak></color> 증가하며, 장착한 캐릭터의 에너지 최대치가 <unbreak>#3[i]</unbreak>pt 이상일 경우 장착한 캐릭터의 에너지를 장착한 캐릭터 에너지 최대치의 <unbreak>#5[i]%</unbreak>만큼 고정으로 회복하고, 장착한 캐릭터의 공격력을 <color=#f29e38ff><unbreak>#2[i]%</unbreak></color> 증가시킨다, 지속 시간: <unbreak>#4[i]</unbreak>턴",
"Desc": "장착한 캐릭터의 치명타 피해가 <color=#f29e38ff><unbreak>#1[i]%</unbreak></color> 증가한다. 필살기 발동 시 장착한 캐릭터의 공격력이 <color=#f29e38ff><unbreak>#6[i]%</unbreak></color> 증가하며, 장착한 캐릭터의 에너지 최대치가 <unbreak>#3[i]</unbreak>pt 이상일 경우 장착한 캐릭터의 에너지를 장착한 캐릭터 에너지 최대치의 <unbreak>#5[i]%</unbreak>만큼 고정으로 회복하고, 다시 장착한 캐릭터의 공격력을 <color=#f29e38ff><unbreak>#2[i]%</unbreak></color> 증가시킨다, 지속 시간: <unbreak>#4[i]</unbreak>턴",
"Level": {
"1": {
"ParamList": [
@@ -33331,7 +33578,7 @@
"BaseType": "Rogue",
"Refinements": {
"Name": "적원엽병",
"Desc": "장착한 캐릭터의 치명타 확률이 <color=#f29e38ff><unbreak>#1[i]%</unbreak></color> 증가한다. 전투 진입 시 아군의 전투 스킬 포인트 최대치가 <unbreak>#2[i]</unbreak>pt 이상일 경우, 장착한 캐릭터의 공격력이 <color=#f29e38ff><unbreak>#3[i]%</unbreak></color> 증가한다. 장착한 캐릭터가 전투 스킬을 발동할 때마다 장착한 캐릭터의 공격력 <color=#f29e38ff><unbreak>#4[i]%</unbreak></color> 증가시킨다, 최대 중첩수: <unbreak>#5[i]</unbreak>회",
"Desc": "장착한 캐릭터의 치명타 확률이 <color=#f29e38ff><unbreak>#1[i]%</unbreak></color> 증가한다. 전투 진입 시 아군의 전투 스킬 포인트 최대치가 <unbreak>#2[i]</unbreak>pt 이상일 경우, 장착한 캐릭터의 공격력이 <color=#f29e38ff><unbreak>#3[i]%</unbreak></color> 증가한다. 장착한 캐릭터가 전투 스킬을 발동할 때마다 장착한 캐릭터의 공격력 <color=#f29e38ff><unbreak>#4[i]%</unbreak></color> 증가다, 최대 중첩수: <unbreak>#5[i]</unbreak>회",
"Level": {
"1": {
"ParamList": [
@@ -33581,6 +33828,530 @@
}
]
},
"23047": {
"Name": "바다는 왜 노래하는가",
"Desc": "파도가 몰아치고, 그 울림이 하늘 끝까지 울려 퍼진다.\\n물고기는 텅 빈 바다를 유영하고, 과거의 거품은 출렁거린다. 한 곡의 선율이 여전히 메아리치는 듯하다.\\n\\n<i>「존귀한 공주님, 저희와 함께 연회를 즐기시죠!」</i>\\n심해의 부름에 응답하기 위해, 그녀는 성대한 무대 위에 혼자 남을 때까지 검은 물결과 함께 춤췄다.\\n\\n<i>「바다의 검객이여, 어째서 침묵하고, 무엇을 위해 노래하시나요?」</i>\\n그녀는 대지를 딛는 매 순간 고통을 견뎠다. 그녀는 좀처럼 노래하지 않고, 오직 그 핏빛의 검날로 생명의 비명을 연주할 뿐이었다.\\n\\n<i>「글래디오럼 경, 나와 함께 별바다를 정복하자!」</i>\\n그 뜨거운 불빛을 발견했을 때 그녀는 그것이 연회의 시작이라 믿었고, 마침내 연주의 활을 들었다.\\n\\n……\\n\\n<i>「헬렉트라... 헬렉트라… 노래가 끝난 지금, 어디로 향해야 할까?」</i>\\n\\n헛된 희망이 부서질 때마다 그녀는 늘 고독한 선율로 사람들의 질문에 답했지만, 그 소리는 듣는 이를 매혹시킬 뿐이었다——\\n\\n어쩌면… 인간 세상이든 깊은 바다든, 진정으로 들을 수 있는 자는 연주자 자신뿐일지도 모른다",
"Rarity": "CombatPowerLightconeRarity5",
"BaseType": "Warlock",
"Refinements": {
"Name": "독주",
"Desc": "장착한 캐릭터의 효과 명중이 <color=#f29e38ff><unbreak>#1[i]%</unbreak></color> 증가하고, 장착한 캐릭터가 부여한 디버프 상태에 빠진 적이 있을 시 대상을 <unbreak>#2[i]%</unbreak>의 기본 확률로 [혼미] 상태에 빠트린다, 지속 시간: <unbreak>#3[i]</unbreak>턴, 같은 유형의 효과는 중첩되지 않는다. [혼미] 상태일 때 장착한 캐릭터가 부여한 디버프 상태 1개당 받는 지속 피해가 <color=#f29e38ff><unbreak>#4[f1]%</unbreak></color> 증가한다, 해당 효과 최대 중첩수: <unbreak>#5[i]</unbreak>스택, 아군의 공격을 받을 시 공격자의 속도가 <color=#f29e38ff><unbreak>#6[f1]%</unbreak></color> 증가한다, 지속 시간: <unbreak>#7[i]</unbreak>턴",
"Level": {
"1": {
"ParamList": [
0.40000000037252903,
0.8000000007450581,
3,
0.05000000004656613,
6,
0.10000000009313226,
3
]
},
"2": {
"ParamList": [
0.45000000041909516,
0.8000000007450581,
3,
0.06250000023283064,
6,
0.12499999976716936,
3
]
},
"3": {
"ParamList": [
0.5000000004656613,
0.8000000007450581,
3,
0.07499999972060323,
6,
0.1500000001396984,
3
]
},
"4": {
"ParamList": [
0.5500000005122274,
0.8000000007450581,
3,
0.08749999990686774,
6,
0.17499999981373549,
3
]
},
"5": {
"ParamList": [
0.6000000005587935,
0.8000000007450581,
3,
0.10000000009313226,
6,
0.20000000018626451,
3
]
}
}
},
"Stats": [
{
"EquipmentID": 23047,
"PromotionCostList": [
{
"$type": "ItemConfigRow",
"ItemID": 2,
"ItemNum": 5000,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 115001,
"ItemNum": 8,
"Rarity": "NotNormal"
}
],
"PlayerLevelRequire": 15,
"MaxLevel": 20,
"BaseHP": 43.200000000186265,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 28.800000000745058,
"BaseAttackAdd": 4.320000000298023,
"BaseDefence": 21,
"BaseDefenceAdd": 3.1500000001396984
},
{
"EquipmentID": 23047,
"Promotion": 1,
"PromotionCostList": [
{
"$type": "ItemConfigRow",
"ItemID": 2,
"ItemNum": 10000,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 110221,
"ItemNum": 4,
"Rarity": "NotNormal"
},
{
"$type": "ItemConfigRow",
"ItemID": 115001,
"ItemNum": 12,
"Rarity": "NotNormal"
}
],
"WorldLevelRequire": 1,
"MaxLevel": 30,
"BaseHP": 95.04000000003725,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 63.360000000335276,
"BaseAttackAdd": 4.320000000298023,
"BaseDefence": 46.200000000186265,
"BaseDefenceAdd": 3.1500000001396984
},
{
"EquipmentID": 23047,
"Promotion": 2,
"PromotionCostList": [
{
"$type": "ItemConfigRow",
"ItemID": 2,
"ItemNum": 20000,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 110222,
"ItemNum": 4,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 115002,
"ItemNum": 8,
"Rarity": "Rare"
}
],
"WorldLevelRequire": 2,
"MaxLevel": 40,
"BaseHP": 164.160000000149,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 109.44000000040978,
"BaseAttackAdd": 4.320000000298023,
"BaseDefence": 79.80000000074506,
"BaseDefenceAdd": 3.1500000001396984
},
{
"EquipmentID": 23047,
"Promotion": 3,
"PromotionCostList": [
{
"$type": "ItemConfigRow",
"ItemID": 2,
"ItemNum": 50000,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 110222,
"ItemNum": 8,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 115002,
"ItemNum": 12,
"Rarity": "Rare"
}
],
"WorldLevelRequire": 3,
"MaxLevel": 50,
"BaseHP": 233.28000000026077,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 155.5200000004843,
"BaseAttackAdd": 4.320000000298023,
"BaseDefence": 113.40000000037253,
"BaseDefenceAdd": 3.1500000001396984
},
{
"EquipmentID": 23047,
"Promotion": 4,
"PromotionCostList": [
{
"$type": "ItemConfigRow",
"ItemID": 2,
"ItemNum": 100000,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 110223,
"ItemNum": 5,
"Rarity": "VeryRare"
},
{
"$type": "ItemConfigRow",
"ItemID": 115003,
"ItemNum": 6,
"Rarity": "VeryRare"
}
],
"WorldLevelRequire": 4,
"MaxLevel": 60,
"BaseHP": 302.40000000037253,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 201.6000000005588,
"BaseAttackAdd": 4.320000000298023,
"BaseDefence": 147,
"BaseDefenceAdd": 3.1500000001396984
},
{
"EquipmentID": 23047,
"Promotion": 5,
"PromotionCostList": [
{
"$type": "ItemConfigRow",
"ItemID": 2,
"ItemNum": 200000,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 110223,
"ItemNum": 10,
"Rarity": "VeryRare"
},
{
"$type": "ItemConfigRow",
"ItemID": 115003,
"ItemNum": 8,
"Rarity": "VeryRare"
}
],
"WorldLevelRequire": 5,
"MaxLevel": 70,
"BaseHP": 371.5200000004843,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 247.6800000006333,
"BaseAttackAdd": 4.320000000298023,
"BaseDefence": 180.6000000005588,
"BaseDefenceAdd": 3.1500000001396984
},
{
"EquipmentID": 23047,
"Promotion": 6,
"PromotionCostList": [],
"WorldLevelRequire": 5,
"MaxLevel": 80,
"BaseHP": 440.64000000059605,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 293.7600000007078,
"BaseAttackAdd": 4.320000000298023,
"BaseDefence": 214.20000000018626,
"BaseDefenceAdd": 3.1500000001396984
}
]
},
"23048": {
"Name": "황금 피가 새긴 시대",
"Desc": "<i>「천 년 후, 역사는 케리드라를 어떻게 기억할까?」</i>\\n누군가 그녀에게 이렇게 물은 적이 있다.\\n\\n어떤 이들의 마음속에서 그녀는 위명을 떨친 「카이사르」였다.\\n차갑고 잔혹한 폭군, 야망을 품은 「화염의 왕관」, 그리고 피와 불로 구시대를 태운 여황……\\n\\n어떤 이들은 그녀가 가여운 한낱 「체스 말」에 불과하다고 반박했다.\\n모두에게 버림받은 왕은 결국 야망의 포로였으며, 신과의 대국에서 그녀는 모든 것을 잃을 수밖에 없는 운명이었다고.\\n\\n또 어떤 이들은 말했다. 바다보다 어두운 마음 깊은 곳에서, 그녀는 다만 「케리드라」라는 이름의 평범한 인간이었노라고——\\n그녀 역시 희생된 동료를 애도했고, 무거운 사명 앞에서 두려움에 떨며 망설였다고……\\n\\n<i>「훗, 역사가 나를 기억할 필요 없다……」</i>\\n\\n그녀는 가슴 깊이의 쓰라림을 삼키고, 희생으로 포장된 불 쫓는 길을 계속 걸어갔다——\\n\\n<i>「우리의 타오르는 황금 피가 그것을 새겼노라!」</i>",
"Rarity": "CombatPowerLightconeRarity5",
"BaseType": "Shaman",
"Refinements": {
"Name": "정벌",
"Desc": "장착한 캐릭터의 공격력이 <color=#f29e38ff><unbreak>#1[i]%</unbreak></color> 증가한다. 필살기를 발동해 공격하면 전투 스킬 포인트를 <unbreak>#3[i]</unbreak>pt 회복하며, 장착한 캐릭터가 단일 아군 캐릭터에게 전투 스킬 발동 후 목표가 가하는 전투 스킬 피해가 <color=#f29e38ff><unbreak>#4[f1]%</unbreak></color> 증가한다, 지속 시간: <unbreak>#5[i]</unbreak>턴",
"Level": {
"1": {
"ParamList": [
0.6400000005960464,
1,
1,
0.5400000005029142,
3
]
},
"2": {
"ParamList": [
0.8000000007450581,
1,
1,
0.6750000002793968,
3
]
},
"3": {
"ParamList": [
0.9600000008940697,
1,
1,
0.8100000007543713,
3
]
},
"4": {
"ParamList": [
1.1200000001117587,
1,
1,
0.9450000005308539,
3
]
},
"5": {
"ParamList": [
1.2800000002607703,
1,
1,
1.0800000000745058,
3
]
}
}
},
"Stats": [
{
"EquipmentID": 23048,
"PromotionCostList": [
{
"$type": "ItemConfigRow",
"ItemID": 2,
"ItemNum": 5000,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 115011,
"ItemNum": 8,
"Rarity": "NotNormal"
}
],
"PlayerLevelRequire": 15,
"MaxLevel": 20,
"BaseHP": 43.200000000186265,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 28.800000000745058,
"BaseAttackAdd": 4.320000000298023,
"BaseDefence": 21,
"BaseDefenceAdd": 3.1500000001396984
},
{
"EquipmentID": 23048,
"Promotion": 1,
"PromotionCostList": [
{
"$type": "ItemConfigRow",
"ItemID": 2,
"ItemNum": 10000,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 110231,
"ItemNum": 4,
"Rarity": "NotNormal"
},
{
"$type": "ItemConfigRow",
"ItemID": 115011,
"ItemNum": 12,
"Rarity": "NotNormal"
}
],
"WorldLevelRequire": 1,
"MaxLevel": 30,
"BaseHP": 95.04000000003725,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 63.360000000335276,
"BaseAttackAdd": 4.320000000298023,
"BaseDefence": 46.200000000186265,
"BaseDefenceAdd": 3.1500000001396984
},
{
"EquipmentID": 23048,
"Promotion": 2,
"PromotionCostList": [
{
"$type": "ItemConfigRow",
"ItemID": 2,
"ItemNum": 20000,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 110232,
"ItemNum": 4,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 115012,
"ItemNum": 8,
"Rarity": "Rare"
}
],
"WorldLevelRequire": 2,
"MaxLevel": 40,
"BaseHP": 164.160000000149,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 109.44000000040978,
"BaseAttackAdd": 4.320000000298023,
"BaseDefence": 79.80000000074506,
"BaseDefenceAdd": 3.1500000001396984
},
{
"EquipmentID": 23048,
"Promotion": 3,
"PromotionCostList": [
{
"$type": "ItemConfigRow",
"ItemID": 2,
"ItemNum": 50000,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 110232,
"ItemNum": 8,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 115012,
"ItemNum": 12,
"Rarity": "Rare"
}
],
"WorldLevelRequire": 3,
"MaxLevel": 50,
"BaseHP": 233.28000000026077,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 155.5200000004843,
"BaseAttackAdd": 4.320000000298023,
"BaseDefence": 113.40000000037253,
"BaseDefenceAdd": 3.1500000001396984
},
{
"EquipmentID": 23048,
"Promotion": 4,
"PromotionCostList": [
{
"$type": "ItemConfigRow",
"ItemID": 2,
"ItemNum": 100000,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 110233,
"ItemNum": 5,
"Rarity": "VeryRare"
},
{
"$type": "ItemConfigRow",
"ItemID": 115013,
"ItemNum": 6,
"Rarity": "VeryRare"
}
],
"WorldLevelRequire": 4,
"MaxLevel": 60,
"BaseHP": 302.40000000037253,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 201.6000000005588,
"BaseAttackAdd": 4.320000000298023,
"BaseDefence": 147,
"BaseDefenceAdd": 3.1500000001396984
},
{
"EquipmentID": 23048,
"Promotion": 5,
"PromotionCostList": [
{
"$type": "ItemConfigRow",
"ItemID": 2,
"ItemNum": 200000,
"Rarity": "Rare"
},
{
"$type": "ItemConfigRow",
"ItemID": 110233,
"ItemNum": 10,
"Rarity": "VeryRare"
},
{
"$type": "ItemConfigRow",
"ItemID": 115013,
"ItemNum": 8,
"Rarity": "VeryRare"
}
],
"WorldLevelRequire": 5,
"MaxLevel": 70,
"BaseHP": 371.5200000004843,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 247.6800000006333,
"BaseAttackAdd": 4.320000000298023,
"BaseDefence": 180.6000000005588,
"BaseDefenceAdd": 3.1500000001396984
},
{
"EquipmentID": 23048,
"Promotion": 6,
"PromotionCostList": [],
"WorldLevelRequire": 5,
"MaxLevel": 80,
"BaseHP": 440.64000000059605,
"BaseHPAdd": 6.480000000447035,
"BaseAttack": 293.7600000007078,
"BaseAttackAdd": 4.320000000298023,
"BaseDefence": 214.20000000018626,
"BaseDefenceAdd": 3.1500000001396984
}
]
},
"24000": {
"Name": "어떤 에이언즈의 몰락",
"Desc": "한 줄기의 빛에서 시작되어,\\n그들은 추락하고, 멸망의 위협이 고조되고 있다.\\n그들은 어쩔 수 없이 자가 복제를 멈추고, 앞을 다투어 서로 껴안아,\\n번식할 권리를 대가로 생존의 가능성을 바꾸고자 했다.\\n그들은 손을 맞잡았다, 이렇게 단결해 본 적이 없음에도\\n——운명의 길은 끊기고,\\n그들은 진정한 죽음을 향해 갔다",

63143
data/moc.cn.json Normal file

File diff suppressed because it is too large Load Diff

63143
data/moc.en.json Normal file

File diff suppressed because it is too large Load Diff

63143
data/moc.jp.json Normal file

File diff suppressed because it is too large Load Diff

63143
data/moc.kr.json Normal file

File diff suppressed because it is too large Load Diff

27434
data/pf.cn.json Normal file

File diff suppressed because it is too large Load Diff

27434
data/pf.en.json Normal file

File diff suppressed because it is too large Load Diff

27434
data/pf.jp.json Normal file

File diff suppressed because it is too large Load Diff

27434
data/pf.kr.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -11,12 +11,12 @@
"path": "命运",
"rarity": "稀有度",
"element": "元素",
"technique": "秘",
"technique": "秘",
"talent": "天赋",
"basic": "普通攻击",
"skill": "技能",
"ultimate": "终技",
"servant": "仆从",
"ultimate": "终技",
"servant": "忆灵",
"damage": "伤害",
"type": "类型",
"warrior": "毁灭",
@@ -24,12 +24,12 @@
"mage": "博学",
"priest": "丰饶",
"rogue": "狩猎",
"shaman": "谐",
"shaman": "谐",
"warlock": "虚无",
"memory": "忆",
"memory": "忆",
"fire": "火",
"ice": "冰",
"imaginary": "幻象",
"imaginary": "虚数",
"physical": "物理",
"quantum": "量子",
"thunder": "雷",
@@ -39,14 +39,14 @@
"speed": "速度",
"critRate": "暴击率",
"critDmg": "暴击伤害",
"breakEffect": "破伤害",
"breakEffect": "破伤害",
"effectRes": "效果抗性",
"energyRegenerationRate": "能量恢复速率",
"effectHitRate": "效果命中率",
"outgoingHealingBoost": "治疗增强",
"fireDmgBoost": "火元素伤害增强",
"iceDmgBoost": "冰元素伤害增强",
"imaginaryDmgBoost": "幻象伤害增强",
"imaginaryDmgBoost": "虚数伤害增强",
"physicalDmgBoost": "物理伤害增强",
"quantumDmgBoost": "量子伤害增强",
"thunderDmgBoost": "雷元素伤害增强",
@@ -54,12 +54,12 @@
"pursued": "附加伤害",
"true damage": "真实伤害",
"follow-up": "后续伤害",
"elemental damage": "破甲与超破甲伤害",
"elemental damage": "击破与超击破伤害",
"dot": "持续伤害",
"qte": "QTE 技能",
"level": "等级",
"relics": "遗器",
"eidolons": "虚数形态",
"eidolons": "星魂",
"lightcones": "光锥",
"loadData": "加载数据",
"exportData": "导出数据",
@@ -172,6 +172,46 @@
"save": "保存",
"reset": "重置",
"roll": "滚动",
"step": "步数"
"step": "步数",
"memoryOfChaos": "混沌之忆",
"pureFiction": "虚构叙事",
"apocalypticShadow": "末日幻影",
"customEnemy": "自定义敌人",
"simulatedUniverse": "模拟宇宙",
"floor": "层数",
"side": "上/下半场",
"wave": "波次",
"stage": "关卡",
"useCycleCount": "使用轮次数?",
"useTurbulenceBuff": "使用紊乱增益?",
"firstHalfEnemies": "上半场敌人",
"secondHalfEnemies": "下半场敌人",
"turbulenceBuff": "紊乱增益",
"noEventSelected": "未选择事件",
"noTurbulenceBuff": "未选择紊乱增益",
"upper": "上半",
"lower": "下半",
"upperToLower": "上半 -> 下半",
"lowerToUpper": "下半 -> 上半",
"selectMOCEvent": "选择 MOC 事件",
"selectPFEvent": "选择 PF 事件",
"selectASEvent": "选择 AS 事件",
"selectCEEvent": "选择 CE 事件",
"selectEvent": "选择事件",
"selectFloor": "选择层数",
"selectSide": "选择上/下半场",
"selectBuff": "选择 buff",
"selectStage": "选择关卡",
"previous": "上一页",
"next": "下一页",
"noMonstersFound": "未找到怪物",
"addNewWave": "添加新波次",
"searchStage": "搜索关卡...",
"noStageFound": "未找到关卡",
"searchMonster": "搜索怪物...",
"changeRelic": "更换遗物",
"deleteRelic": "删除遗物",
"deleteRelicConfirm": "确定要删除插槽中的遗物吗",
"setEffects": "设置效果"
}
}

View File

@@ -172,7 +172,46 @@
"save": "Save",
"reset": "Reset",
"roll": "Roll",
"step": "Step"
"step": "Step",
"memoryOfChaos": "Memory of Chaos",
"pureFiction": "Pure Fiction",
"apocalypticShadow": "Apocalyptic Shadow",
"customEnemy": "Custom Enemy",
"simulatedUniverse": "Simulated Universe",
"floor": "Floor",
"side": "Side",
"wave": "Wave",
"stage": "Stage",
"useCycleCount": "Use cycle count?",
"useTurbulenceBuff": "Use turbulence buff?",
"firstHalfEnemies": "First half enemies",
"secondHalfEnemies": "Second half enemies",
"turbulenceBuff": "Turbulence Buff",
"noEventSelected": "No event selected",
"noTurbulenceBuff": "No Turbulence Buff",
"upper": "Upper",
"lower": "Lower",
"upperToLower": "Upper -> Lower",
"lowerToUpper": "Lower -> Upper",
"selectMOCEvent": "Select MOC Event",
"selectPFEvent": "Select PF Event",
"selectASEvent": "Select AS Event",
"selectCEEvent": "Select CE Event",
"selectEvent": "Select Event",
"selectFloor": "Select a Floor",
"selectSide": "Select a Side",
"selectBuff": "Select a Buff",
"selectStage": "Select a Stage",
"previous": "Previous",
"next": "Next",
"noMonstersFound": "No monsters found",
"addNewWave": "Add New Wave",
"searchStage": "Search stage...",
"noStageFound": "No stage found",
"searchMonster": "Search monster...",
"changeRelic": "Change relic",
"deleteRelic": "Delete relic",
"deleteRelicConfirm": "Are you sure you want to delete relic in slot",
"setEffects": "Set Effects"
}
}

View File

@@ -172,6 +172,46 @@
"save": "保存",
"reset": "リセット",
"roll": "ロール",
"step": "ステップ"
"step": "ステップ",
"memoryOfChaos": "忘却の庭",
"pureFiction": "虚構叙事",
"apocalypticShadow": "終末の幻影",
"customEnemy": "カスタム敵",
"simulatedUniverse": "模擬宇宙",
"floor": "階層",
"side": "前半/後半",
"wave": "ウェーブ",
"stage": "ステージ",
"useCycleCount": "サイクル数を使用しますか?",
"useTurbulenceBuff": "乱気流バフを使用しますか?",
"firstHalfEnemies": "前半の敵",
"secondHalfEnemies": "後半の敵",
"turbulenceBuff": "乱気流バフ",
"noEventSelected": "未選択のイベント",
"noTurbulenceBuff": "乱気流バフがありません",
"upper": "上半",
"lower": "下半",
"upperToLower": "上半 -> 下半",
"lowerToUpper": "下半 -> 上半",
"selectMOCEvent": "MOC イベントを選択",
"selectPFEvent": "PF イベントを選択",
"selectASEvent": "AS イベントを選択",
"selectCEEvent": "CE イベントを選択",
"selectEvent": "イベントを選択",
"selectFloor": "階層を選択",
"selectSide": "前半/後半を選択",
"selectBuff": "バフを選択",
"selectStage": "ステージを選択",
"previous": "前へ",
"next": "次へ",
"noMonstersFound": "敵が見つかりません",
"addNewWave": "新しいウェーブを追加",
"searchStage": "ステージを検索...",
"noStageFound": "ステージが見つかりません",
"searchMonster": "敵を検索...",
"changeRelic": "遺物を変更",
"deleteRelic": "遺物を削除",
"deleteRelicConfirm": "スロットの遺物を削除してもよろしいですか?",
"setEffects": "効果を設定"
}
}

View File

@@ -172,6 +172,46 @@
"save": "저장",
"reset": "초기화",
"roll": "굴리기",
"step": "단계"
"step": "단계",
"memoryOfChaos": "망각의 정원",
"pureFiction": "허구 이야기",
"apocalypticShadow": "종말의 환영",
"customEnemy": "커스텀 적",
"simulatedUniverse": "시뮬레이티드 유니버스",
"floor": "층수",
"side": "전반/후반",
"wave": "웨이브",
"stage": "스테이지",
"useCycleCount": "사이클 수 사용?",
"useTurbulenceBuff": "난류 버프 사용?",
"firstHalfEnemies": "전반 적",
"secondHalfEnemies": "후반 적",
"turbulenceBuff": "난류 버프",
"noEventSelected": "이벤트가 선택되지 않음",
"noTurbulenceBuff": "난류 버프가 없음",
"upper": "상반",
"lower": "하반",
"upperToLower": "상반 -> 하반",
"lowerToUpper": "하반 -> 상반",
"selectMOCEvent": "MOC 이벤트 선택",
"selectPFEvent": "PF 이벤트 선택",
"selectASEvent": "AS 이벤트 선택",
"selectCEEvent": "CE 이벤트 선택",
"selectEvent": "이벤트 선택",
"selectFloor": "층 선택",
"selectSide": "전반/후반 선택",
"selectBuff": "버프 선택",
"selectStage": "스테이지 선택",
"previous": "이전",
"next": "다음",
"noMonstersFound": "적을 찾을 수 없음",
"addNewWave": "새 웨이브 추가",
"searchStage": "스테이지 검색...",
"noStageFound": "스테이지를 찾을 수 없음",
"searchMonster": "적 검색...",
"changeRelic": "유물 변경",
"deleteRelic": "유물 삭제",
"deleteRelicConfirm": "이 슬롯의 유물을 삭제하시겠습니까?",
"setEffects": "효과 설정"
}
}

View File

@@ -172,6 +172,46 @@
"save": "Lưu",
"reset": "Đặt lại",
"roll": "Sô lượt",
"step": "Bước nhảy"
"step": "Bước nhảy",
"memoryOfChaos": "Hồi ức hỗn độn",
"pureFiction": "Kể chuyện hư cấu",
"apocalypticShadow": "Ảo ảnh tận thế",
"customEnemy": "Kẻ địch tùy chỉnh",
"simulatedUniverse": "Vũ trụ mô phỏng",
"floor": "Tầng",
"side": "Nửa trận",
"wave": "Đợt",
"stage": "Màn",
"useCycleCount": "Dùng dếm chu kỳ?",
"useTurbulenceBuff": "Dùng buff hỗn loạn?",
"firstHalfEnemies": "Địch nửa đầu",
"secondHalfEnemies": "Địch nửa sau",
"turbulenceBuff": "Buff hỗn loạn",
"noEventSelected": "Không có sự kiện",
"noTurbulenceBuff": "Không có buff hỗn loạn",
"upper": "Nửa trên",
"lower": "Nửa dưới",
"upperToLower": "Nửa trên -> Nửa dưới",
"lowerToUpper": "Nửa dưới -> Nửa trên",
"selectMOCEvent": "Chọn sự kiện MOC",
"selectPFEvent": "Chọn sự kiện PF",
"selectASEvent": "Chọn sự kiện AS",
"selectCEEvent": "Chọn sự kiện CE",
"selectEvent": "Chọn sự kiện",
"selectFloor": "Chọn tầng",
"selectSide": "Chọn nửa trận",
"selectBuff": "Chọn buff",
"selectStage": "Chọn màn",
"previous": "Trước",
"next": "Tiếp",
"noMonstersFound": "Không tìm thấy quái",
"addNewWave": "Thêm đợt mới",
"searchStage": "Tìm màn...",
"noStageFound": "Không tìm thấy màn",
"searchMonster": "Tìm quái...",
"changeRelic": "Thay đổi di vật",
"deleteRelic": "Xóa di vật",
"deleteRelicConfirm": "Bạn có chắc chắn muốn xóa di vật trong ô này không?",
"setEffects": "Thiết lập hiệu ứng"
}
}

View File

@@ -11,12 +11,12 @@
"path": "命运",
"rarity": "稀有度",
"element": "元素",
"technique": "秘",
"technique": "秘",
"talent": "天赋",
"basic": "普通攻击",
"skill": "技能",
"ultimate": "终技",
"servant": "仆从",
"ultimate": "终技",
"servant": "忆灵",
"damage": "伤害",
"type": "类型",
"warrior": "毁灭",
@@ -24,12 +24,12 @@
"mage": "博学",
"priest": "丰饶",
"rogue": "狩猎",
"shaman": "谐",
"shaman": "谐",
"warlock": "虚无",
"memory": "忆",
"memory": "忆",
"fire": "火",
"ice": "冰",
"imaginary": "幻象",
"imaginary": "虚数",
"physical": "物理",
"quantum": "量子",
"thunder": "雷",
@@ -39,14 +39,14 @@
"speed": "速度",
"critRate": "暴击率",
"critDmg": "暴击伤害",
"breakEffect": "破伤害",
"breakEffect": "破伤害",
"effectRes": "效果抗性",
"energyRegenerationRate": "能量恢复速率",
"effectHitRate": "效果命中率",
"outgoingHealingBoost": "治疗增强",
"fireDmgBoost": "火元素伤害增强",
"iceDmgBoost": "冰元素伤害增强",
"imaginaryDmgBoost": "幻象伤害增强",
"imaginaryDmgBoost": "虚数伤害增强",
"physicalDmgBoost": "物理伤害增强",
"quantumDmgBoost": "量子伤害增强",
"thunderDmgBoost": "雷元素伤害增强",
@@ -54,12 +54,12 @@
"pursued": "附加伤害",
"true damage": "真实伤害",
"follow-up": "后续伤害",
"elemental damage": "破甲与超破甲伤害",
"elemental damage": "击破与超击破伤害",
"dot": "持续伤害",
"qte": "QTE 技能",
"level": "等级",
"relics": "遗器",
"eidolons": "虚数形态",
"eidolons": "星魂",
"lightcones": "光锥",
"loadData": "加载数据",
"exportData": "导出数据",
@@ -172,6 +172,46 @@
"save": "保存",
"reset": "重置",
"roll": "滚动",
"step": "步数"
"step": "步数",
"memoryOfChaos": "混沌之忆",
"pureFiction": "虚构叙事",
"apocalypticShadow": "末日幻影",
"customEnemy": "自定义敌人",
"simulatedUniverse": "模拟宇宙",
"floor": "层数",
"side": "上/下半场",
"wave": "波次",
"stage": "关卡",
"useCycleCount": "使用轮次数?",
"useTurbulenceBuff": "使用紊乱增益?",
"firstHalfEnemies": "上半场敌人",
"secondHalfEnemies": "下半场敌人",
"turbulenceBuff": "紊乱增益",
"noEventSelected": "未选择事件",
"noTurbulenceBuff": "未选择紊乱增益",
"upper": "上半",
"lower": "下半",
"upperToLower": "上半 -> 下半",
"lowerToUpper": "下半 -> 上半",
"selectMOCEvent": "选择 MOC 事件",
"selectPFEvent": "选择 PF 事件",
"selectASEvent": "选择 AS 事件",
"selectCEEvent": "选择 CE 事件",
"selectEvent": "选择事件",
"selectFloor": "选择层数",
"selectSide": "选择上/下半场",
"selectBuff": "选择 buff",
"selectStage": "选择关卡",
"previous": "上一页",
"next": "下一页",
"noMonstersFound": "未找到怪物",
"addNewWave": "添加新波次",
"searchStage": "搜索关卡...",
"noStageFound": "未找到关卡",
"searchMonster": "搜索怪物...",
"changeRelic": "更换遗物",
"deleteRelic": "删除遗物",
"deleteRelicConfirm": "确定要删除插槽中的遗物吗",
"setEffects": "设置效果"
}
}

View File

@@ -9,8 +9,11 @@
"lint": "next lint"
},
"dependencies": {
"@tanstack/react-query": "^5.83.0",
"axios": "^1.10.0",
"framer-motion": "^12.12.1",
"lodash": "^4.17.21",
"lucide-react": "^0.525.0",
"next": "15.3.2",
"next-intl": "^4.1.0",
"react": "^19.0.0",
@@ -25,6 +28,7 @@
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4",
"@types/jest": "^29.5.14",
"@types/lodash": "^4.17.20",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
public/icon/knight.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
public/icon/mage.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
public/icon/memory.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
public/icon/priest.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

BIN
public/icon/rogue.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
public/icon/shaman.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
public/icon/warlock.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
public/icon/warrior.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -0,0 +1,18 @@
import { NextRequest, NextResponse } from 'next/server'
import { loadAS } from '@/lib/loader'
export async function GET(
req: NextRequest,
{ params }: { params: Promise<{ id: string, locale: string }> }
) {
const { id, locale } = await params
const asData = await loadAS([id], locale)
const as = asData[id]
if (!as) {
return NextResponse.json({ error: 'AS info not found' }, { status: 404 })
}
return NextResponse.json(as)
}

View File

@@ -0,0 +1,20 @@
import { loadAS } from "@/lib/loader";
import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest, { params }: { params: Promise<{ locale: string }> }) {
try {
const body = await request.json();
const asIds = body.asIds as string[];
const { locale } = await params;
if (!Array.isArray(asIds) || asIds.some(id => typeof id !== 'string')) {
return NextResponse.json({ error: 'Invalid asIds' }, { status: 400 });
}
const asData = await loadAS(asIds, locale);
return NextResponse.json(asData);
} catch {
return NextResponse.json({ error: 'Failed to load as data' }, { status: 500 });
}
}

View File

@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from 'next/server'
import { loadCharacters } from '@/lib/characterLoader'
import { loadCharacters } from '@/lib/loader'
export async function GET(
req: NextRequest,

View File

@@ -1,4 +1,4 @@
import { loadCharacters } from "@/lib/characterLoader";
import { loadCharacters } from "@/lib/loader";
import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest, { params }: { params: Promise<{ locale: string }> }) {

View File

@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from 'next/server'
import { loadLightcones } from '@/lib/lighconeLoader'
import { loadLightcones } from '@/lib/loader'
export async function GET(
req: NextRequest,

View File

@@ -1,4 +1,4 @@
import { loadLightcones } from "@/lib/lighconeLoader";
import { loadLightcones } from "@/lib/loader";
import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest, { params }: { params: Promise<{ locale: string }> }) {

View File

@@ -0,0 +1,18 @@
import { NextRequest, NextResponse } from 'next/server'
import { loadMOC } from '@/lib/loader'
export async function GET(
req: NextRequest,
{ params }: { params: Promise<{ id: string, locale: string }> }
) {
const { id, locale } = await params
const mocData = await loadMOC([id], locale)
const moc = mocData[id]
if (!moc) {
return NextResponse.json({ error: 'MOC info not found' }, { status: 404 })
}
return NextResponse.json(moc)
}

View File

@@ -0,0 +1,20 @@
import { loadMOC } from "@/lib/loader";
import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest, { params }: { params: Promise<{ locale: string }> }) {
try {
const body = await request.json();
const mocIds = body.mocIds as string[];
const { locale } = await params;
if (!Array.isArray(mocIds) || mocIds.some(id => typeof id !== 'string')) {
return NextResponse.json({ error: 'Invalid mocIds' }, { status: 400 });
}
const mocData = await loadMOC(mocIds, locale);
return NextResponse.json(mocData);
} catch {
return NextResponse.json({ error: 'Failed to load moc data' }, { status: 500 });
}
}

View File

@@ -0,0 +1,18 @@
import { NextRequest, NextResponse } from 'next/server'
import { loadPF } from '@/lib/loader'
export async function GET(
req: NextRequest,
{ params }: { params: Promise<{ id: string, locale: string }> }
) {
const { id, locale } = await params
const pfData = await loadPF([id], locale)
const pf = pfData[id]
if (!pf) {
return NextResponse.json({ error: 'PF info not found' }, { status: 404 })
}
return NextResponse.json(pf)
}

View File

@@ -0,0 +1,20 @@
import { loadPF } from "@/lib/loader";
import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest, { params }: { params: Promise<{ locale: string }> }) {
try {
const body = await request.json();
const pfIds = body.pfIds as string[];
const { locale } = await params;
if (!Array.isArray(pfIds) || pfIds.some(id => typeof id !== 'string')) {
return NextResponse.json({ error: 'Invalid pfIds' }, { status: 400 });
}
const pfData = await loadPF(pfIds, locale);
return NextResponse.json(pfData);
} catch {
return NextResponse.json({ error: 'Failed to load pf data' }, { status: 500 });
}
}

View File

@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from 'next/server'
import { loadRelics } from '@/lib/relicLoader'
import { loadRelics } from '@/lib/loader'
export async function GET(
req: NextRequest,

View File

@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from "next/server";
import { loadRelics } from "@/lib/relicLoader";
import { loadRelics } from "@/lib/loader";
export async function POST(request: NextRequest, { params }: { params: Promise<{ locale: string }> }) {
try {

View File

@@ -1,4 +1,4 @@
import { loadConfigMaze } from "@/lib/configMazeLoader";
import { loadConfigMaze } from "@/lib/loader";
import { NextResponse } from "next/server";
export async function GET() {

View File

@@ -1,4 +1,4 @@
import { loadMainAffix } from "@/lib/affixLoader";
import { loadMainAffix } from "@/lib/loader";
import { NextResponse } from "next/server";
export async function GET() {

View File

@@ -1,4 +1,4 @@
import { loadSubAffix } from "@/lib/affixLoader";
import { loadSubAffix } from "@/lib/loader";
import { NextResponse } from "next/server";
export async function GET() {

View File

@@ -9,6 +9,7 @@ import { getLocale, getMessages } from "next-intl/server";
import { ToastContainer } from 'react-toastify';
import AvatarBar from "@/components/avatarBar";
import ActionBar from "@/components/actionBar";
import QueryProviderWrapper from "@/components/queryProvider";
const geistSans = Geist({
variable: "--font-geist-sans",
@@ -39,25 +40,27 @@ export default async function RootLayout({
suppressHydrationWarning
>
<NextIntlClientProvider messages={messages}>
<ThemeProvider>
<ClientThemeWrapper>
<div className="min-h-screen w-full">
<Header />
<div className="grid grid-cols-12 w-full">
<div className="col-span-3 sticky top-0 self-start h-fit">
<AvatarBar />
</div>
<div className="col-span-9">
<ActionBar />
{children}
<QueryProviderWrapper>
<ThemeProvider>
<ClientThemeWrapper>
<div className="min-h-screen w-full">
<Header />
<div className="grid grid-cols-12 w-full">
<div className="col-span-3 sticky top-0 self-start h-fit">
<AvatarBar />
</div>
<div className="col-span-9">
<ActionBar />
{children}
</div>
</div>
<Footer />
</div>
<Footer />
</div>
</ClientThemeWrapper>
</ThemeProvider>
</ClientThemeWrapper>
</ThemeProvider>
</QueryProviderWrapper>
</NextIntlClientProvider>
<ToastContainer />
</body>

View File

@@ -24,7 +24,7 @@ export default function ActionBar() {
const { avatarSelected, listRawAvatar } = useListAvatarStore()
const { setListCopyAvatar } = useCopyProfileStore()
const transI18n = useTranslations("DataPage")
const { locale, setLocale } = useLocaleStore()
const { locale } = useLocaleStore()
const { isOpenCreateProfile, setIsOpenCreateProfile, isOpenCopy, setIsOpenCopy } = useModelStore()
const { avatars, setAvatar } = useUserDataStore()
const [profileName, setProfileName] = useState("");
@@ -106,7 +106,7 @@ export default function ActionBar() {
window.addEventListener('keydown', handleEscKey);
return () => window.removeEventListener('keydown', handleEscKey);
}, [isOpenCreateProfile]);
}, [isOpenCopy, isOpenCreateProfile]);
const actionMove = (path: string) => {
router.push(`/${path}`)
@@ -162,7 +162,7 @@ export default function ActionBar() {
{avatarSelected && (
<>
<Image
src={"https://api.hakush.in/hsr/UI/element/" + avatarSelected.damageType.toLowerCase() + ".webp"}
src={ `/icon/${avatarSelected.damageType.toLowerCase()}.webp`}
alt={'fire'}
className="h-[40px] w-[40px] object-contain"
width={100}
@@ -209,7 +209,7 @@ export default function ActionBar() {
<li key={index} className="grid grid-cols-12">
<button
className={`col-span-8 btn btn-ghost`}
onClick={(e) => handleProfileSelect(index)}
onClick={() => handleProfileSelect(index)}
>
<span className="flex-1 truncate text-left">
{profile.profile_name}

View File

@@ -1,91 +1,31 @@
"use client"
import { getCharacterListApi, fetchCharactersByIdsNative, getConfigMazeApi, getLightconeListApi, fetchLightconesByIdsNative, fetchRelicsByIdsNative, getRelicSetListApi, getMainAffixApi, getSubAffixApi } from "@/lib/api"
import Image from "next/image"
import { useEffect, useState } from "react"
import CharacterCard from "../card/characterCard"
import useLocaleStore from "@/stores/localeStore"
import { listCurrentLanguageApi } from "@/lib/constant"
import useAvatarStore from "@/stores/avatarStore"
import useUserDataStore from "@/stores/userDataStore"
import { converterToAvatarStore, getAvatarNotExist } from "@/helper"
import useLightconeStore from "@/stores/lightconeStore"
import useRelicStore from "@/stores/relicStore"
import useAffixStore from "@/stores/affixStore"
import useMazeStore from "@/stores/mazeStore"
import { useTranslations } from "next-intl"
import { useFetchASData, useFetchAvatarData, useFetchConfigData, useFetchLightconeData, useFetchMOCData, useFetchMonsterData, useFetchPFData, useFetchRelicData } from "@/hooks"
export default function AvatarBar() {
const [listElement, setListElement] = useState<Record<string, boolean>>({ "fire": false, "ice": false, "imaginary": false, "physical": false, "quantum": false, "thunder": false, "wind": false })
const [listPath, setListPath] = useState<Record<string, boolean>>({ "knight": false, "mage": false, "priest": false, "rogue": false, "shaman": false, "warlock": false, "warrior": false, "memory": false })
const { listAvatar, setListAvatar, setAvatarSelected, setFilter, filter, setAllMapAvatarInfo, avatarSelected } = useAvatarStore()
const { setAvatars, avatars } = useUserDataStore()
const { listAvatar, setAvatarSelected, setFilter, filter } = useAvatarStore()
const transI18n = useTranslations("DataPage")
const { locale } = useLocaleStore()
const { setListLightcone, setAllMapLightconeInfo } = useLightconeStore()
const { setListRelic, setAllMapRelicInfo } = useRelicStore()
const { setMapMainAffix, setMapSubAffix } = useAffixStore()
const { setAllData } = useMazeStore()
useEffect(() => {
const fetchData = async () => {
// Maze
const maze = await getConfigMazeApi()
setAllData(maze)
// Affix
const mapMainAffix = await getMainAffixApi()
setMapMainAffix(mapMainAffix)
const mapSubAffix = await getSubAffixApi()
setMapSubAffix(mapSubAffix)
}
fetchData()
}, [])
useEffect(() => {
const fetchData = async () => {
// Avatar
const listAvatar = await getCharacterListApi()
listAvatar.sort((a, b) => {
const aHasRelease = typeof a.release === 'number';
const bHasRelease = typeof b.release === 'number';
if (!aHasRelease && !bHasRelease) return 0;
if (!aHasRelease) return -1;
if (!bHasRelease) return 1;
return b.release! - a.release!;
});
const mapAvatar = await fetchCharactersByIdsNative(listAvatar.map((item) => item.id), listCurrentLanguageApi[locale.toLowerCase()])
setListAvatar(listAvatar)
setAllMapAvatarInfo(mapAvatar)
const avatarStore = converterToAvatarStore(getAvatarNotExist())
if (Object.keys(avatarStore).length > 0) {
setAvatars({ ...avatars, ...avatarStore })
}
if (!avatarSelected) {
setAvatarSelected(listAvatar[0])
}
// Lightcone
const listLightcone = await getLightconeListApi()
listLightcone.sort((a, b) => Number(b.id) - Number(a.id))
setListLightcone(listLightcone)
const mapLightcone = await fetchLightconesByIdsNative(listLightcone.map((item) => item.id), listCurrentLanguageApi[locale.toLowerCase()])
setAllMapLightconeInfo(mapLightcone)
// Relic
const listRelic = await getRelicSetListApi()
setListRelic(listRelic)
const mapRelic = await fetchRelicsByIdsNative(listRelic.map((item) => item.id), listCurrentLanguageApi[locale.toLowerCase()])
setAllMapRelicInfo(mapRelic)
}
fetchData()
}, [locale])
useFetchConfigData()
useFetchAvatarData()
useFetchLightconeData()
useFetchRelicData()
useFetchMonsterData()
useFetchPFData()
useFetchMOCData()
useFetchASData()
useEffect(() => {
setFilter({ ...filter, locale: locale, element: Object.keys(listElement).filter((key) => listElement[key]), path: Object.keys(listPath).filter((key) => listPath[key]) })
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [locale, listElement, listPath])
return (
@@ -103,7 +43,7 @@ export default function AvatarBar() {
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-4 lg:grid-cols-7 mb-1 mx-1 gap-2 w-full max-h-[17vh] min-h-[5vh] overflow-y-auto">
{Object.entries(listElement).map(([key, value], index) => (
{Object.keys(listElement).map((key, index) => (
<div
key={index}
onClick={() => {
@@ -113,7 +53,7 @@ export default function AvatarBar() {
style={{
backgroundColor: listElement[key] ? "#374151" : "#6B7280"
}}>
<Image src={"https://api.hakush.in/hsr/UI/element/" + key + ".webp"}
<Image src={ `/icon/${key}.webp`}
alt={key}
className="h-[28px] w-[28px] 2xl:h-[40px] 2xl:w-[40px] object-contain rounded-md"
width={200}
@@ -123,7 +63,7 @@ export default function AvatarBar() {
</div>
<div className="grid grid-cols-1 md:grid-cols-4 lg:grid-cols-8 mb-1 mx-1 gap-2 overflow-y-auto w-full max-h-[17vh] min-h-[5vh]">
{Object.entries(listPath).map(([key, value], index) => (
{Object.keys(listPath).map((key, index) => (
<div
key={index}
onClick={() => {
@@ -135,7 +75,7 @@ export default function AvatarBar() {
}}
>
<Image src={"https://api.hakush.in/hsr/UI/pathicon/" + key + ".webp"}
<Image src={`/icon/${key}.webp`}
alt={key}
className="h-[28px] w-[28px] 2xl:h-[40px] 2xl:w-[40px] object-contain rounded-md"
width={200}

View File

@@ -1,9 +1,8 @@
"use client"
import { useRouter } from 'next/navigation'
import useAvatarStore from "@/stores/avatarStore"
import useUserDataStore from "@/stores/userDataStore";
import { useEffect, useMemo, useState } from "react";
import { useEffect, useMemo } from "react";
import { motion } from "framer-motion";
import LightconeBar from '../lightconeBar'
import useLightconeStore from '@/stores/lightconeStore'
@@ -16,7 +15,6 @@ import useModelStore from '@/stores/modelStore';
import useMazeStore from '@/stores/mazeStore';
export default function AvatarInfo() {
const router = useRouter()
const { avatarSelected, mapAvatarInfo } = useAvatarStore()
const { Technique } = useMazeStore()
const { avatars, setAvatars, setAvatar } = useUserDataStore()
@@ -252,7 +250,7 @@ export default function AvatarInfo() {
className="select select-bordered select-secondary"
>
<option value="">{transI18n("origin")}</option>
{Object.entries(mapAvatarInfo[avatarSelected?.id || ""]?.Enhanced || {}).map(([key, value]) => (
{Object.keys(mapAvatarInfo[avatarSelected?.id || ""]?.Enhanced || {}).map((key) => (
<option key={key} value={key}>
{key}
</option>

View File

@@ -32,6 +32,7 @@ export default function CharacterCard({ data }: CharacterCardProps) {
width={376}
height={512}
src={`https://api.hakush.in/hsr/UI/avatarshopicon/${data.id}.webp`}
priority={true}
className="w-full h-full rounded-md object-cover"
alt="ALT"
/>
@@ -39,14 +40,14 @@ export default function CharacterCard({ data }: CharacterCardProps) {
width={32}
height={32}
src={`https://api.hakush.in/hsr/UI/element/${data.damageType.toLowerCase()}.webp`}
src={`/icon/${data.damageType.toLowerCase()}.webp`}
className="absolute top-0 left-0 w-6 h-6 rounded-full"
alt={data.damageType.toLowerCase()}
/>
<Image
width={32}
height={32}
src={`https://api.hakush.in/hsr/UI/pathicon/${data.baseType.toLowerCase()}.webp`}
src={`/icon/${data.baseType.toLowerCase()}.webp`}
className="absolute top-0 right-0 w-6 h-6 rounded-full"
alt={data.baseType.toLowerCase()}
style={{

View File

@@ -1,11 +1,10 @@
"use client";
import React, { useState, useMemo } from 'react';
import React from 'react';
import { CharacterInfoCardType } from '@/types';
import { getNameChar, replaceByParam } from '@/helper';
import useLocaleStore from '@/stores/localeStore';
import useAvatarStore from '@/stores/avatarStore';
import useLightconeStore from '@/stores/lightconeStore';
import useRelicStore from '@/stores/relicStore';
import Image from 'next/image';
import ParseText from '../parseText';
@@ -14,7 +13,6 @@ export default function CharacterInfoCard({ character, selectedCharacters, onCha
const isSelected = selectedCharacters.some((selectedCharacter) => selectedCharacter.avatar_id === character.avatar_id);
const { mapAvatarInfo } = useAvatarStore();
const { mapLightconeInfo } = useLightconeStore();
const { mapRelicInfo } = useRelicStore();
const { locale } = useLocaleStore();
return (
@@ -40,14 +38,14 @@ export default function CharacterInfoCard({ character, selectedCharacters, onCha
<Image
width={48}
height={48}
src={`https://api.hakush.in/hsr/UI/element/${mapAvatarInfo[character.avatar_id.toString()]?.DamageType.toLowerCase()}.webp`}
src={`/icon/${mapAvatarInfo[character.avatar_id.toString()]?.DamageType.toLowerCase()}.webp`}
className="absolute top-0 left-0 w-10 h-10 rounded-full"
alt={mapAvatarInfo[character.avatar_id.toString()]?.DamageType.toLowerCase()}
/>
<Image
width={48}
height={48}
src={`https://api.hakush.in/hsr/UI/pathicon/${mapAvatarInfo[character.avatar_id.toString()]?.BaseType.toLowerCase()}.webp`}
src={`/icon/${mapAvatarInfo[character.avatar_id.toString()]?.BaseType.toLowerCase()}.webp`}
className="absolute top-0 right-0 w-10 h-10 rounded-full"
alt={mapAvatarInfo[character.avatar_id.toString()]?.BaseType.toLowerCase()}
style={{

View File

@@ -1,6 +1,6 @@
"use client";
import { getNameLightcone } from '@/helper';
import { getLocaleName } from '@/helper';
import useLocaleStore from '@/stores/localeStore';
import { LightConeBasic } from '@/types';
import ParseText from '../parseText';
@@ -12,7 +12,7 @@ interface LightconeCardProps {
export default function LightconeCard({ data }: LightconeCardProps) {
const { locale } = useLocaleStore();
const text = getNameLightcone(locale, data)
const text = getLocaleName(locale, data)
return (
<li className="z-10 flex flex-col items-center rounded-md shadow-lg
bg-gradient-to-b from-customStart to-customEnd transform transition-transform duration-300

View File

@@ -1,10 +1,9 @@
"use client";
import React from 'react';
import { AvatarProfileCardType } from '@/types';
import useLocaleStore from '@/stores/localeStore';
import useAvatarStore from '@/stores/avatarStore';
import useLightconeStore from '@/stores/lightconeStore';
import useRelicStore from '@/stores/relicStore';
import Image from 'next/image';
import ParseText from '../parseText';
@@ -50,7 +49,7 @@ export default function ProfileCard({ profile, selectedProfile, onProfileToggle
{Object.keys(profile.relics).length > 0 && (
<div className="flex flex-wrap items-center justify-center gap-2 mb-4">
{Object.entries(profile.relics).map(([key, relic], index) => (
{Object.values(profile.relics).map((relic, index) => (
<div key={index} className="relative">
<div className="w-9 h-9 rounded-lg flex items-center justify-center border border-amber-500/50">
<Image

View File

@@ -1,4 +1,5 @@
import useLocaleStore from "@/stores/localeStore";
"use client";
import useUserDataStore from "@/stores/userDataStore";
import Image from "next/image";
import { useMemo } from "react";
@@ -26,31 +27,31 @@ const getRarityName = (slot: string) => {
);
case '2': return (
<div className="flex items-center gap-1">
<Image src="/relics/HAND.png" alt="Head" width={20} height={20} />
<Image src="/relics/HAND.png" alt="Hand" width={20} height={20} />
<h2>Hands</h2>
</div>
);
case '3': return (
<div className="flex items-center gap-1">
<Image src="/relics/BODY.png" alt="Head" width={20} height={20} />
<Image src="/relics/BODY.png" alt="Body" width={20} height={20} />
<h2>Body</h2>
</div>
);
case '4': return (
<div className="flex items-center gap-1">
<Image src="/relics/FOOT.png" alt="Head" width={20} height={20} />
<Image src="/relics/FOOT.png" alt="Foot" width={20} height={20} />
<h2>Feet</h2>
</div>
);
case '5': return (
<div className="flex items-center gap-1">
<Image src="/relics/OBJECT.png" alt="Head" width={20} height={20} />
<Image src="/relics/NECK.png" alt="Neck" width={20} height={20} />
<h2>Planar sphere</h2>
</div>
);
case '6': return (
<div className="flex items-center gap-1">
<Image src="/relics/NECK.png" alt="Head" width={20} height={20} />
<Image src="/relics/OBJECT.png" alt="Object" width={20} height={20} />
<h2>Link rope</h2>
</div>
);
@@ -58,7 +59,6 @@ const getRarityName = (slot: string) => {
}
};
export default function RelicCard({ slot, avatarId }: RelicCardProps) {
const { locale } = useLocaleStore();
const { avatars } = useUserDataStore()
const relicDetail = useMemo(() => {

View File

@@ -1,4 +1,5 @@
"use client";
import React, { useState, useRef} from 'react';
export default function ShowCaseInfo() {

View File

@@ -1,3 +1,4 @@
/* eslint-disable react-hooks/exhaustive-deps */
"use client"
import { replaceByParam } from "@/helper";
import useListAvatarStore from "@/stores/avatarStore";
@@ -30,8 +31,8 @@ export default function EidolonsInfo() {
className="flex flex-col items-center cursor-pointer hover:scale-105"
onClick={() => {
let newRank = Number(key)
if (Number(key) == 1 && avatars[avatarSelected?.id || ""]?.data?.rank == 1) {
newRank = 0
if (avatars[avatarSelected?.id || ""]?.data?.rank == Number(key)) {
newRank = Number(key) - 1
}
setAvatars({ ...avatars, [avatarSelected?.id || ""]: { ...avatars[avatarSelected?.id || ""], data: { ...avatars[avatarSelected?.id || ""].data, rank: newRank } } })
}}

View File

@@ -2,7 +2,6 @@
import { connectToPS, downloadJson, syncDataToPS } from "@/helper";
import { converterToFreeSRJson } from "@/helper/converterToFreeSRJson";
import { useChangeTheme } from "@/hooks/useChangeTheme";
import { listCurrentLanguage } from "@/lib/constant";
import useLocaleStore from "@/stores/localeStore";
import useUserDataStore from "@/stores/userDataStore";
@@ -18,6 +17,7 @@ import { toast } from "react-toastify";
import { micsSchema } from "@/zod";
import useConnectStore from "@/stores/connectStore";
import useGlobalStore from "@/stores/globalStore";
import MonsterBar from "../monsterBar";
const themes = [
{ label: "Winter" },
@@ -29,15 +29,34 @@ const themes = [
export default function Header() {
const { changeTheme } = useChangeTheme()
const { locale, setLocale } = useLocaleStore()
const { avatars, battle_config, setAvatars, setBattleConfig } = useUserDataStore()
const {
avatars,
battle_type,
setAvatars,
setBattleType,
moc_config,
pf_config,
as_config,
ce_config,
setMocConfig,
setPfConfig,
setAsConfig,
setCeConfig,
} = useUserDataStore()
const router = useRouter()
const transI18n = useTranslations("DataPage")
const { setIsOpenImport, isOpenImport } = useModelStore()
const {
setIsOpenImport,
isOpenImport,
setIsOpenMonster,
isOpenMonster,
setIsOpenConnect,
isOpenConnect
} = useModelStore()
const [message, setMessage] = useState({ text: '', type: '' });
const [importModal, setImportModal] = useState("enka");
const [isModalOpen, setIsModalOpen] = useState(false);
const {
connectionType,
privateType,
@@ -84,17 +103,14 @@ export default function Header() {
}
const handleShow = (modalId: string) => {
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
if (modal) {
setIsModalOpen(true);
modal.showModal();
}
};
// Close modal handler
const handleCloseModal = (modalId: string) => {
setIsModalOpen(false);
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
if (modal) {
modal.close()
@@ -107,16 +123,25 @@ export default function Header() {
handleCloseModal("import_modal");
return;
}
if (!isOpenMonster) {
handleCloseModal("monster_modal");
return;
}
if (!isOpenConnect) {
handleCloseModal("connect_modal");
return;
}
const handleEscKey = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
handleCloseModal("connect_modal");
handleCloseModal("import_modal");
handleCloseModal("monster_modal");
}
};
window.addEventListener('keydown', handleEscKey);
return () => window.removeEventListener('keydown', handleEscKey);
}, [isModalOpen, isOpenImport]);
}, [isOpenImport, isOpenMonster, isOpenConnect]);
const handleImportDatabase = (event: React.ChangeEvent<HTMLInputElement>) => {
@@ -139,9 +164,13 @@ export default function Header() {
const data = JSON.parse(e.target?.result as string);
const parsed = micsSchema.parse(data)
setAvatars(parsed.avatars)
setBattleConfig(parsed.battle_config)
setBattleType(parsed.battle_type)
setMocConfig(parsed.moc_config)
setPfConfig(parsed.pf_config)
setAsConfig(parsed.as_config)
setCeConfig(parsed.ce_config)
toast.success(transI18n("importDatabaseSuccess"))
} catch (error) {
} catch {
toast.error(transI18n("fileMustBeAValidJsonFile"))
}
};
@@ -201,25 +230,31 @@ export default function Header() {
<details>
<summary className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium">{transI18n("exportData")}</summary>
<ul className="p-2">
<li><a onClick={() => downloadJson("freesr-data", converterToFreeSRJson(avatars, battle_config))}>{transI18n("freeSr")}</a></li>
<li><a onClick={() => downloadJson("database-data", { avatars: avatars, battle_config: battle_config })}>{transI18n("database")}</a></li>
<li><a onClick={() => downloadJson("freesr-data", converterToFreeSRJson(avatars, battle_type, moc_config, pf_config, as_config, ce_config))}>{transI18n("freeSr")}</a></li>
<li><a onClick={() => downloadJson("database-data", { avatars: avatars, battle_type: battle_type, moc_config: moc_config, pf_config: pf_config, as_config: as_config, ce_config: ce_config })}>{transI18n("database")}</a></li>
</ul>
</details>
</li>
<li>
<button
className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium"
onClick={() => handleShow("connection_modal")}
onClick={() => {
setIsOpenConnect(true)
handleShow("connect_modal")
}}
>
{transI18n("connectSetting")}
</button>
</li>
<li>
<button
disabled
onClick={() => {
setIsOpenMonster(true)
handleShow("monster_modal")
}}
className="disabled px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium"
>
{transI18n("monsterSetting")} ({transI18n("comingSoon")})
{transI18n("monsterSetting")}
</button>
</li>
</ul>
@@ -279,25 +314,31 @@ export default function Header() {
<details>
<summary className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium">{transI18n("exportData")}</summary>
<ul className="p-2">
<li><a onClick={() => downloadJson("freesr-data", converterToFreeSRJson(avatars, battle_config))}>{transI18n("freeSr")}</a></li>
<li><a onClick={() => downloadJson("database-data", { avatars: avatars, battle_config: battle_config })}>{transI18n("database")}</a></li>
<li><a onClick={() => downloadJson("freesr-data", converterToFreeSRJson(avatars, battle_type, moc_config, pf_config, as_config, ce_config))}>{transI18n("freeSr")}</a></li>
<li><a onClick={() => downloadJson("database-data", { avatars: avatars, battle_type: battle_type, moc_config: moc_config, pf_config: pf_config, as_config: as_config, ce_config: ce_config })}>{transI18n("database")}</a></li>
</ul>
</details>
</li>
<li>
<button
className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium"
onClick={() => handleShow("connection_modal")}
onClick={() => {
setIsOpenConnect(true)
handleShow("connect_modal")
}}
>
{transI18n("connectSetting")}
</button>
</li>
<li>
<button
disabled
className="disabled:opacity-50 px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium"
onClick={() => {
setIsOpenMonster(true)
handleShow("monster_modal")
}}
className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium"
>
{transI18n("monsterSetting")} ({transI18n("comingSoon")})
{transI18n("monsterSetting")}
</button>
</li>
</ul>
@@ -409,14 +450,17 @@ export default function Header() {
</Link>
</div>
<dialog id="connection_modal" className="modal sm:modal-middle backdrop-blur-sm">
<dialog id="connect_modal" className="modal sm:modal-middle backdrop-blur-sm">
<div className="modal-box w-11/12 max-w-7xl bg-base-100 text-base-content border border-purple-500/50 shadow-lg shadow-purple-500/20">
<div className="sticky top-0 z-10">
<motion.button
whileHover={{ scale: 1.1, rotate: 90 }}
transition={{ duration: 0.2 }}
className="btn btn-circle btn-md absolute right-2 top-2 bg-red-600 hover:bg-red-700 text-white border-none"
onClick={() => handleCloseModal("connection_modal")}
onClick={() => {
setIsOpenConnect(false)
handleCloseModal("connect_modal")
}}
>
</motion.button>
@@ -597,6 +641,32 @@ export default function Header() {
{importModal === "freesr" && <FreeSRImport />}
</div>
</dialog>
<dialog id="monster_modal" className="modal sm:backdrop-blur-sm md:backdrop-blur-none">
<div className="modal-box w-11/12 max-w-max bg-base-100 text-base-content border border-purple-500/50 shadow-lg shadow-purple-500/20">
<div className="sticky top-0 z-10">
<motion.button
whileHover={{ scale: 1.1, rotate: 90 }}
transition={{ duration: 0.2 }}
className="btn btn-circle btn-md absolute right-2 top-2 bg-red-600 hover:bg-red-700 text-white border-none"
onClick={() => {
handleCloseModal("monster_modal")
setIsOpenMonster(false)
}}
>
</motion.button>
</div>
<div className="border-b border-purple-500/30 px-6 py-4 mb-4">
<h3 className="font-bold text-2xl text-transparent bg-clip-text bg-gradient-to-r from-pink-400 to-cyan-400">
{transI18n("monsterSetting")}
</h3>
</div>
<MonsterBar />
</div>
</dialog>
</div>
)
}

View File

@@ -1,16 +1,15 @@
"use client"
import useModelStore from "@/stores/modelStore";
import useUserDataStore from "@/stores/userDataStore";
import { useEffect, useState } from "react";
import useCopyProfileStore from "@/stores/copyProfile";
import ProfileCard from "../card/profileCard";
import { AvatarProfileCardType, AvatarProfileStore } from "@/types";
import Image from "next/image";
import SelectCustom from "../select";
import useListAvatarStore from "@/stores/avatarStore";
import { getNameChar } from "@/helper";
import useLocaleStore from "@/stores/localeStore";
import { useTranslations } from "next-intl";
import SelectCustomImage from "../select/customSelectImage";
export default function CopyImport() {
const { avatars, setAvatar } = useUserDataStore();
@@ -50,6 +49,7 @@ export default function CopyImport() {
element: Object.keys(listElement).filter((key) => listElement[key]),
rarity: Object.keys(listRank).filter((key) => listRank[key])
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [listPath, listRank, locale])
const clearSelection = () => {
@@ -101,7 +101,7 @@ export default function CopyImport() {
return;
}
const newListProfile = avatars[avatarCopySelected.id.toString()].profileList.map((profile, index) => {
const newListProfile = avatars[avatarCopySelected.id.toString()].profileList.map((profile) => {
if (!profile.lightcone?.item_id && Object.keys(profile.relics).length == 0) {
return null;
}
@@ -149,7 +149,7 @@ export default function CopyImport() {
backgroundColor: listPath[key] ? "#374151" : "#6B7280"
}}>
<Image
src={`https://api.hakush.in/hsr/UI/pathicon/${key}.webp`}
src={`/icon/${key}.webp`}
alt={key}
className="h-[32px] w-[32px] object-contain rounded-md"
width={200}
@@ -174,7 +174,7 @@ export default function CopyImport() {
backgroundColor: listElement[key] ? "#374151" : "#6B7280"
}}>
<Image
src={`https://api.hakush.in/hsr/UI/element/${key}.webp`}
src={`/icon/${key}.webp`}
alt={key}
className="h-[28px] w-[28px] 2xl:h-[40px] 2xl:w-[40px] object-contain rounded-md"
width={200}
@@ -210,7 +210,7 @@ export default function CopyImport() {
<div>
<div>{transI18n("characterName")}</div>
{listCopyAvatar.length > 0 && (
<SelectCustom
<SelectCustomImage
customSet={listCopyAvatar.map((avatar) => ({
value: avatar.id.toString(),
label: getNameChar(locale, avatar),

View File

@@ -2,7 +2,7 @@
import { useState } from "react";
import CharacterInfoCard from "../card/characterInfoCard";
import useEnkaStore from "@/stores/enkaStore";
import { SendDataThroughProxy } from "@/lib/api";
import { SendDataThroughProxy } from "@/lib/api/api";
import { CharacterInfoCardType, EnkaResponse } from "@/types";
import useUserDataStore from "@/stores/userDataStore";
import { converterOneEnkaDataToAvatarStore } from "@/helper";
@@ -21,7 +21,7 @@ export default function EnkaImport() {
} = useEnkaStore();
const transI18n = useTranslations("DataPage")
const { avatars, setAvatar } = useUserDataStore();
const { isOpenImport, setIsOpenImport } = useModelStore()
const { setIsOpenImport } = useModelStore()
const [isLoading, setIsLoading] = useState(false)
const [Error, setError] = useState("")
@@ -191,7 +191,7 @@ export default function EnkaImport() {
)}
{/* Character Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{enkaData?.detailInfo.avatarDetailList.map((character, index) => (
{enkaData?.detailInfo.avatarDetailList.map((character) => (
<CharacterInfoCard
key={character.avatarId}
character={{

View File

@@ -12,8 +12,8 @@ import { converterOneFreeSRDataToAvatarStore } from "@/helper";
import { useTranslations } from "next-intl";
export default function FreeSRImport() {
const { avatars, setAvatar, setBattleConfig } = useUserDataStore();
const { isOpenImport, setIsOpenImport } = useModelStore()
const { avatars, setAvatar } = useUserDataStore();
const { setIsOpenImport } = useModelStore()
const [isLoading, setIsLoading] = useState(false)
const [Error, setError] = useState("")
const { freeSRData, setFreeSRData, selectedCharacters, setSelectedCharacters } = useFreeSRStore()
@@ -33,7 +33,7 @@ export default function FreeSRImport() {
const selectAll = () => {
if (freeSRData) {
setSelectedCharacters(Object.entries(freeSRData?.avatars).map(([key, character]) => {
setSelectedCharacters(Object.values(freeSRData?.avatars).map((character) => {
const lightcone = freeSRData.lightcones.find((lightcone) => lightcone.equip_avatar === character.avatar_id)
const relics = freeSRData.relics.filter((relic) => relic.equip_avatar === character.avatar_id)
return {
@@ -84,7 +84,7 @@ export default function FreeSRImport() {
setFreeSRData(parsed)
setError("")
setSelectedCharacters(Object.entries(parsed?.avatars || {}).map(([key, character]) => {
setSelectedCharacters(Object.values(parsed?.avatars || {}).map((character) => {
const lightcone = parsed?.lightcones.find((lightcone) => lightcone.equip_avatar === character.avatar_id)
const relics = parsed?.relics.filter((relic) => relic.equip_avatar === character.avatar_id)
return {
@@ -104,7 +104,7 @@ export default function FreeSRImport() {
})) ?? [],
} as CharacterInfoCardType
}));
} catch (error) {
} catch {
setSelectedCharacters([])
setFreeSRData(null)
setError(transI18n("fileIsNotAValidFreeSRJsonFile"))
@@ -128,8 +128,8 @@ export default function FreeSRImport() {
setError("");
const listAvatars = { ...avatars }
const filterData = Object.entries(freeSRData?.avatars || {}).filter(([key, character]) => selectedCharacters.some((selectedCharacter) => selectedCharacter.avatar_id === character.avatar_id))
filterData.forEach(([key, character]) => {
const filterData = Object.values(freeSRData?.avatars || {}).filter((character) => selectedCharacters.some((selectedCharacter) => selectedCharacter.avatar_id === character.avatar_id))
filterData.forEach((character) => {
const newAvatar = { ...listAvatars[character.avatar_id] }
if (Object.keys(newAvatar).length !== 0) {
newAvatar.level = character.level
@@ -147,7 +147,6 @@ export default function FreeSRImport() {
}
})
setBattleConfig(freeSRData?.battle_config)
setIsOpenImport(false)
toast.success(transI18n("importFreeSRDataSuccess"))
}
@@ -200,7 +199,7 @@ export default function FreeSRImport() {
)}
{/* Character Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{Object.entries(freeSRData?.avatars || {}).map(([key, character]) => {
{Object.values(freeSRData?.avatars || {}).map((character) => {
const lightcone = freeSRData?.lightcones.find((lightcone) => lightcone.equip_avatar === character.avatar_id)
const relics = freeSRData?.relics.filter((relic) => relic.equip_avatar === character.avatar_id)
return (

View File

@@ -26,6 +26,7 @@ export default function LightconeBar() {
path: Object.keys(listPath).filter((key) => listPath[key]),
rarity: Object.keys(listRank).filter((key) => listRank[key])
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [listPath, listRank, locale])
return (
@@ -49,7 +50,7 @@ export default function LightconeBar() {
<div className="grid grid-cols-12 mt-1 w-full">
<div className="grid justify-items-start col-span-8">
<div className="grid grid-cols-3 md:grid-cols-6 lg:grid-cols-8 mb-1 mx-1 gap-2">
{Object.entries(listPath).map(([key, value], index) => (
{Object.keys(listPath).map((key, index) => (
<div
key={index}
onClick={() => {
@@ -59,8 +60,8 @@ export default function LightconeBar() {
style={{
backgroundColor: listPath[key] ? "#374151" : "#6B7280"
}}>
<Image src={"https://api.hakush.in/hsr/UI/pathicon/" + key + ".webp"}
alt={key}
<Image src={`/icon/${key}.webp`}
alt={key}
className="h-[32px] w-[32px] object-contain rounded-md "
width={200}
height={200} />
@@ -71,7 +72,7 @@ export default function LightconeBar() {
<div className="grid justify-items-end col-span-4 w-full">
<div className="grid grid-cols-1 sm:grid-cols-3 mb-1 mx-1 gap-2">
{Object.entries(listRank).map(([key, value], index) => (
{Object.keys(listRank).map((key, index) => (
<div
key={index}
onClick={() => {

View File

@@ -0,0 +1,338 @@
"use client"
import { useEffect, useMemo } from "react";
import SelectCustomText from "../select/customSelectText";
import useEventStore from "@/stores/eventStore";
import { getLocaleName, replaceByParam } from "@/helper";
import useLocaleStore from "@/stores/localeStore";
import useUserDataStore from "@/stores/userDataStore";
import useMonsterStore from "@/stores/monsterStore";
import Image from "next/image";
import { MonsterStore } from "@/types";
import cloneDeep from 'lodash/cloneDeep'
import useMazeStore from "@/stores/mazeStore";
import { useTranslations } from "next-intl";
export default function AsBar() {
const { ASEvent, mapASInfo } = useEventStore()
const { listMonster } = useMonsterStore()
const { locale } = useLocaleStore()
const {
as_config,
setAsConfig
} = useUserDataStore()
const { AS } = useMazeStore()
const transI18n = useTranslations("DataPage")
const challengeSelected = useMemo(() => {
return mapASInfo[as_config.event_id.toString()]?.Level.find((as) => as.Id === as_config.challenge_id)
}, [as_config, mapASInfo])
const eventSelected = useMemo(() => {
return mapASInfo[as_config.event_id.toString()]
}, [as_config, mapASInfo])
const buffList = useMemo(() => {
const challenge = AS[as_config.event_id.toString()];
if (!challenge) return { buffList: [], buffId: [] };
if (as_config.floor_side === "Upper" || as_config.floor_side === "Upper -> Lower") {
return {
buffList: eventSelected?.BuffList1 ?? [],
buffId: challenge.buff_1 ?? [],
};
}
if (as_config.floor_side === "Lower" || as_config.floor_side === "Lower -> Upper") {
return {
buffList: eventSelected?.BuffList2 ?? [],
buffId: challenge.buff_2 ?? [],
};
}
return { buffList: [], buffId: [] };
}, [AS, as_config.event_id, as_config.floor_side, eventSelected?.BuffList1, eventSelected?.BuffList2]);
useEffect(() => {
const challenge = mapASInfo[as_config.event_id.toString()]?.Level.find((as) => as.Id === as_config.challenge_id)
if (as_config.event_id !== 0 && as_config.challenge_id !== 0 && challenge) {
const newBattleConfig = cloneDeep(as_config)
newBattleConfig.cycle_count = 4
newBattleConfig.blessings = []
if (as_config.buff_id !== 0) {
newBattleConfig.blessings.push({
id: as_config.buff_id,
level: 1
})
}
if (AS[as_config.challenge_id.toString()]) {
newBattleConfig.blessings.push({
id: Number(AS[as_config.challenge_id.toString()].maze_buff),
level: 1
})
}
newBattleConfig.monsters = []
newBattleConfig.stage_id = 0
if ((as_config.floor_side === "Upper" || as_config.floor_side === "Upper -> Lower") && challenge.EventIDList1.length > 0) {
newBattleConfig.stage_id = challenge.EventIDList1[0].StageID
for (const wave of challenge.EventIDList1[0].MonsterList) {
const newWave: MonsterStore[] = []
for (const value of Object.values(wave)) {
newWave.push({
monster_id: Number(value),
level: challenge.EventIDList1[0].Level,
amount: 1,
})
}
newBattleConfig.monsters.push(newWave)
}
}
if ((as_config.floor_side === "Lower" || as_config.floor_side === "Lower -> Upper") && challenge.EventIDList2.length > 0) {
newBattleConfig.stage_id = challenge.EventIDList2[0].StageID
for (const wave of challenge.EventIDList2[0].MonsterList) {
const newWave: MonsterStore[] = []
for (const value of Object.values(wave)) {
newWave.push({
monster_id: Number(value),
level: challenge.EventIDList2[0].Level,
amount: 1,
})
}
newBattleConfig.monsters.push(newWave)
}
}
if (as_config.floor_side === "Lower -> Upper" && challenge.EventIDList1.length > 0) {
for (const wave of challenge.EventIDList1[0].MonsterList) {
const newWave: MonsterStore[] = []
for (const value of Object.values(wave)) {
newWave.push({
monster_id: Number(value),
level: challenge.EventIDList1[0].Level,
amount: 1,
})
}
newBattleConfig.monsters.push(newWave)
}
} else if (as_config.floor_side === "Upper -> Lower" && challenge.EventIDList2.length > 0) {
for (const wave of challenge.EventIDList2[0].MonsterList) {
const newWave: MonsterStore[] = []
for (const value of Object.values(wave)) {
newWave.push({
monster_id: Number(value),
level: challenge.EventIDList2[0].Level,
amount: 1,
})
}
newBattleConfig.monsters.push(newWave)
}
}
setAsConfig(newBattleConfig)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
as_config.event_id,
as_config.challenge_id,
as_config.floor_side,
as_config.buff_id,
mapASInfo,
AS,
])
return (
<div className="container mx-auto px-4 py-8 relative">
{/* Title Card */}
<div className="rounded-xl p-4 mb-2 border border-warning">
<div className="mb-4 w-full">
<SelectCustomText
customSet={ASEvent.filter(as => as.lang.get(locale)).map((as) => ({
id: as.id,
name: getLocaleName(locale, as),
time: `${as.begin} - ${as.end}`,
}))}
excludeSet={[]}
selectedCustomSet={as_config.event_id.toString()}
placeholder={transI18n("selectASEvent")}
setSelectedCustomSet={(id) => setAsConfig({ ...as_config, event_id: Number(id), challenge_id: 0, buff_id: 0 })}
/>
</div>
{/* Settings */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<div className="flex items-center gap-2">
<label className="label">
<span className="label-text font-bold text-success">{transI18n("floor")}:{" "}</span>
</label>
<select
value={as_config.challenge_id}
className="select select-success"
onChange={(e) => setAsConfig({ ...as_config, challenge_id: Number(e.target.value) })}
>
<option value={0} disabled={true}>{transI18n("selectFloor")}</option>
{mapASInfo[as_config.event_id.toString()]?.Level.map((as) => (
<option key={as.Id} value={as.Id}>{as.Id % 10}</option>
))}
</select>
</div>
<div className="flex items-center gap-2">
<label className="label">
<span className="label-text font-bold text-success">{transI18n("side")}:{" "}</span>
</label>
<select
value={as_config.floor_side}
className="select select-success"
onChange={(e) => setAsConfig({ ...as_config, floor_side: e.target.value })}
>
<option value={0} disabled={true}>{transI18n("selectSide")}</option>
<option value="Upper">{transI18n("upper")}</option>
<option value="Lower">{transI18n("lower")}</option>
<option value="Upper -> Lower">{transI18n("upperToLower")}</option>
<option value="Lower -> Upper">{transI18n("lowerToUpper")}</option>
</select>
</div>
</div>
{eventSelected && (
<div className="mb-4 w-full">
<SelectCustomText
customSet={
Array.isArray(buffList?.buffList) && Array.isArray(buffList?.buffId)
? buffList.buffList.map((buff, index) => ({
id: buffList.buffId?.[index]?.toString() || "",
name: buff?.Name || "",
description: replaceByParam(buff?.Desc || "", buff?.Param || []),
}))
: []
}
excludeSet={[]}
selectedCustomSet={as_config?.buff_id?.toString()}
placeholder={transI18n("selectBuff")}
setSelectedCustomSet={(id) => setAsConfig({ ...as_config, buff_id: Number(id) })}
/>
</div>
)}
{/* Turbulence Buff */}
<div className="bg-base-200/20 rounded-lg p-4 border border-purple-500/20">
<h2 className="text-2xl font-bold mb-2 text-info">{transI18n("turbulenceBuff")}</h2>
{eventSelected && eventSelected.Buff?.Name ? (
<div
className="text-base"
dangerouslySetInnerHTML={{
__html: replaceByParam(
eventSelected.Buff?.Desc || "",
eventSelected.Buff?.Param || []
)
}}
/>
) : (
<div className="text-base">{transI18n("noTurbulenceBuff")}</div>
)}
</div>
</div>
{/* Enemy Waves */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* First Half */}
<div className="rounded-xl p-4 mt-2 border border-warning">
<h2 className="text-2xl font-bold mb-6 text-info">{transI18n("firstHalfEnemies")}</h2>
{challengeSelected && challengeSelected?.EventIDList1?.length > 0 && challengeSelected?.EventIDList1[0].MonsterList.map((wave, waveIndex) => (
<div key={waveIndex} className="mb-6">
<h3 className="text-lg font-semibold mb-t">{transI18n("wave")} {waveIndex + 1}</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{Object.values(wave).map((waveValue, enemyIndex) => (
<div
key={enemyIndex}
className="rounded-xl p-2 border border-white/10 shadow-md hover:border-white/20 hover:shadow-lg transition"
>
<div className="flex items-center space-x-3">
<div className="relative w-20 h-20 rounded-full overflow-hidden flex-shrink-0 border border-white/10 shadow-sm">
<Image
src={`https://api.hakush.in/hsr/UI/monstermiddleicon/${listMonster.find((monster) => monster.child.includes(waveValue))?.icon?.split("/")?.pop()?.replace(".png", "")}.webp`}
alt="Enemy Icon"
width={376}
height={512}
className="w-full h-full object-cover"
/>
</div>
<div className="flex flex-col">
<div className="text-sm font-semibold">Lv. {challengeSelected?.EventIDList1[0].Level}</div>
<div className="flex items-center space-x-1 mt-1">
{listMonster
.find((monster) => monster.child.includes(waveValue))
?.weak?.map((icon, iconIndex) => (
<Image
src={`/icon/${icon.toLowerCase()}.webp`}
alt={icon}
className="h-[28px] w-[28px] 2xl:h-[40px] 2xl:w-[40px] object-contain rounded-md border border-white/20 shadow-sm"
width={200}
height={200}
key={iconIndex}
/>
))}
</div>
</div>
</div>
</div>
))}
</div>
</div>
))}
</div>
{/* Second Half */}
<div className="rounded-xl p-4 mt-2 border border-warning">
<h2 className="text-2xl font-bold mb-6 text-info">{transI18n("secondHalfEnemies")}</h2>
{challengeSelected && challengeSelected?.EventIDList2?.length > 0 && challengeSelected?.EventIDList2[0].MonsterList.map((wave, waveIndex) => (
<div key={waveIndex} className="mb-6">
<h3 className="text-lg font-semibold mb-t">{transI18n("wave")} {waveIndex + 1}</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{Object.values(wave).map((waveValue, enemyIndex) => (
<div
key={enemyIndex}
className="rounded-xl p-2 border border-white/10 shadow-md hover:border-white/20 hover:shadow-lg transition"
>
<div className="flex items-center space-x-3">
<div className="relative w-20 h-20 rounded-full overflow-hidden flex-shrink-0 border border-white/10 shadow-sm">
<Image
src={`https://api.hakush.in/hsr/UI/monstermiddleicon/${listMonster.find((monster) => monster.child.includes(waveValue))?.icon?.split("/")?.pop()?.replace(".png", "")}.webp`}
alt="Enemy Icon"
width={376}
height={512}
className="w-full h-full object-cover"
/>
</div>
<div className="flex flex-col">
<div className="text-sm font-semibold">Lv. {challengeSelected?.EventIDList1[0].Level}</div>
<div className="flex items-center space-x-1 mt-1">
{listMonster
.find((monster) => monster.child.includes(waveValue))
?.weak?.map((icon, iconIndex) => (
<Image
src={`/icon/${icon.toLowerCase()}.webp`}
alt={icon}
className="h-[28px] w-[28px] 2xl:h-[40px] 2xl:w-[40px] object-contain rounded-md border border-white/20 shadow-sm"
width={200}
height={200}
key={iconIndex}
/>
))}
</div>
</div>
</div>
</div>
))}
</div>
</div>
))}
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,364 @@
"use client";
import React, { useEffect, useMemo, useState } from "react";
import {
Plus,
Trash2,
ChevronUp,
ChevronDown,
Search,
} from "lucide-react";
import useMazeStore from "@/stores/mazeStore";
import useUserDataStore from "@/stores/userDataStore";
import useMonsterStore from "@/stores/monsterStore";
import useLocaleStore from "@/stores/localeStore";
import { getLocaleName } from "@/helper";
import Image from "next/image";
import { MonsterBasic } from "@/types";
import { cloneDeep } from "lodash";
import { useTranslations } from "next-intl";
export default function CeBar() {
const [searchTerm, setSearchTerm] = useState("");
const [showSearchWaveId, setShowSearchWaveId] = useState<number | null>(null);
const { Stage } = useMazeStore()
const { ce_config, setCeConfig } = useUserDataStore()
const { listMonster } = useMonsterStore()
const { locale } = useLocaleStore()
const filteredMonsters = useMemo(() => {
const newlistMonster = new Set<MonsterBasic>()
for (const monster of listMonster) {
if (getLocaleName(locale, monster).toLowerCase().includes(searchTerm.toLowerCase())) {
newlistMonster.add(monster)
}
if (monster.id.toLowerCase().includes(searchTerm.toLowerCase())) {
newlistMonster.add(monster)
}
}
return Array.from(newlistMonster)
}, [listMonster, locale, searchTerm]);
const transI18n = useTranslations("DataPage")
const [showSearchStage, setShowSearchStage] = useState(false)
const [stageSearchTerm, setStageSearchTerm] = useState("")
const [stagePage, setStagePage] = useState(1)
const pageSize = 30
const stageList = useMemo(() => Object.values(Stage).map((stage) => ({
id: stage.stage_id.toString(),
name: `${stage.stage_type} (${stage.stage_id})`,
})), [Stage])
const filteredStages = useMemo(() => stageList.filter((s) =>
s.name.toLowerCase().includes(stageSearchTerm.toLowerCase())
), [stageList, stageSearchTerm])
const paginatedStages = useMemo(() => filteredStages.slice(
(stagePage - 1) * pageSize,
stagePage * pageSize
), [filteredStages, stagePage, pageSize])
const hasMorePages = useMemo(() => stagePage * pageSize < filteredStages.length, [stagePage, filteredStages])
useEffect(() => {
setStagePage(1)
}, [stageSearchTerm])
return (
<div className="container mx-auto p-6 z-4" onClick={() => {
setShowSearchWaveId(null)
setShowSearchStage(false)
}}>
<div className="mb-4 w-full relative">
<div className="flex items-center justify-center gap-2">
<Search className="w-6 h-6" />
<button
className="btn btn-outline w-[95%] text-left"
onClick={(e) => {
e.stopPropagation()
setShowSearchStage(true)
}}
>
<span className="text-left"> {transI18n("stage")}: {stageList.find((s) => s.id === ce_config.stage_id.toString())?.name || transI18n("selectStage")}</span>
</button>
</div>
{showSearchStage && (
<div className="absolute top-full mt-2 w-full z-50 border bg-base-200 border-slate-600 rounded-lg p-4 shadow-lg">
<div className="flex items-center gap-2 mb-2">
<Search className="w-4 h-4 text-slate-400" />
<input
type="text"
placeholder={transI18n("searchStage")}
className="input input-sm w-full placeholder-slate-400"
value={stageSearchTerm}
onChange={(e) => setStageSearchTerm(e.target.value)}
autoFocus
/>
</div>
<div className="max-h-60 overflow-y-auto space-y-1">
{paginatedStages.length > 0 ? (
<>
{paginatedStages.map((stage) => (
<div
key={stage.id}
className="p-2 hover:bg-primary/20 rounded cursor-pointer"
onClick={() => {
setCeConfig({ ...ce_config, stage_id: Number(stage.id), cycle_count: 30 })
setShowSearchStage(false)
setStageSearchTerm("")
}}
>
{stage.name}
</div>
))}
</>
) : (
<div className="text-sm text-center py-4">{transI18n("noStageFound")}</div>
)}
</div>
{paginatedStages.length > 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
<button
disabled={stagePage === 1}
className="btn btn-sm btn-outline btn-success mt-2"
onClick={(e) => {
e.stopPropagation()
setStagePage(stagePage - 1)
}}
>
{transI18n("previous")}
</button>
<button
disabled={!hasMorePages}
className="btn btn-sm btn-outline btn-success mt-2"
onClick={(e) => {
e.stopPropagation()
setStagePage(stagePage + 1)
}}
>
{transI18n("next")}
</button>
</div>
)}
</div>
)}
</div>
<div className="relative max-w-7xl mx-auto space-y-6">
{ce_config.monsters.map((wave, waveIndex) => (
<div key={waveIndex} className="card border border-slate-700/50 ">
<div className="card-body p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-bold text-white">{transI18n("wave")} {waveIndex + 1}</h2>
<div className="flex gap-2">
<button
disabled={waveIndex === 0}
onClick={(e) => {
e.stopPropagation()
const newCeConfig = cloneDeep(ce_config)
const waves = newCeConfig.monsters
const temp = waves[waveIndex - 1]
waves[waveIndex - 1] = waves[waveIndex]
waves[waveIndex] = temp
setCeConfig(newCeConfig)
}}
className="btn btn-sm btn-success"
>
<ChevronUp className="w-4 h-4" />
</button>
<button
disabled={waveIndex === ce_config.monsters.length - 1}
onClick={(e) => {
e.stopPropagation()
const newCeConfig = cloneDeep(ce_config)
const waves = newCeConfig.monsters
const temp = waves[waveIndex + 1]
waves[waveIndex + 1] = waves[waveIndex]
waves[waveIndex] = temp
setCeConfig(newCeConfig)
}}
className="btn btn-sm btn-success">
<ChevronDown className="w-4 h-4" />
</button>
<button
onClick={(e) => {
e.stopPropagation()
const newCeConfig = cloneDeep(ce_config)
newCeConfig.monsters.splice(waveIndex, 1)
setCeConfig(newCeConfig)
}}
className="btn btn-sm btn-error">
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4 w-full h-full">
{wave.map((member, memberIndex) => (
<div key={memberIndex} className="relative group z-7 w-full h-full">
<div className="card border hover:border-slate-500 transition-colors w-full h-full">
<div className="card-body p-4">
<button
className="btn btn-xs btn-error absolute -top-2 -right-2 opacity-50 group-hover:opacity-100 transition-opacity"
onClick={() => {
const newCeConfig = cloneDeep(ce_config)
newCeConfig.monsters[waveIndex].splice(memberIndex, 1)
setCeConfig(newCeConfig)
}}
>
<Trash2 className="w-3 h-3" />
</button>
<div className="flex justify-center">
<Image
src={`https://api.hakush.in/hsr/UI/monstermiddleicon/${listMonster.find((monster2) => monster2.child.includes(member.monster_id))?.icon?.split("/")?.pop()?.replace(".png", "")}.webp`}
alt="Enemy Icon"
width={376}
height={512}
className=" object-contain w-20 h-20 overflow-hidden"
/>
</div>
<div className="flex justify-center gap-1 mb-2">
{listMonster
.find((monster) => monster.child.includes(member.monster_id))
?.weak?.map((icon, iconIndex) => (
<Image
src={`/icon/${icon.toLowerCase()}.webp`}
alt={icon}
className="h-[28px] w-[28px] 2xl:h-[40px] 2xl:w-[40px] object-contain rounded-md border border-white/20 shadow-sm"
width={200}
height={200}
key={iconIndex}
/>
))}
</div>
<div className="text-center flex flex-col items-center justify-center">
<div className="text-sm font-medium">
{getLocaleName(locale, listMonster.find((monster) => monster.child.includes(member.monster_id)))}
</div>
<div className="flex items-center gap-1 mt-1">
<span className="text-sm">Lv.</span>
<input
type="number"
className="w-16 text-center input input-sm"
value={member.level}
onChange={(e) => {
try {
Number(e.target.value)
} catch {
return;
}
if (Number(e.target.value) < 1 || Number(e.target.value) > 95) return;
const newCeConfig = cloneDeep(ce_config)
newCeConfig.monsters[waveIndex][memberIndex].level = Number(e.target.value)
setCeConfig(newCeConfig)
}}
/>
</div>
</div>
</div>
</div>
</div>
))}
{/* Add Member Button + Search */}
<div className="relative flex items-start justify-center w-full h-full">
<button
className="btn btn-outline btn-primary w-full h-full border-dashed"
onClick={(e) => {
e.stopPropagation();
setShowSearchWaveId(waveIndex)
}}
>
<Plus className="w-8 h-8" />
</button>
{showSearchWaveId === waveIndex && (
<div className="absolute top-full mt-2 w-72 z-50 border bg-base-200 border-slate-600 rounded-lg p-4 shadow-lg">
<div className="flex items-center gap-2 mb-2">
<Search className="w-4 h-4" />
<input
type="text"
placeholder={transI18n("searchMonster")}
className="input input-sm w-full placeholder-slate-400"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
autoFocus
/>
</div>
<div className="max-h-60 overflow-y-auto space-y-1">
{filteredMonsters.length > 0 ? (
filteredMonsters.map((monster) => (
<div
key={monster.id}
className="flex items-center gap-2 p-2 hover:bg-success/40 rounded cursor-pointer"
onClick={() => {
const newCeConfig = cloneDeep(ce_config)
newCeConfig.monsters[waveIndex].push({
monster_id: Number(monster.id),
level: 95,
amount: 1,
})
setCeConfig(newCeConfig)
setShowSearchWaveId(null);
}}
>
<div className="relative w-8 h-8 rounded-full overflow-hidden flex-shrink-0 border border-white/10 shadow-sm">
<Image
src={`https://api.hakush.in/hsr/UI/monstermiddleicon/${listMonster.find((monster2) => monster2.child.includes(Number(monster.id)))?.icon?.split("/")?.pop()?.replace(".png", "")}.webp`}
alt="Enemy Icon"
width={376}
height={512}
className="w-full h-full object-cover"
/>
</div>
<span className="">{getLocaleName(locale, monster)} {`(${monster.id})`}</span>
</div>
))
) : (
<div className="text-sm text-center py-4">
{transI18n("noMonstersFound")}
</div>
)}
</div>
</div>
)}
</div>
</div>
</div>
</div>
))}
{/* Add New Wave Button */}
<div className="card backdrop-blur-sm border border-slate-700/50 border-dashed">
<div className="card-body p-8">
<button
onClick={() => {
const newCeConfig = cloneDeep(ce_config)
newCeConfig.monsters.push([])
setCeConfig(newCeConfig)
}}
className="btn btn-primary btn-lg w-full">
<Plus className="w-6 h-6 mr-2" />
{transI18n("addNewWave")}
</button>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,87 @@
import MocBar from "./moc";
import useUserDataStore from "@/stores/userDataStore";
import PfBar from "./pf";
import Image from "next/image";
import AsBar from "./as";
import { useTranslations } from "next-intl";
import CeBar from "./ce";
export default function MonsterBar() {
const { battle_type, setBattleType } = useUserDataStore()
const transI18n = useTranslations("DataPage")
const navItems = [
{ name: transI18n("memoryOfChaos"), icon: 'AbyssIcon01', value: 'MOC' },
{ name: transI18n("pureFiction"), icon: 'ChallengeStory', value: 'PF' },
{ name: transI18n("apocalypticShadow"), icon: 'ChallengeBoss', value: 'AS' },
{ name: transI18n("customEnemy"), icon: 'MonsterIcon', value: 'CE' },
{ name: transI18n("simulatedUniverse"), icon: 'SimulatedUniverse', value: 'SU' },
];
return (
<div className="min-h-screen">
{/* Header Navigation */}
<nav className="border-b border-warning/30 relative">
<div className="container mx-auto px-4">
<div className="flex items-center justify-center h-16">
{/* Navigation Tabs */}
<div className="flex space-x-1">
{navItems.map((item) => (
<button
key={item.name}
onClick={() => setBattleType(item.value.toUpperCase())}
className={`px-4 py-2 rounded-lg transition-all cursor-pointer duration-300 flex items-center space-x-2 ${battle_type.toUpperCase() === item.value.toUpperCase()
? 'bg-success/30 shadow-lg'
: 'bg-base-200/20 hover:bg-base-200/40 '
}`}
>
<span
style={
battle_type.toUpperCase() === item.value.toUpperCase()
? {
filter:
'brightness(0) saturate(100%) invert(63%) sepia(78%) saturate(643%) hue-rotate(1deg) brightness(93%) contrast(89%)',
}
: undefined
}
>
<Image src={`/icon/${item.icon}.webp`} alt={item.name} width={24} height={24} />
</span>
<span
style={
battle_type.toUpperCase() === item.value.toUpperCase()
? {
filter:
'brightness(0) saturate(100%) invert(63%) sepia(78%) saturate(643%) hue-rotate(1deg) brightness(93%) contrast(89%)',
}
: undefined
}
>
<span className="font-medium">{item.name}</span>
</span>
</button>
))}
</div>
</div>
</div>
</nav>
{(battle_type.toUpperCase() === "DEFAULT" || battle_type.toUpperCase() === "") && (
<div className="container mx-auto px-4 py-8 text-center font-bold text-3xl">
{transI18n("noEventSelected")}
</div>
)}
{battle_type.toUpperCase() === 'MOC' && <MocBar />}
{battle_type.toUpperCase() === 'PF' && <PfBar />}
{battle_type.toUpperCase() === 'AS' && <AsBar />}
{battle_type.toUpperCase() === 'CE' && <CeBar />}
{battle_type.toUpperCase() === 'SU' && (
<div className="container mx-auto px-4 py-8 text-center font-bold text-3xl">
{transI18n("comingSoon")}
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,321 @@
"use client"
import { useEffect, useMemo } from "react";
import SelectCustomText from "../select/customSelectText";
import useEventStore from "@/stores/eventStore";
import { getLocaleName, replaceByParam } from "@/helper";
import useLocaleStore from "@/stores/localeStore";
import useUserDataStore from "@/stores/userDataStore";
import useMonsterStore from "@/stores/monsterStore";
import Image from "next/image";
import { MonsterStore } from "@/types";
import cloneDeep from 'lodash/cloneDeep'
import useMazeStore from "@/stores/mazeStore";
import { useTranslations } from "next-intl";
export default function MocBar() {
const { MOCEvent, mapMOCInfo } = useEventStore()
const { listMonster } = useMonsterStore()
const { locale } = useLocaleStore()
const {
moc_config,
setMocConfig
} = useUserDataStore()
const { MOC } = useMazeStore()
const transI18n = useTranslations("DataPage")
const challengeSelected = useMemo(() => {
return mapMOCInfo[moc_config.event_id.toString()]?.find((moc) => moc.Id === moc_config.challenge_id)
}, [moc_config, mapMOCInfo])
useEffect(() => {
const challenge = mapMOCInfo[moc_config.event_id.toString()]?.find((moc) => moc.Id === moc_config.challenge_id)
if (moc_config.event_id !== 0 && moc_config.challenge_id !== 0 && challenge) {
const newBattleConfig = cloneDeep(moc_config)
newBattleConfig.cycle_count = 0
if (moc_config.use_cycle_count) {
newBattleConfig.cycle_count = challenge.Countdown
}
newBattleConfig.blessings = []
if (moc_config.use_turbulence_buff && MOC[moc_config.challenge_id.toString()]) {
newBattleConfig.blessings.push({
id: Number(MOC[moc_config.challenge_id.toString()].maze_buff),
level: 1
})
}
newBattleConfig.monsters = []
newBattleConfig.stage_id = 0
if ((moc_config.floor_side === "Upper" || moc_config.floor_side === "Upper -> Lower") && challenge.EventIDList1.length > 0) {
newBattleConfig.stage_id = challenge.EventIDList1[0].StageID
for (const wave of challenge.EventIDList1[0].MonsterList) {
const newWave: MonsterStore[] = []
for (const value of Object.values(wave)) {
newWave.push({
monster_id: Number(value),
level: challenge.EventIDList1[0].Level,
amount: 1,
})
}
newBattleConfig.monsters.push(newWave)
}
}
if ((moc_config.floor_side === "Lower" || moc_config.floor_side === "Lower -> Upper") && challenge.EventIDList2.length > 0) {
newBattleConfig.stage_id = challenge.EventIDList2[0].StageID
for (const wave of challenge.EventIDList2[0].MonsterList) {
const newWave: MonsterStore[] = []
for (const value of Object.values(wave)) {
newWave.push({
monster_id: Number(value),
level: challenge.EventIDList2[0].Level,
amount: 1,
})
}
newBattleConfig.monsters.push(newWave)
}
}
if (moc_config.floor_side === "Lower -> Upper" && challenge.EventIDList1.length > 0) {
for (const wave of challenge.EventIDList1[0].MonsterList) {
const newWave: MonsterStore[] = []
for (const value of Object.values(wave)) {
newWave.push({
monster_id: Number(value),
level: challenge.EventIDList1[0].Level,
amount: 1,
})
}
newBattleConfig.monsters.push(newWave)
}
} else if (moc_config.floor_side === "Upper -> Lower" && challenge.EventIDList2.length > 0) {
for (const wave of challenge.EventIDList2[0].MonsterList) {
const newWave: MonsterStore[] = []
for (const value of Object.values(wave)) {
newWave.push({
monster_id: Number(value),
level: challenge.EventIDList2[0].Level,
amount: 1,
})
}
newBattleConfig.monsters.push(newWave)
}
}
setMocConfig(newBattleConfig)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
moc_config.event_id,
moc_config.challenge_id,
moc_config.floor_side,
mapMOCInfo,
MOC,
moc_config.use_cycle_count,
moc_config.use_turbulence_buff,
])
return (
<div className="container mx-auto px-4 py-8 relative">
{/* Title Card */}
<div className="rounded-xl p-4 mb-2 border border-warning">
<div className="mb-4 w-full">
<SelectCustomText
customSet={MOCEvent.map((moc) => ({
id: moc.id,
name: getLocaleName(locale, moc),
time: `${moc.begin} - ${moc.end}`,
}))}
excludeSet={[]}
selectedCustomSet={moc_config.event_id.toString()}
placeholder={transI18n("selectMOCEvent")}
setSelectedCustomSet={(id) => setMocConfig({ ...moc_config, event_id: Number(id), challenge_id: 0 })}
/>
</div>
{/* Settings */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<div className="flex items-center gap-2">
<label className="label">
<span className="label-text font-bold text-success">{transI18n("floor")}:{" "}</span>
</label>
<select
value={moc_config.challenge_id}
className="select select-success"
onChange={(e) => setMocConfig({ ...moc_config, challenge_id: Number(e.target.value) })}
>
<option value={0} disabled={true}>Select a Floor</option>
{mapMOCInfo[moc_config.event_id.toString()]?.map((moc) => (
<option key={moc.Id} value={moc.Id}>{moc.Id % 100}</option>
))}
</select>
</div>
<div className="flex items-center gap-2">
<label className="label">
<span className="label-text font-bold text-success">{transI18n("side")}:{" "}</span>
</label>
<select
value={moc_config.floor_side}
className="select select-success"
onChange={(e) => setMocConfig({ ...moc_config, floor_side: e.target.value })}
>
<option value={0} disabled={true}>{transI18n("selectSide")}</option>
<option value="Upper">{transI18n("upper")}</option>
<option value="Lower">{transI18n("lower")}</option>
<option value="Upper -> Lower">{transI18n("upperToLower")}</option>
<option value="Lower -> Upper">{transI18n("lowerToUpper")}</option>
</select>
</div>
<div className="flex items-center gap-2">
<label className="label cursor-pointer">
<span
onClick={() => setMocConfig({ ...moc_config, use_cycle_count: !moc_config.use_cycle_count })}
className="label-text font-bold text-success cursor-pointer"
>
{transI18n("useCycleCount")} {" "}
</span>
<input
type="checkbox"
checked={moc_config.use_cycle_count}
onChange={(e) => setMocConfig({ ...moc_config, use_cycle_count: e.target.checked })}
className="checkbox checkbox-primary"
/>
</label>
</div>
</div>
{/* Turbulence Buff */}
<div className="bg-base-200/20 rounded-lg p-4 border border-purple-500/20">
<div className="flex items-center space-x-2 mb-2">
<input
type="checkbox"
checked={moc_config.use_turbulence_buff}
onChange={(e) => setMocConfig({ ...moc_config, use_turbulence_buff: e.target.checked })}
className="checkbox checkbox-primary"
/>
<span
onClick={() => setMocConfig({ ...moc_config, use_turbulence_buff: !moc_config.use_turbulence_buff })}
className="font-bold text-success cursor-pointer">
{transI18n("useTurbulenceBuff")}
</span>
</div>
{challengeSelected && challengeSelected?.Desc ? (
<div
className="text-base"
dangerouslySetInnerHTML={{
__html: replaceByParam(
challengeSelected?.Desc || "",
challengeSelected?.Param || []
)
}}
/>
) : (
<div className="text-base">{transI18n("noTurbulenceBuff")}</div>
)}
</div>
</div>
{/* Enemy Waves */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* First Half */}
<div className="rounded-xl p-4 mt-2 border border-warning">
<h2 className="text-2xl font-bold mb-6 text-info">{transI18n("firstHalfEnemies")}</h2>
{challengeSelected && challengeSelected?.EventIDList1?.length > 0 && challengeSelected?.EventIDList1[0].MonsterList.map((wave, waveIndex) => (
<div key={waveIndex} className="mb-6">
<h3 className="text-lg font-semibold mb-t">{transI18n("wave")} {waveIndex + 1}</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{Object.values(wave).map((waveValue, enemyIndex) => (
<div
key={enemyIndex}
className="rounded-xl p-2 border border-white/10 shadow-md hover:border-white/20 hover:shadow-lg transition"
>
<div className="flex items-center space-x-3">
<div className="relative w-20 h-20 rounded-full overflow-hidden flex-shrink-0 border border-white/10 shadow-sm">
<Image
src={`https://api.hakush.in/hsr/UI/monstermiddleicon/${listMonster.find((monster) => monster.child.includes(waveValue))?.icon?.split("/")?.pop()?.replace(".png", "")}.webp`}
alt="Enemy Icon"
width={376}
height={512}
className="w-full h-full object-cover"
/>
</div>
<div className="flex flex-col">
<div className="text-sm font-semibold">Lv. {challengeSelected?.EventIDList1[0].Level}</div>
<div className="flex items-center space-x-1 mt-1">
{listMonster
.find((monster) => monster.child.includes(waveValue))
?.weak?.map((icon, iconIndex) => (
<Image
src={`/icon/${icon.toLowerCase()}.webp`}
alt={icon}
className="h-[28px] w-[28px] 2xl:h-[40px] 2xl:w-[40px] object-contain rounded-md border border-white/20 shadow-sm"
width={200}
height={200}
key={iconIndex}
/>
))}
</div>
</div>
</div>
</div>
))}
</div>
</div>
))}
</div>
{/* Second Half */}
<div className="rounded-xl p-4 mt-2 border border-warning">
<h2 className="text-2xl font-bold mb-6 text-info">{transI18n("secondHalfEnemies")}</h2>
{challengeSelected && challengeSelected?.EventIDList2?.length > 0 && challengeSelected?.EventIDList2[0].MonsterList.map((wave, waveIndex) => (
<div key={waveIndex} className="mb-6">
<h3 className="text-lg font-semibold mb-t">{transI18n("wave")} {waveIndex + 1}</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{Object.values(wave).map((waveValue, enemyIndex) => (
<div
key={enemyIndex}
className="rounded-xl p-2 border border-white/10 shadow-md hover:border-white/20 hover:shadow-lg transition"
>
<div className="flex items-center space-x-3">
<div className="relative w-20 h-20 rounded-full overflow-hidden flex-shrink-0 border border-white/10 shadow-sm">
<Image
src={`https://api.hakush.in/hsr/UI/monstermiddleicon/${listMonster.find((monster) => monster.child.includes(waveValue))?.icon?.split("/")?.pop()?.replace(".png", "")}.webp`}
alt="Enemy Icon"
width={376}
height={512}
className="w-full h-full object-cover"
/>
</div>
<div className="flex flex-col">
<div className="text-sm font-semibold">Lv. {challengeSelected?.EventIDList1[0].Level}</div>
<div className="flex items-center space-x-1 mt-1">
{listMonster
.find((monster) => monster.child.includes(waveValue))
?.weak?.map((icon, iconIndex) => (
<Image
src={`/icon/${icon.toLowerCase()}.webp`}
alt={icon}
className="h-[28px] w-[28px] 2xl:h-[40px] 2xl:w-[40px] object-contain rounded-md border border-white/20 shadow-sm"
width={200}
height={200}
key={iconIndex}
/>
))}
</div>
</div>
</div>
</div>
))}
</div>
</div>
))}
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,330 @@
"use client"
import { useEffect, useMemo } from "react";
import SelectCustomText from "../select/customSelectText";
import useEventStore from "@/stores/eventStore";
import { getLocaleName, replaceByParam } from "@/helper";
import useLocaleStore from "@/stores/localeStore";
import useUserDataStore from "@/stores/userDataStore";
import useMonsterStore from "@/stores/monsterStore";
import Image from "next/image";
import { MonsterStore } from "@/types";
import cloneDeep from 'lodash/cloneDeep'
import useMazeStore from "@/stores/mazeStore";
import { useTranslations } from "next-intl";
export default function PfBar() {
const { PFEvent, mapPFInfo } = useEventStore()
const { listMonster } = useMonsterStore()
const { locale } = useLocaleStore()
const {
pf_config,
setPfConfig
} = useUserDataStore()
const { PF } = useMazeStore()
const transI18n = useTranslations("DataPage")
const challengeSelected = useMemo(() => {
return mapPFInfo[pf_config.event_id.toString()]?.Level.find((pf) => pf.Id === pf_config.challenge_id)
}, [pf_config, mapPFInfo])
const eventSelected = useMemo(() => {
return mapPFInfo[pf_config.event_id.toString()]
}, [pf_config, mapPFInfo])
useEffect(() => {
const challenge = mapPFInfo[pf_config.event_id.toString()]?.Level.find((pf) => pf.Id === pf_config.challenge_id)
if (pf_config.event_id !== 0 && pf_config.challenge_id !== 0 && challenge) {
const newBattleConfig = cloneDeep(pf_config)
newBattleConfig.cycle_count = 0
newBattleConfig.blessings = []
if (pf_config.buff_id !== 0) {
newBattleConfig.blessings.push({
id: pf_config.buff_id,
level: 1
})
}
if (PF[pf_config.challenge_id.toString()]) {
newBattleConfig.blessings.push({
id: Number(PF[pf_config.challenge_id.toString()].maze_buff),
level: 1
})
}
newBattleConfig.monsters = []
newBattleConfig.stage_id = 0
if ((pf_config.floor_side === "Upper" || pf_config.floor_side === "Upper -> Lower" ) && challenge.EventIDList1.length > 0 ) {
newBattleConfig.stage_id = challenge.EventIDList1[0].StageID
for (const wave of challenge.EventIDList1[0].MonsterList) {
const newWave: MonsterStore[] = []
for (const value of Object.values(wave)) {
newWave.push({
monster_id: Number(value),
level: challenge.EventIDList1[0].Level,
amount: 1,
})
}
newBattleConfig.monsters.push(newWave)
}
}
if ((pf_config.floor_side === "Lower" || pf_config.floor_side === "Lower -> Upper") && challenge.EventIDList2.length > 0) {
newBattleConfig.stage_id = challenge.EventIDList2[0].StageID
for (const wave of challenge.EventIDList2[0].MonsterList) {
const newWave: MonsterStore[] = []
for (const value of Object.values(wave)) {
newWave.push({
monster_id: Number(value),
level: challenge.EventIDList2[0].Level,
amount: 1,
})
}
newBattleConfig.monsters.push(newWave)
}
}
if (pf_config.floor_side === "Lower -> Upper" && challenge.EventIDList1.length > 0) {
for (const wave of challenge.EventIDList1[0].MonsterList) {
const newWave: MonsterStore[] = []
for (const value of Object.values(wave)) {
newWave.push({
monster_id: Number(value),
level: challenge.EventIDList1[0].Level,
amount: 1,
})
}
newBattleConfig.monsters.push(newWave)
}
} else if (pf_config.floor_side === "Upper -> Lower" && challenge.EventIDList2.length > 0) {
for (const wave of challenge.EventIDList2[0].MonsterList) {
const newWave: MonsterStore[] = []
for (const value of Object.values(wave)) {
newWave.push({
monster_id: Number(value),
level: challenge.EventIDList2[0].Level,
amount: 1,
})
}
newBattleConfig.monsters.push(newWave)
}
}
setPfConfig(newBattleConfig)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
pf_config.event_id,
pf_config.challenge_id,
pf_config.floor_side,
pf_config.buff_id,
mapPFInfo,
PF,
])
return (
<div className="container mx-auto px-4 py-8 relative">
{/* Title Card */}
<div className="rounded-xl p-4 mb-2 border border-warning">
<div className="mb-4 w-full">
<SelectCustomText
customSet={PFEvent.map((pf) => ({
id: pf.id,
name: getLocaleName(locale, pf),
time: `${pf.begin} - ${pf.end}`,
}))}
excludeSet={[]}
selectedCustomSet={pf_config.event_id.toString()}
placeholder={transI18n("selectPFEvent")}
setSelectedCustomSet={(id) => setPfConfig({ ...pf_config, event_id: Number(id), challenge_id: 0, buff_id: 0 })}
/>
</div>
{/* Settings */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<div className="flex items-center gap-2">
<label className="label">
<span className="label-text font-bold text-success">{transI18n("floor")}:{" "}</span>
</label>
<select
value={pf_config.challenge_id}
className="select select-success"
onChange={(e) => setPfConfig({ ...pf_config, challenge_id: Number(e.target.value) })}
>
<option value={0} disabled={true}>{transI18n("selectFloor")}</option>
{mapPFInfo[pf_config.event_id.toString()]?.Level.map((pf) => (
<option key={pf.Id} value={pf.Id}>{pf.Id % 10}</option>
))}
</select>
</div>
<div className="flex items-center gap-2">
<label className="label">
<span className="label-text font-bold text-success">{transI18n("side")}:{" "}</span>
</label>
<select
value={pf_config.floor_side}
className="select select-success"
onChange={(e) => setPfConfig({ ...pf_config, floor_side: e.target.value })}
>
<option value={0} disabled={true}>{transI18n("selectSide")}</option>
<option value="Upper">{transI18n("upper")}</option>
<option value="Lower">{transI18n("lower")}</option>
<option value="Upper -> Lower">{transI18n("upperToLower")}</option>
<option value="Lower -> Upper">{transI18n("lowerToUpper")}</option>
</select>
</div>
</div>
{eventSelected && (
<div className="mb-4 w-full">
<SelectCustomText
customSet={eventSelected.Option.map((buff, index) => ({
id: PF[eventSelected.Id.toString()]?.buff[index]?.toString(),
name: buff.Name,
description: replaceByParam(buff.Desc, buff.Param || []),
}))}
excludeSet={[]}
selectedCustomSet={pf_config?.buff_id?.toString()}
placeholder={transI18n("selectBuff")}
setSelectedCustomSet={(id) => setPfConfig({ ...pf_config, buff_id: Number(id) })}
/>
</div>
)}
{/* Turbulence Buff */}
<div className="bg-base-200/20 rounded-lg p-4 border border-purple-500/20">
<h2 className="text-2xl font-bold mb-2 text-info">{transI18n("turbulenceBuff")}</h2>
{eventSelected && eventSelected.SubOption.length > 0 ? (
eventSelected.SubOption.map((subOption, index) => (
<div key={index}>
<label className="label">
<span className="label-text font-bold text-success">{index + 1}. {subOption.Name}</span>
</label>
<div
className="text-base"
dangerouslySetInnerHTML={{
__html: replaceByParam(
subOption.Desc,
subOption.Param || []
)
}}
/>
</div>
))
) : eventSelected && eventSelected.SubOption.length === 0 ? (
<div
className="text-base"
dangerouslySetInnerHTML={{
__html: replaceByParam(
eventSelected.Buff?.Desc || "",
eventSelected.Buff?.Param || []
)
}}
/>
) : (
<div className="text-base">{transI18n("noTurbulenceBuff")}</div>
)}
</div>
</div>
{/* Enemy Waves */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* First Half */}
<div className="rounded-xl p-4 mt-2 border border-warning">
<h2 className="text-2xl font-bold mb-6 text-info">{transI18n("firstHalfEnemies")}</h2>
{challengeSelected && Object.values(challengeSelected.InfiniteList1).map((waveValue, waveIndex) => (
<div key={waveIndex} className="mb-6">
<h3 className="text-lg font-semibold mb-t">{transI18n("wave")} {waveIndex + 1}</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{Array.from(new Set(waveValue.MonsterGroupIDList)).map((monsterId, enemyIndex) => (
<div
key={enemyIndex}
className="rounded-xl p-2 border border-white/10 shadow-md hover:border-white/20 hover:shadow-lg transition"
>
<div className="flex items-center space-x-3">
<div className="relative w-20 h-20 rounded-full overflow-hidden flex-shrink-0 border border-white/10 shadow-sm">
<Image
src={`https://api.hakush.in/hsr/UI/monstermiddleicon/${listMonster.find((monster) => monster.child.includes(monsterId))?.icon?.split("/")?.pop()?.replace(".png", "")}.webp`}
alt="Enemy Icon"
width={376}
height={512}
className="w-full h-full object-cover"
/>
</div>
<div className="flex flex-col">
<div className="text-sm font-semibold">Lv. {challengeSelected?.EventIDList1[0].Level}</div>
<div className="flex items-center space-x-1 mt-1">
{listMonster
.find((monster) => monster.child.includes(monsterId))
?.weak?.map((icon, iconIndex) => (
<Image
src={`/icon/${icon.toLowerCase()}.webp`}
alt={icon}
className="h-[28px] w-[28px] 2xl:h-[40px] 2xl:w-[40px] object-contain rounded-md border border-white/20 shadow-sm"
width={200}
height={200}
key={iconIndex}
/>
))}
</div>
</div>
</div>
</div>
))}
</div>
</div>
))}
</div>
{/* Second Half */}
<div className="rounded-xl p-4 mt-2 border border-warning">
<h2 className="text-2xl font-bold mb-6 text-info">{transI18n("secondHalfEnemies")}</h2>
{challengeSelected && Object.values(challengeSelected?.InfiniteList2).map((waveValue, waveIndex) => (
<div key={waveIndex} className="mb-6">
<h3 className="text-lg font-semibold mb-t">{transI18n("wave")} {waveIndex + 1}</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{Array.from(new Set(waveValue.MonsterGroupIDList)).map((monsterId, enemyIndex) => (
<div
key={enemyIndex}
className="rounded-xl p-2 border border-white/10 shadow-md hover:border-white/20 hover:shadow-lg transition"
>
<div className="flex items-center space-x-3">
<div className="relative w-20 h-20 rounded-full overflow-hidden flex-shrink-0 border border-white/10 shadow-sm">
<Image
src={`https://api.hakush.in/hsr/UI/monstermiddleicon/${listMonster.find((monster) => monster.child.includes(monsterId))?.icon?.split("/")?.pop()?.replace(".png", "")}.webp`}
alt="Enemy Icon"
width={376}
height={512}
className="w-full h-full object-cover"
/>
</div>
<div className="flex flex-col">
<div className="text-sm font-semibold">Lv. {challengeSelected?.EventIDList1[0].Level}</div>
<div className="flex items-center space-x-1 mt-1">
{listMonster
.find((monster) => monster.child.includes(monsterId))
?.weak?.map((icon, iconIndex) => (
<Image
src={`/icon/${icon.toLowerCase()}.webp`}
alt={icon}
className="h-[28px] w-[28px] 2xl:h-[40px] 2xl:w-[40px] object-contain rounded-md border border-white/20 shadow-sm"
width={200}
height={200}
key={iconIndex}
/>
))}
</div>
</div>
</div>
</div>
))}
</div>
</div>
))}
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,12 @@
"use client";
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import React from 'react';
export default function QueryProviderWrapper({ children }: { children: React.ReactNode }) {
const [queryClient] = React.useState(() => new QueryClient());
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
}

View File

@@ -3,7 +3,7 @@ import useRelicStore from '@/stores/relicStore';
import useUserDataStore from '@/stores/userDataStore';
import { AffixDetail, RelicDetail } from '@/types';
import React, { useEffect, useMemo } from 'react';
import SelectCustom from '../select';
import SelectCustomImage from '../select/customSelectImage';
import { calcAffixBonus, randomPartition, randomStep, replaceByParam } from '@/helper';
import useAffixStore from '@/stores/affixStore';
import { mapingStats } from '@/lib/constant';
@@ -12,6 +12,7 @@ import useModelStore from '@/stores/modelStore';
import useRelicMakerStore from '@/stores/relicMakerStore';
import { toast } from 'react-toastify';
import { useTranslations } from 'next-intl';
import cloneDeep from 'lodash/cloneDeep'
export default function RelicMaker() {
const { avatars, setAvatars } = useUserDataStore()
@@ -77,7 +78,7 @@ export default function RelicMaker() {
const mainProp = mainAffixMap[selectedMainStat]?.property;
if (!mainProp) return;
const newSubAffixes = listSelectedSubStats.map(item => ({ ...item }));
const newSubAffixes = cloneDeep(listSelectedSubStats);
let updated = false;
for (let i = 0; i < newSubAffixes.length; i++) {
@@ -91,6 +92,7 @@ export default function RelicMaker() {
}
if (updated) setListSelectedSubStats(newSubAffixes);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedMainStat, mapSubAffix, mapMainAffix, selectedRelicSlot]);
const exSubAffixOptions = useMemo(() => {
@@ -129,7 +131,7 @@ export default function RelicMaker() {
}, [mapMainAffix, selectedRelicSlot, selectedMainStat, selectedRelicLevel]);
const handleSubStatChange = (key: string, index: number, rollCount: number, stepCount: number) => {
const newSubAffixes = listSelectedSubStats.map(item => ({ ...item }));
const newSubAffixes = cloneDeep(listSelectedSubStats);
if (!subAffixOptions[key]) {
newSubAffixes[index].affixId = "";
newSubAffixes[index].property = "";
@@ -153,8 +155,8 @@ export default function RelicMaker() {
const keys = Object.keys(preSelectedSubStats[index]);
if (keys.length <= 1) return;
const newSubAffixes = listSelectedSubStats.map(item => ({ ...item }));
const listHistory = preSelectedSubStats[index].map(item => ({ ...item }));
const newSubAffixes = cloneDeep(listSelectedSubStats);
const listHistory = cloneDeep(preSelectedSubStats[index]);
const secondLastKey = listHistory.length - 2;
const preSubAffixes = { ...listHistory[secondLastKey] };
newSubAffixes[index].rollCount = preSubAffixes.rollCount;
@@ -164,7 +166,7 @@ export default function RelicMaker() {
};
const resetSubStat = (index: number) => {
const newSubAffixes = listSelectedSubStats.map(item => ({ ...item }));
const newSubAffixes = cloneDeep(listSelectedSubStats);
resetHistory(index);
newSubAffixes[index].affixId = "";
newSubAffixes[index].property = "";
@@ -174,7 +176,7 @@ export default function RelicMaker() {
};
const randomizeStats = () => {
const newSubAffixes = listSelectedSubStats.map(item => ({ ...item }));
const newSubAffixes = cloneDeep(listSelectedSubStats);
const exKeys = Object.keys(exSubAffixOptions);
for (let i = 0; i < newSubAffixes.length; i++) {
const keys = Object.keys(subAffixOptions).filter((key) => !exKeys.includes(key));
@@ -194,7 +196,7 @@ export default function RelicMaker() {
};
const randomizeRolls = () => {
const newSubAffixes = listSelectedSubStats.map(item => ({ ...item }));
const newSubAffixes = cloneDeep(listSelectedSubStats);
const randomRolls = randomPartition(9, listSelectedSubStats.length);
for (let i = 0; i < listSelectedSubStats.length; i++) {
newSubAffixes[i].rollCount = randomRolls[i];
@@ -252,7 +254,7 @@ export default function RelicMaker() {
{/* Main Stat */}
<div>
<label className="block text-lg font-medium mb-2">{transI18n("mainStat")}</label>
<SelectCustom
<SelectCustomImage
customSet={Object.entries(mapMainAffix["5" + selectedRelicSlot] || {}).map(([key, value]) => ({
value: key,
label: mapingStats[value.property].name + " " + mapingStats[value.property].unit,
@@ -267,7 +269,7 @@ export default function RelicMaker() {
{/* Relic Set Selection */}
<div>
<label className="block text-lg font-medium mb-2">{transI18n("set")}</label>
<SelectCustom
<SelectCustomImage
customSet={Object.entries(relicSets).map(([key, value]) => ({
value: key,
label: value.Name,
@@ -356,7 +358,7 @@ export default function RelicMaker() {
{/* Stat Selection */}
<div className="col-span-8">
<SelectCustom
<SelectCustomImage
customSet={Object.entries(subAffixOptions).map(([key, value]) => ({
value: key,
label: mapingStats[value.property].name + " " + mapingStats[value.property].unit,

View File

@@ -1,11 +1,8 @@
"use client"
;
import { useRouter } from 'next/navigation'
import { useCallback, useEffect, useMemo, useState } from "react";
"use client";
import { useCallback, useEffect, useMemo } from "react";
import RelicMaker from "../relicBar";
import { motion } from "framer-motion";
import useUserDataStore from "@/stores/userDataStore";
import useLocaleStore from "@/stores/localeStore";
import { useTranslations } from "next-intl";
import RelicCard from "../card/relicCard";
import useAvatarStore from "@/stores/avatarStore";
@@ -16,7 +13,6 @@ import useRelicMakerStore from '@/stores/relicMakerStore';
import useAffixStore from '@/stores/affixStore';
export default function RelicsInfo() {
const router = useRouter()
const { avatars, setAvatars } = useUserDataStore()
const { avatarSelected } = useAvatarStore()
const {
@@ -32,7 +28,7 @@ export default function RelicsInfo() {
const { mapSubAffix } = useAffixStore()
const { isOpenRelic, setIsOpenRelic } = useModelStore()
const transI18n = useTranslations("DataPage")
const { locale } = useLocaleStore();
const { mapRelicInfo } = useRelicStore()
const handleShow = (modalId: string) => {
@@ -67,6 +63,7 @@ export default function RelicsInfo() {
window.addEventListener('keydown', handleEscKey);
return () => window.removeEventListener('keydown', handleEscKey);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOpenRelic]);
const getRelic = useCallback((slot: string) => {
@@ -124,7 +121,7 @@ export default function RelicsInfo() {
const avatar = avatars[avatarSelected?.id || ""];
const relicCount: { [key: string]: number } = {};
if (avatar) {
for (const [slot, relic] of Object.entries(avatar.profileList[avatar.profileSelect].relics)) {
for (const relic of Object.values(avatar.profileList[avatar.profileSelect].relics)) {
if (relicCount[relic.relic_set_id]) {
relicCount[relic.relic_set_id]++;
} else {
@@ -152,7 +149,7 @@ export default function RelicsInfo() {
<div className="bg-base-100 rounded-xl p-6 shadow-lg">
<h2 className="flex items-center gap-2 text-2xl font-bold mb-6 text-base-content">
<div className="w-2 h-6 bg-gradient-to-b from-primary to-primary/50 rounded-full"></div>
Equipment
{transI18n("relics")}
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 max-w-2xl mx-auto">
@@ -170,16 +167,14 @@ export default function RelicsInfo() {
/>
</div>
{/* Action buttons - chỉ hiện khi hover */}
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex gap-1">
{/* Nút chuyển relic */}
<button
onClick={(e) => {
e.stopPropagation()
handlerChangeRelic(item)
}}
className="btn btn-info p-1.5 rounded-full shadow-lg transition-colors duration-200"
title="Chuyển relic"
title={transI18n("changeRelic")}
>
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" />
@@ -190,12 +185,12 @@ export default function RelicsInfo() {
<button
onClick={(e) => {
e.stopPropagation()
if (window.confirm(`Bạn có chắc muốn xóa relic ở slot ${item}?`)) {
if (window.confirm(`${transI18n("deleteRelicConfirm")} ${item}?`)) {
handlerDeleteRelic(item)
}
}}
className="btn btn-error p-1.5 rounded-full shadow-lg transition-colors duration-200"
title="Xóa relic"
title={transI18n("deleteRelic")}
>
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
@@ -216,7 +211,7 @@ export default function RelicsInfo() {
<div className="bg-base-100 rounded-xl p-6 shadow-lg">
<h3 className="flex items-center gap-2 text-xl font-bold mb-4 text-base-content">
<div className="w-2 h-6 bg-gradient-to-b from-primary to-primary/50 rounded-full"></div>
Set Effects
{transI18n("setEffects")}
</h3>
<div className="space-y-6">

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import Select, { SingleValue } from 'react-select'
import Image from 'next/image'
@@ -18,7 +19,7 @@ type SelectCustomProp = {
setSelectedCustomSet: (value: string) => void
}
export default function SelectCustom({ customSet, excludeSet, selectedCustomSet, placeholder, setSelectedCustomSet }: SelectCustomProp) {
export default function SelectCustomImage({ customSet, excludeSet, selectedCustomSet, placeholder, setSelectedCustomSet }: SelectCustomProp) {
const options: SelectOption[] = customSet
const { locale } = useLocaleStore()
const customStyles = {
@@ -27,22 +28,24 @@ export default function SelectCustom({ customSet, excludeSet, selectedCustomSet,
display: 'flex',
alignItems: 'center',
gap: '8px',
padding: '8px'
padding: '8px',
backgroundColor: 'transparent',
}),
singleValue: (provided: any) => ({
...provided,
display: 'flex',
alignItems: 'center',
gap: '8px'
gap: '8px',
backgroundColor: 'transparent',
}),
menuPortal: (provided: any) => ({ ...provided, zIndex: 9999 }),
menu: (provided: any) => ({ ...provided, zIndex: 9999 })
}
const formatOptionLabel = (option: SelectOption) => (
<div className="flex items-center gap-1 bg-slate-400 w-full h-full z-50">
<Image src={option.imageUrl} alt="" width={125} height={125} className="w-8 h-8 object-contain" />
<ParseText className='text-black font-bold' text={option.label} locale={locale} />
<div className="flex items-center gap-1 w-full h-full z-50">
<Image src={option.imageUrl} alt="" width={125} height={125} className="w-8 h-8 object-contain bg-warning-content rounded-full" />
<ParseText className='font-bold text-warning-content' text={option.label} locale={locale} />
</div>
)
@@ -52,7 +55,6 @@ export default function SelectCustom({ customSet, excludeSet, selectedCustomSet,
value={options.find(opt => {
return opt.value === selectedCustomSet}) || null}
onChange={(selected: SingleValue<SelectOption>) => {
console.log(selected);
setSelectedCustomSet(selected?.value || '')
}}
formatOptionLabel={formatOptionLabel}

View File

@@ -0,0 +1,83 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import Select, { SingleValue } from 'react-select'
import { replaceByParam } from '@/helper'
export type SelectOption = {
id: string
name: string
time?: string
description?: string
}
type SelectCustomProp = {
customSet: SelectOption[]
excludeSet: SelectOption[]
selectedCustomSet: string
placeholder: string
setSelectedCustomSet: (value: string) => void
}
export default function SelectCustomText({ customSet, excludeSet, selectedCustomSet, placeholder, setSelectedCustomSet }: SelectCustomProp) {
const options: SelectOption[] = customSet
const customStyles = {
option: (provided: any) => ({
...provided,
display: 'flex',
alignItems: 'center',
gap: '8px',
padding: '8px',
backgroundColor: 'transparent',
}),
singleValue: (provided: any) => ({
...provided,
display: 'flex',
alignItems: 'center',
gap: '8px'
}),
menuPortal: (provided: any) => ({ ...provided, zIndex: 9999 }),
menu: (provided: any) => ({ ...provided, zIndex: 9999 })
}
const formatOptionLabel = (option: SelectOption) => (
<div className="flex flex-col gap-1 w-full h-full z-50 text-warning-content ">
<div
className="font-bold text-lg"
dangerouslySetInnerHTML={{
__html: replaceByParam(
option.name,
[]
)
}}
/>
{option.time && <div className='text-base'>{option.time}</div>}
{option.description && <div
className="text-base"
dangerouslySetInnerHTML={{
__html: option.description
}}
/>}
</div>
)
return (
<Select
options={options.filter(opt => !excludeSet.some(ex => ex.id === opt.id))}
value={options.find(opt => {
return opt.id === selectedCustomSet
}) || null}
onChange={(selected: SingleValue<SelectOption>) => {
setSelectedCustomSet(selected?.id || '')
}}
formatOptionLabel={formatOptionLabel}
styles={customStyles}
placeholder={placeholder}
className="my-react-select-container"
classNamePrefix="my-react-select"
isSearchable
isClearable
/>
)
}

View File

@@ -1,9 +1,9 @@
"use client"
import { SendDataThroughProxy, SendDataToServer } from "@/lib/api"
import { SendDataThroughProxy, SendDataToServer } from "@/lib/api/api"
import useConnectStore from "@/stores/connectStore"
import useUserDataStore from "@/stores/userDataStore"
import { converterToFreeSRJson } from "./converterToFreeSRJson"
import { psResponseSchema } from "@/zod"
import { pSResponseSchema } from "@/zod"
export const connectToPS = async (): Promise<{ success: boolean, message: string }> => {
const {
@@ -27,7 +27,7 @@ export const connectToPS = async (): Promise<{ success: boolean, message: string
} else if (response.error) {
return { success: false, message: response.error }
} else {
const parsed = psResponseSchema.safeParse(response.data)
const parsed = pSResponseSchema.safeParse(response.data)
if (!parsed.success) {
return { success: false, message: "Invalid response schema" }
}
@@ -53,8 +53,8 @@ export const syncDataToPS = async (): Promise<{ success: boolean, message: strin
password
} = useConnectStore.getState()
const {avatars, battle_config} = useUserDataStore.getState()
const data = converterToFreeSRJson(avatars, battle_config)
const {avatars, battle_type, moc_config, pf_config, as_config, ce_config} = useUserDataStore.getState()
const data = converterToFreeSRJson(avatars, battle_type, moc_config, pf_config, as_config, ce_config)
let urlQuery = serverUrl
if (!urlQuery.startsWith("http://") && !urlQuery.startsWith("https://")) {
@@ -69,7 +69,7 @@ export const syncDataToPS = async (): Promise<{ success: boolean, message: strin
} else if (response.error) {
return { success: false, message: response.error }
} else {
const parsed = psResponseSchema.safeParse(response.data)
const parsed = pSResponseSchema.safeParse(response.data)
if (!parsed.success) {
return { success: false, message: "Invalid response schema" }
}

View File

@@ -1,4 +1,4 @@
import { CharacterBasic, CharacterBasicRaw, LightConeBasic, LightConeBasicRaw, RelicBasic, RelicBasicEffect, RelicBasicRaw } from "@/types";
import { CharacterBasic, CharacterBasicRaw, EventBasic, EventBasicRaw, LightConeBasic, LightConeBasicRaw, MonsterBasic, MonsterBasicRaw, RelicBasic, RelicBasicEffect, RelicBasicRaw } from "@/types";
export function convertRelicSet(id: string, item: RelicBasicRaw): RelicBasic {
let lang = new Map<string, string>([
@@ -86,4 +86,43 @@ export function convertAvatar(id: string, item: CharacterBasicRaw): CharacterBas
return result;
}
export function convertEvent(id: string, item: EventBasicRaw): EventBasic {
let lang = new Map<string, string>([
['en', item.en],
['kr', item.kr],
['cn', item.cn],
['jp', item.jp]
]);
const result: EventBasic = {
lang: lang,
id: id,
begin: item.begin,
end: item.end,
live_begin: item.live_begin,
live_end: item.live_end,
param: item.param,
};
return result;
}
export function convertMonster(id: string, item: MonsterBasicRaw): MonsterBasic {
let lang = new Map<string, string>([
['en', item.en],
['kr', item.kr],
['cn', item.cn],
['jp', item.jp]
]);
const result: MonsterBasic = {
id: id,
rank: item.rank,
camp: item.camp,
icon: item.icon,
child: item.child,
weak: item.weak,
desc: item.desc,
lang: lang
};
return result;
}

View File

@@ -1,18 +1,69 @@
import { AvatarJson, AvatarStore, BattleConfigJson, BattleConfigStore, FreeSRJson, LightconeJson, RelicJson } from "@/types";
import useUserDataStore from "@/stores/userDataStore";
import { ASConfigStore, AvatarJson, AvatarStore, BattleConfigJson, CEConfigStore, FreeSRJson, LightconeJson, MOCConfigStore, PFConfigStore, RelicJson } from "@/types";
export function converterToFreeSRJson(avatars: Record<string, AvatarStore>, battle_config: BattleConfigStore): FreeSRJson {
export function converterToFreeSRJson(
avatars: Record<string, AvatarStore>,
battle_type: string,
moc_config: MOCConfigStore,
pf_config: PFConfigStore,
as_config: ASConfigStore,
ce_config: CEConfigStore,
): FreeSRJson {
const lightcones: LightconeJson[] = []
const relics: RelicJson[] = []
const battleJson: BattleConfigJson = {
battle_type: battle_config.battle_type,
blessings: battle_config.blessings,
custom_stats: battle_config.custom_stats,
cycle_count: battle_config.cycle_count,
stage_id: battle_config.stage_id,
path_resonance_id: battle_config.path_resonance_id,
monsters: battle_config.monsters,
let battleJson: BattleConfigJson
if (battle_type === "MOC") {
battleJson = {
battle_type: battle_type,
blessings: moc_config.blessings,
custom_stats: [],
cycle_count: moc_config.cycle_count,
stage_id: moc_config.stage_id,
path_resonance_id: 0,
monsters: moc_config.monsters,
}
} else if (battle_type === "PF") {
battleJson = {
battle_type: battle_type,
blessings: pf_config.blessings,
custom_stats: [],
cycle_count: pf_config.cycle_count,
stage_id: pf_config.stage_id,
path_resonance_id: 0,
monsters: pf_config.monsters,
}
} else if (battle_type === "AS") {
battleJson = {
battle_type: battle_type,
blessings: as_config.blessings,
custom_stats: [],
cycle_count: as_config.cycle_count,
stage_id: as_config.stage_id,
path_resonance_id: 0,
monsters: as_config.monsters,
}
} else if (battle_type === "CE") {
battleJson = {
battle_type: battle_type,
blessings: ce_config.blessings,
custom_stats: [],
cycle_count: ce_config.cycle_count,
stage_id: ce_config.stage_id,
path_resonance_id: 0,
monsters: ce_config.monsters,
}
} else {
battleJson = {
battle_type: battle_type,
blessings: [],
custom_stats: [],
cycle_count: 0,
stage_id: 0,
path_resonance_id: 0,
monsters: [],
}
}
const avatarsJson: { [key: string]: AvatarJson } = {}
let internalUidLightcone = 0
let internalUidRelic = 0

View File

@@ -1,5 +1,5 @@
import { listCurrentLanguage } from "@/lib/constant";
import { CharacterBasic, LightConeBasic } from "@/types";
import { CharacterBasic, EventBasic, LightConeBasic, MonsterBasic } from "@/types";
export function getNameChar(locale: string, data: CharacterBasic | undefined): string {
@@ -22,7 +22,7 @@ export function getNameChar(locale: string, data: CharacterBasic | undefined): s
return text
}
export function getNameLightcone(locale: string, data: LightConeBasic | undefined): string {
export function getLocaleName(locale: string, data: LightConeBasic | EventBasic | MonsterBasic | undefined): string {
if (!data) {
return ""
}

9
src/hooks/index.ts Normal file
View File

@@ -0,0 +1,9 @@
export * from "./useFetchConfigData";
export * from "./useChangeTheme";
export * from "./useFetchAvatarData";
export * from "./useFetchLightconeData";
export * from "./useFetchRelicData";
export * from "./useFetchMonsterData";
export * from "./useFetchPFData";
export * from "./useFetchMOCData";
export * from "./useFetchASData";

View File

@@ -0,0 +1,66 @@
"use client"
import { useQuery } from '@tanstack/react-query'
import { fetchASByIdsNative, getASEventListApi } from '@/lib/api'
import { useEffect } from 'react'
import { listCurrentLanguageApi } from '@/lib/constant'
import useLocaleStore from '@/stores/localeStore'
import { toast } from 'react-toastify'
import useEventStore from '@/stores/eventStore'
import { EventStageDetail } from '@/types'
export const useFetchASData = () => {
const { setASEvent, setMapASInfo } = useEventStore()
const { locale } = useLocaleStore()
const { data: dataAS, error: errorAS } = useQuery({
queryKey: ['asData'],
queryFn: getASEventListApi,
select: (data) => data.sort((a, b) => Number(b.id) - Number(a.id)),
staleTime: 1000 * 60 * 5,
})
const { data: dataASInfo, error: errorASInfo } = useQuery({
queryKey: ['asInfoData', locale],
queryFn: () =>
fetchASByIdsNative(
dataAS!.map((item) => item.id),
listCurrentLanguageApi[locale.toLowerCase()]
),
staleTime: 1000 * 60 * 5,
enabled: !!dataAS,
select: (data) => {
const newData = { ...data }
for (const key in newData) {
for (const item of newData[key].Level) {
item.EventIDList1 = item.EventIDList1.map((event: EventStageDetail) => ({
...event,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
MonsterList: event.MonsterList.map(({ $type, ...rest }) => rest)
}))
item.EventIDList2 = item.EventIDList2.map((event: EventStageDetail) => ({
...event,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
MonsterList: event.MonsterList.map(({ $type, ...rest }) => rest)
}))
}
}
return newData
},
});
useEffect(() => {
if (dataAS && !errorAS) {
setASEvent(dataAS)
} else if (errorAS) {
toast.error("Failed to load AS data")
}
}, [dataAS, errorAS, setASEvent])
useEffect(() => {
if (dataASInfo && !errorASInfo) {
setMapASInfo(dataASInfo)
} else if (errorASInfo) {
toast.error("Failed to load AS info data")
}
}, [dataASInfo, errorASInfo, setMapASInfo])
}

View File

@@ -0,0 +1,61 @@
"use client"
import { useQuery } from '@tanstack/react-query'
import { fetchCharactersByIdsNative, getCharacterListApi } from '@/lib/api'
import { useEffect } from 'react'
import { toast } from 'react-toastify'
import useAvatarStore from '@/stores/avatarStore'
import { listCurrentLanguageApi } from '@/lib/constant'
import useLocaleStore from '@/stores/localeStore'
import { converterToAvatarStore, getAvatarNotExist } from '@/helper'
import useUserDataStore from '@/stores/userDataStore'
export const useFetchAvatarData = () => {
const { setAvatars, avatars } = useUserDataStore()
const { setListAvatar, setAllMapAvatarInfo, setAvatarSelected } = useAvatarStore()
const { locale } = useLocaleStore()
const { data: dataAvatar, error: errorAvatar } = useQuery({
queryKey: ['avatarData'],
queryFn: getCharacterListApi,
select: (data) => data.sort((a, b) => {
const aHasRelease = typeof a.release === 'number';
const bHasRelease = typeof b.release === 'number';
if (!aHasRelease && !bHasRelease) return 0;
if (!aHasRelease) return -1;
if (!bHasRelease) return 1;
return b.release! - a.release!;
}),
staleTime: 1000 * 60 * 5,
})
const { data: dataAvatarInfo, error: errorAvatarInfo } = useQuery({
queryKey: ['avatarInfoData', locale],
queryFn: () =>
fetchCharactersByIdsNative(
dataAvatar!.map((item) => item.id),
listCurrentLanguageApi[locale.toLowerCase()]
),
staleTime: 1000 * 60 * 5,
enabled: !!dataAvatar,
});
useEffect(() => {
if (dataAvatar && !errorAvatar) {
setListAvatar(dataAvatar)
setAvatarSelected(dataAvatar[0])
const avatarStore = converterToAvatarStore(getAvatarNotExist())
if (Object.keys(avatarStore).length > 0) {
setAvatars({ ...avatars, ...avatarStore })
}
} else if (errorAvatar) {
toast.error("Failed to load avatar data")
}
}, [dataAvatar, errorAvatar, setAvatarSelected, setAvatars, setListAvatar, avatars])
useEffect(() => {
if (dataAvatarInfo && !errorAvatarInfo) {
setAllMapAvatarInfo(dataAvatarInfo)
} else if (errorAvatarInfo) {
toast.error("Failed to load avatar info data")
}
}, [dataAvatarInfo, errorAvatarInfo, setAllMapAvatarInfo])
}

View File

@@ -0,0 +1,37 @@
"use client"
import { useQuery } from '@tanstack/react-query'
import { getConfigMazeApi, getMainAffixApi, getSubAffixApi } from '@/lib/api'
import useAffixStore from '@/stores/affixStore'
import useMazeStore from '@/stores/mazeStore'
import { useEffect } from 'react'
import { toast } from 'react-toastify'
export const useFetchConfigData = () => {
const { setMapMainAffix, setMapSubAffix } = useAffixStore()
const { setAllMazeData } = useMazeStore()
const { data, error } = useQuery({
queryKey: ['initialConfigData'],
queryFn: async () => {
const [maze, main, sub] = await Promise.all([
getConfigMazeApi(),
getMainAffixApi(),
getSubAffixApi(),
])
return { maze, main, sub }
},
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (data && !error) {
setAllMazeData(data.maze)
setMapMainAffix(data.main)
setMapSubAffix(data.sub)
}
else if (error) {
toast.error("Failed to load initial config data")
}
}, [data, error, setAllMazeData, setMapMainAffix, setMapSubAffix])
}

View File

@@ -0,0 +1,47 @@
"use client"
import { useQuery } from '@tanstack/react-query'
import { fetchLightconesByIdsNative, getLightconeListApi } from '@/lib/api'
import { useEffect } from 'react'
import useLightconeStore from '@/stores/lightconeStore'
import { listCurrentLanguageApi } from '@/lib/constant'
import useLocaleStore from '@/stores/localeStore'
import { toast } from 'react-toastify'
export const useFetchLightconeData = () => {
const { setListLightcone, setAllMapLightconeInfo } = useLightconeStore()
const { locale } = useLocaleStore()
const { data: dataLightcone, error: errorLightcone } = useQuery({
queryKey: ['lightconeData'],
queryFn: getLightconeListApi,
select: (data) => data.sort((a, b) => Number(b.id) - Number(a.id)),
staleTime: 1000 * 60 * 5,
})
const { data: dataLightconeInfo, error: errorLightconeInfo } = useQuery({
queryKey: ['lightconeInfoData', locale],
queryFn: () =>
fetchLightconesByIdsNative(
dataLightcone!.map((item) => item.id),
listCurrentLanguageApi[locale.toLowerCase()]
),
staleTime: 1000 * 60 * 5,
enabled: !!dataLightcone,
});
useEffect(() => {
if (dataLightcone && !errorLightcone) {
setListLightcone(dataLightcone)
} else if (errorLightcone) {
toast.error("Failed to load lightcone data")
}
}, [dataLightcone, errorLightcone, setListLightcone])
useEffect(() => {
if (dataLightconeInfo && !errorLightconeInfo) {
setAllMapLightconeInfo(dataLightconeInfo)
} else if (errorLightconeInfo) {
toast.error("Failed to load lightcone info data")
}
}, [dataLightconeInfo, errorLightconeInfo, setAllMapLightconeInfo])
}

View File

@@ -0,0 +1,68 @@
"use client"
import { useQuery } from '@tanstack/react-query'
import { fetchMOCByIdsNative, getMOCEventListApi } from '@/lib/api'
import { useEffect } from 'react'
import { listCurrentLanguageApi } from '@/lib/constant'
import useLocaleStore from '@/stores/localeStore'
import { toast } from 'react-toastify'
import useEventStore from '@/stores/eventStore'
import { EventStageDetail, MocDetail } from '@/types'
export const useFetchMOCData = () => {
const { setMOCEvent, setMapMOCInfo } = useEventStore()
const { locale } = useLocaleStore()
const { data: dataMOC, error: errorMOC } = useQuery({
queryKey: ['mocData'],
queryFn: getMOCEventListApi,
select: (data) => data.sort((a, b) => Number(b.id) - Number(a.id)),
staleTime: 1000 * 60 * 5,
})
const { data: dataMOCInfo, error: errorMOCInfo } = useQuery({
queryKey: ['mocInfoData', locale],
queryFn: async () => {
const result = await fetchMOCByIdsNative(
dataMOC!.map((item) => item.id),
listCurrentLanguageApi[locale.toLowerCase()]
);
return result;
},
staleTime: 1000 * 60 * 5,
select: (data) => {
const newData = { ...data }
for (const key in newData) {
for (const item of newData[key]) {
item.EventIDList1 = item.EventIDList1.map((event: EventStageDetail) => ({
...event,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
MonsterList: event.MonsterList.map(({ $type, ...rest }) => rest)
}))
item.EventIDList2 = item.EventIDList2.map((event: EventStageDetail) => ({
...event,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
MonsterList: event.MonsterList.map(({ $type, ...rest }) => rest)
}))
}
}
return newData
},
enabled: !!dataMOC,
});
useEffect(() => {
if (dataMOC && !errorMOC) {
setMOCEvent(dataMOC)
} else if (errorMOC) {
toast.error("Failed to load MOC data")
}
}, [dataMOC, errorMOC, setMOCEvent])
useEffect(() => {
if (dataMOCInfo && !errorMOCInfo) {
setMapMOCInfo(dataMOCInfo as Record<string, MocDetail[]>)
} else if (errorMOCInfo) {
toast.error("Failed to load MOC info data")
}
}, [dataMOCInfo, errorMOCInfo, setMapMOCInfo])
}

View File

@@ -0,0 +1,37 @@
"use client"
import { useQuery } from '@tanstack/react-query'
import { getMonsterDetailApi, getMonsterListApi } from '@/lib/api'
import { useEffect } from 'react'
import { toast } from 'react-toastify'
import useMonsterStore from '@/stores/monsterStore'
export const useFetchMonsterData = () => {
const { setAllMapMonster, setListMonster, setAllMapMonsterInfo } = useMonsterStore()
const { data: dataMonster, error: errorMonster } = useQuery({
queryKey: ['monsterData'],
queryFn: getMonsterListApi,
staleTime: 1000 * 60 * 5,
})
const { data: dataMonsterInfo, error: errorMonsterInfo } = useQuery({
queryKey: ['monsterInfoData'],
queryFn: getMonsterDetailApi,
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (dataMonster && !errorMonster) {
setListMonster(dataMonster.list.sort((a, b) => Number(b.id) - Number(a.id)))
setAllMapMonster(dataMonster.map)
} else if (errorMonster) {
toast.error("Failed to load monster data")
}
}, [dataMonster, errorMonster, setAllMapMonster, setListMonster])
useEffect(() => {
if (dataMonsterInfo && !errorMonsterInfo) {
setAllMapMonsterInfo(dataMonsterInfo)
} else if (errorMonsterInfo) {
toast.error("Failed to load monster info data")
}
}, [dataMonsterInfo, errorMonsterInfo, setAllMapMonsterInfo])
}

View File

@@ -0,0 +1,66 @@
"use client"
import { useQuery } from '@tanstack/react-query'
import { fetchPFByIdsNative, getPFEventListApi } from '@/lib/api'
import { useEffect } from 'react'
import { listCurrentLanguageApi } from '@/lib/constant'
import useLocaleStore from '@/stores/localeStore'
import { toast } from 'react-toastify'
import useEventStore from '@/stores/eventStore'
import { EventStageDetail } from '@/types'
export const useFetchPFData = () => {
const { setPFEvent, setMapPFInfo } = useEventStore()
const { locale } = useLocaleStore()
const { data: dataPF, error: errorPF } = useQuery({
queryKey: ['pfData'],
queryFn: getPFEventListApi,
select: (data) => data.sort((a, b) => Number(b.id) - Number(a.id)),
staleTime: 1000 * 60 * 5,
})
const { data: dataPFInfo, error: errorPFInfo } = useQuery({
queryKey: ['pfInfoData', locale],
queryFn: () =>
fetchPFByIdsNative(
dataPF!.map((item) => item.id),
listCurrentLanguageApi[locale.toLowerCase()]
),
staleTime: 1000 * 60 * 5,
enabled: !!dataPF,
select: (data) => {
const newData = { ...data }
for (const key in newData) {
for (const item of newData[key].Level) {
item.EventIDList1 = item.EventIDList1.map((event: EventStageDetail) => ({
...event,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
MonsterList: event.MonsterList.map(({ $type, ...rest }) => rest)
}))
item.EventIDList2 = item.EventIDList2.map((event: EventStageDetail) => ({
...event,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
MonsterList: event.MonsterList.map(({ $type, ...rest }) => rest)
}))
}
}
return newData
},
});
useEffect(() => {
if (dataPF && !errorPF) {
setPFEvent(dataPF)
} else if (errorPF) {
toast.error("Failed to load PF data")
}
}, [dataPF, errorPF, setPFEvent])
useEffect(() => {
if (dataPFInfo && !errorPFInfo) {
setMapPFInfo(dataPFInfo)
} else if (errorPFInfo) {
toast.error("Failed to load PF info data")
}
}, [dataPFInfo, errorPFInfo, setMapPFInfo])
}

View File

@@ -0,0 +1,46 @@
"use client"
import { useQuery } from '@tanstack/react-query'
import { fetchRelicsByIdsNative, getRelicSetListApi } from '@/lib/api'
import { useEffect } from 'react'
import useRelicStore from '@/stores/relicStore'
import { listCurrentLanguageApi } from '@/lib/constant'
import useLocaleStore from '@/stores/localeStore'
import { toast } from 'react-toastify'
export const useFetchRelicData = () => {
const { setListRelic, setAllMapRelicInfo } = useRelicStore()
const { locale } = useLocaleStore()
const { data: dataRelic, error: errorRelic } = useQuery({
queryKey: ['relicData'],
queryFn: getRelicSetListApi,
staleTime: 1000 * 60 * 5,
})
const { data: dataRelicInfo, error: errorRelicInfo } = useQuery({
queryKey: ['relicInfoData', locale],
queryFn: () =>
fetchRelicsByIdsNative(
dataRelic!.map((item) => item.id),
listCurrentLanguageApi[locale.toLowerCase()]
),
staleTime: 1000 * 60 * 5,
enabled: !!dataRelic,
});
useEffect(() => {
if (dataRelic && !errorRelic) {
setListRelic(dataRelic)
} else if (errorRelic) {
toast.error("Failed to load relic data")
}
}, [dataRelic, errorRelic, setListRelic])
useEffect(() => {
if (dataRelicInfo && !errorRelicInfo) {
setAllMapRelicInfo(dataRelicInfo)
} else if (errorRelicInfo) {
toast.error("Failed to load relic info data")
}
}, [dataRelicInfo, errorRelicInfo, setAllMapRelicInfo])
}

218
src/lib/api/api.ts Normal file
View File

@@ -0,0 +1,218 @@
import { AffixDetail, ASDetail, CharacterDetail, ConfigMaze, FreeSRJson, LightConeDetail, MocDetail, PFDetail, PSResponse, RelicDetail } from "@/types";
import axios from 'axios';
import { pSResponseSchema } from "@/zod";
export async function getConfigMazeApi(): Promise<ConfigMaze> {
try {
const res = await axios.get<ConfigMaze>(
`/api/config-maze`,
{
headers: {
'Content-Type': 'application/json',
},
}
);
return res.data as ConfigMaze;
} catch (error: unknown) {
if (axios.isAxiosError(error)) {
console.log(`Error: ${error.response?.status} - ${error.message}`);
} else {
console.log(`Unexpected error: ${String(error)}`);
}
return {
Avatar: {},
MOC: {},
AS: {},
PF: {},
};
}
}
export async function getMainAffixApi(): Promise<Record<string, Record<string, AffixDetail>>> {
try {
const res = await axios.get<Record<string, Record<string, AffixDetail>>>(
`/api/main-affixes`,
{
headers: {
'Content-Type': 'application/json',
},
}
);
return res.data as Record<string, Record<string, AffixDetail>>;
} catch (error: unknown) {
if (axios.isAxiosError(error)) {
console.log(`Error: ${error.response?.status} - ${error.message}`);
} else {
console.log(`Unexpected error: ${String(error)}`);
}
return {};
}
}
export async function getSubAffixApi(): Promise<Record<string, Record<string, AffixDetail>>> {
try {
const res = await axios.get<Record<string, Record<string, AffixDetail>>>(
`/api/sub-affixes`,
{
headers: {
'Content-Type': 'application/json',
},
}
);
return res.data as Record<string, Record<string, AffixDetail>>;
} catch (error: unknown) {
if (axios.isAxiosError(error)) {
console.log(`Error: ${error.response?.status} - ${error.message}`);
} else {
console.log(`Unexpected error: ${String(error)}`);
}
return {};
}
}
export async function fetchCharacterByIdNative(id: string, locale: string): Promise<CharacterDetail | null> {
try {
const res = await axios.get<CharacterDetail>(`/api/${locale}/characters/${id}`);
return res.data;
} catch (error) {
console.error('Failed to fetch character:', error);
return null;
}
}
export async function fetchCharactersByIdsNative(ids: string[], locale: string): Promise<Record<string, CharacterDetail>> {
try {
const res = await axios.post<Record<string, CharacterDetail>>(`/api/${locale}/characters`, { charIds: ids });
return res.data;
} catch (error) {
console.error('Failed to fetch characters:', error);
return {};
}
}
export async function fetchLightconeByIdNative(id: string, locale: string): Promise<LightConeDetail | null> {
try {
const res = await axios.get<LightConeDetail>(`/api/${locale}/lightcones/${id}`);
return res.data;
} catch (error) {
console.error('Failed to fetch lightcone:', error);
return null;
}
}
export async function fetchLightconesByIdsNative(ids: string[], locale: string): Promise<Record<string, LightConeDetail>> {
try {
const res = await axios.post<Record<string, LightConeDetail>>(`/api/${locale}/lightcones`, { lightconeIds: ids });
return res.data;
} catch (error) {
console.error('Failed to fetch lightcones:', error);
return {};
}
}
export async function fetchRelicByIdNative(id: string, locale: string): Promise<RelicDetail | null> {
try {
const res = await axios.get<RelicDetail>(`/api/${locale}/relics/${id}`);
return res.data;
} catch (error) {
console.error('Failed to fetch relic:', error);
return null;
}
}
export async function fetchRelicsByIdsNative(ids: string[], locale: string): Promise<Record<string, RelicDetail>> {
try {
const res = await axios.post<Record<string, RelicDetail>>(`/api/${locale}/relics`, { relicIds: ids });
return res.data;
} catch (error) {
console.error('Failed to fetch relics:', error);
return {};
}
}
export async function fetchASByIdsNative(ids: string[], locale: string): Promise<Record<string, ASDetail> | null> {
try {
const res = await axios.post<Record<string, ASDetail>>(`/api/${locale}/as`, { asIds: ids });
return res.data;
} catch (error) {
console.error('Failed to fetch AS:', error);
return null;
}
}
export async function fetchASByIdNative(ids: string, locale: string): Promise<ASDetail | null> {
try {
const res = await axios.get<ASDetail>(`/api/${locale}/as/${ids}`);
return res.data;
} catch (error) {
console.error('Failed to fetch AS:', error);
return null;
}
}
export async function fetchPFByIdsNative(ids: string[], locale: string): Promise<Record<string, PFDetail> | null> {
try {
const res = await axios.post<Record<string, PFDetail>>(`/api/${locale}/pf`, { pfIds: ids });
return res.data;
} catch (error) {
console.error('Failed to fetch PF:', error);
return null;
}
}
export async function fetchPFByIdNative(ids: string, locale: string): Promise<PFDetail | null> {
try {
const res = await axios.get<PFDetail>(`/api/${locale}/pf/${ids}`);
return res.data;
} catch (error) {
console.error('Failed to fetch PF:', error);
return null;
}
}
export async function fetchMOCByIdsNative(ids: string[], locale: string): Promise<Record<string, MocDetail[]> | null> {
try {
const res = await axios.post<Record<string, MocDetail[]>>(`/api/${locale}/moc`, { mocIds: ids });
return res.data;
} catch (error) {
console.error('Failed to fetch MOC:', error);
return null;
}
}
export async function fetchMOCByIdNative(ids: string, locale: string): Promise<MocDetail[] | null> {
try {
const res = await axios.get<MocDetail[]>(`/api/${locale}/moc/${ids}`);
return res.data;
} catch (error) {
console.error('Failed to fetch MOC:', error);
return null;
}
}
export async function SendDataToServer(username: string, password: string, serverUrl: string, data: FreeSRJson | null): Promise<PSResponse | string> {
try {
const response = await axios.post(`${serverUrl}`, { username, password, data })
const parsed = pSResponseSchema.safeParse(response.data)
if (!parsed.success) {
return "Invalid response schema";
}
return parsed.data;
} catch (error: any) {
return error?.message || "Unknown error";
}
}
export async function SendDataThroughProxy({data}: {data: any}) {
try {
const response = await axios.post(`/api/proxy`, { ...data })
return response.data;
} catch (error: any) {
return error;
}
}

View File

@@ -1,102 +1,6 @@
import { CharacterBasic, CharacterBasicRaw } from "@/types/characterBasic";
import { AffixDetail, CharacterDetail, ConfigMaze, EnkaResponse, FreeSRJson, LightConeBasic, LightConeBasicRaw, LightConeDetail, PSResponse, RelicBasic, RelicBasicEffect, RelicBasicRaw, RelicDetail } from "@/types";
import axios from 'axios';
import { convertAvatar, convertLightcone, convertRelicSet } from "@/helper";
import { psResponseSchema } from "@/zod";
export async function getConfigMazeApi(): Promise<ConfigMaze> {
try {
const res = await axios.get<ConfigMaze>(
`/api/config-maze`,
{
headers: {
'Content-Type': 'application/json',
},
}
);
return res.data as ConfigMaze;
} catch (error: unknown) {
if (axios.isAxiosError(error)) {
console.log(`Error: ${error.response?.status} - ${error.message}`);
} else {
console.log(`Unexpected error: ${String(error)}`);
}
return {
Avatar: {},
MOC: {},
AS: {},
PF: {},
};
}
}
export async function getMainAffixApi(): Promise<Record<string, Record<string, AffixDetail>>> {
try {
const res = await axios.get<Record<string, Record<string, AffixDetail>>>(
`/api/main-affixes`,
{
headers: {
'Content-Type': 'application/json',
},
}
);
return res.data as Record<string, Record<string, AffixDetail>>;
} catch (error: unknown) {
if (axios.isAxiosError(error)) {
console.log(`Error: ${error.response?.status} - ${error.message}`);
} else {
console.log(`Unexpected error: ${String(error)}`);
}
return {};
}
}
export async function getSubAffixApi(): Promise<Record<string, Record<string, AffixDetail>>> {
try {
const res = await axios.get<Record<string, Record<string, AffixDetail>>>(
`/api/sub-affixes`,
{
headers: {
'Content-Type': 'application/json',
},
}
);
return res.data as Record<string, Record<string, AffixDetail>>;
} catch (error: unknown) {
if (axios.isAxiosError(error)) {
console.log(`Error: ${error.response?.status} - ${error.message}`);
} else {
console.log(`Unexpected error: ${String(error)}`);
}
return {};
}
}
export async function getCharacterInfoApi(avatarId: number, locale: string): Promise<CharacterDetail | null> {
try {
const res = await axios.get<CharacterDetail>(
`https://api.hakush.in/hsr/data/${locale}/character/${avatarId}.json`,
{
headers: {
'Content-Type': 'application/json',
},
}
);
return res.data as CharacterDetail;
} catch (error: unknown) {
if (axios.isAxiosError(error)) {
console.log(`Error: ${error.response?.status} - ${error.message}`);
} else {
console.log(`Unexpected error: ${String(error)}`);
}
return null;
}
}
import { convertAvatar, convertEvent, convertLightcone, convertMonster, convertRelicSet } from "@/helper";
import { ASDetail, CharacterBasic, CharacterBasicRaw, CharacterDetail, EventBasic, EventBasicRaw, LightConeBasic, LightConeBasicRaw, LightConeDetail, MocDetail, MonsterBasic, MonsterBasicRaw, MonsterDetail, PFDetail, RelicBasic, RelicBasicRaw, RelicDetail } from "@/types";
import axios from "axios";
export async function getLightconeInfoApi(lightconeId: number, locale: string): Promise<LightConeDetail | null> {
try {
@@ -142,63 +46,91 @@ export async function getRelicInfoApi(relicId: number, locale: string): Promise<
}
}
export async function fetchCharacterByIdNative(id: string, locale: string): Promise<CharacterDetail | null> {
export async function getCharacterInfoApi(avatarId: number, locale: string): Promise<CharacterDetail | null> {
try {
const res = await axios.get<CharacterDetail>(`/api/${locale}/characters/${id}`);
return res.data;
} catch (error) {
console.error('Failed to fetch character:', error);
const res = await axios.get<CharacterDetail>(
`https://api.hakush.in/hsr/data/${locale}/character/${avatarId}.json`,
{
headers: {
'Content-Type': 'application/json',
},
}
);
return res.data as CharacterDetail;
} catch (error: unknown) {
if (axios.isAxiosError(error)) {
console.log(`Error: ${error.response?.status} - ${error.message}`);
} else {
console.log(`Unexpected error: ${String(error)}`);
}
return null;
}
}
export async function fetchCharactersByIdsNative(ids: string[], locale: string): Promise<Record<string, CharacterDetail>> {
export async function getMOCEventInfoApi(eventId: number, locale: string): Promise<MocDetail[] | null> {
try {
const res = await axios.post<Record<string, CharacterDetail>>(`/api/${locale}/characters`, { charIds: ids });
return res.data;
} catch (error) {
console.error('Failed to fetch characters:', error);
return {};
}
}
const res = await axios.get<MocDetail[]>(
`https://api.hakush.in/hsr/data/${locale}/maze/${eventId}.json`,
{
headers: {
'Content-Type': 'application/json',
},
}
);
export async function fetchLightconeByIdNative(id: string, locale: string): Promise<LightConeDetail | null> {
try {
const res = await axios.get<LightConeDetail>(`/api/${locale}/lightcones/${id}`);
return res.data;
} catch (error) {
console.error('Failed to fetch lightcone:', error);
return res.data as MocDetail[];
} catch (error: unknown) {
if (axios.isAxiosError(error)) {
console.log(`Error: ${error.response?.status} - ${error.message}`);
} else {
console.log(`Unexpected error: ${String(error)}`);
}
return null;
}
}
export async function fetchLightconesByIdsNative(ids: string[], locale: string): Promise<Record<string, LightConeDetail>> {
export async function getASEventInfoApi(eventId: number, locale: string): Promise<ASDetail | null> {
try {
const res = await axios.post<Record<string, LightConeDetail>>(`/api/${locale}/lightcones`, { lightconeIds: ids });
return res.data;
} catch (error) {
console.error('Failed to fetch lightcones:', error);
return {};
}
}
const res = await axios.get<ASDetail>(
`https://api.hakush.in/hsr/data/${locale}/boss/${eventId}.json`,
{
headers: {
'Content-Type': 'application/json',
},
}
);
export async function fetchRelicByIdNative(id: string, locale: string): Promise<RelicDetail | null> {
try {
const res = await axios.get<RelicDetail>(`/api/${locale}/relics/${id}`);
return res.data;
} catch (error) {
console.error('Failed to fetch relic:', error);
return res.data as ASDetail;
} catch (error: unknown) {
if (axios.isAxiosError(error)) {
console.log(`Error: ${error.response?.status} - ${error.message}`);
} else {
console.log(`Unexpected error: ${String(error)}`);
}
return null;
}
}
export async function fetchRelicsByIdsNative(ids: string[], locale: string): Promise<Record<string, RelicDetail>> {
export async function getPFEventInfoApi(eventId: number, locale: string): Promise<PFDetail | null> {
try {
const res = await axios.post<Record<string, RelicDetail>>(`/api/${locale}/relics`, { relicIds: ids });
return res.data;
} catch (error) {
console.error('Failed to fetch relics:', error);
return {};
const res = await axios.get<PFDetail>(
`https://api.hakush.in/hsr/data/${locale}/story/${eventId}.json`,
{
headers: {
'Content-Type': 'application/json',
},
}
);
return res.data as PFDetail;
} catch (error: unknown) {
if (axios.isAxiosError(error)) {
console.log(`Error: ${error.response?.status} - ${error.message}`);
} else {
console.log(`Unexpected error: ${String(error)}`);
}
return null;
}
}
@@ -274,24 +206,124 @@ export async function getRelicSetListApi(): Promise<RelicBasic[]> {
}
}
export async function SendDataToServer(username: string, password: string, serverUrl: string, data: FreeSRJson | null): Promise<PSResponse | string> {
export async function getMOCEventListApi(): Promise<EventBasic[]> {
try {
const response = await axios.post(`${serverUrl}`, { username, password, data })
const parsed = psResponseSchema.safeParse(response.data)
if (!parsed.success) {
return "Invalid response schema";
const res = await axios.get<Record<string, EventBasicRaw>>(
'https://api.hakush.in/hsr/data/maze.json',
{
headers: {
'Content-Type': 'application/json',
},
}
);
const data = new Map(Object.entries(res.data));
return Array.from(data.entries()).map(([id, it]) => convertEvent(id, it));
} catch (error: unknown) {
if (axios.isAxiosError(error)) {
console.log(`Error: ${error.response?.status} - ${error.message}`);
} else {
console.log(`Unexpected error: ${String(error)}`);
}
return parsed.data;
} catch (error: any) {
return error?.message || "Unknown error";
return [];
}
}
export async function SendDataThroughProxy({data}: {data: any}) {
export async function getASEventListApi(): Promise<EventBasic[]> {
try {
const response = await axios.post(`/api/proxy`, { ...data })
return response.data;
} catch (error: any) {
return error;
const res = await axios.get<Record<string, EventBasicRaw>>(
'https://api.hakush.in/hsr/data/maze_boss.json',
{
headers: {
'Content-Type': 'application/json',
},
}
);
const data = new Map(Object.entries(res.data));
return Array.from(data.entries()).map(([id, it]) => convertEvent(id, it));
} catch (error: unknown) {
if (axios.isAxiosError(error)) {
console.log(`Error: ${error.response?.status} - ${error.message}`);
} else {
console.log(`Unexpected error: ${String(error)}`);
}
return [];
}
}
}
export async function getPFEventListApi(): Promise<EventBasic[]> {
try {
const res = await axios.get<Record<string, EventBasicRaw>>(
'https://api.hakush.in/hsr/data/maze_extra.json',
{
headers: {
'Content-Type': 'application/json',
},
}
);
const data = new Map(Object.entries(res.data));
return Array.from(data.entries()).map(([id, it]) => convertEvent(id, it));
} catch (error: unknown) {
if (axios.isAxiosError(error)) {
console.log(`Error: ${error.response?.status} - ${error.message}`);
} else {
console.log(`Unexpected error: ${String(error)}`);
}
return [];
}
}
export async function getMonsterListApi(): Promise<{list: MonsterBasic[], map: Record<string, MonsterBasic>}> {
try {
const res = await axios.get<Record<string, MonsterBasicRaw>>(
'https://api.hakush.in/hsr/data/monster.json',
{
headers: {
'Content-Type': 'application/json',
},
}
);
const dataArr = Array.from(Object.entries(res.data)).map(([id, it]) => convertMonster(id, it));
const dataMap = Object.fromEntries(Object.entries(res.data).map(([id, it]) => [id, convertMonster(id, it)]));
return {
list: dataArr,
map: dataMap
};
} catch (error: unknown) {
if (axios.isAxiosError(error)) {
console.log(`Error: ${error.response?.status} - ${error.message}`);
} else {
console.log(`Unexpected error: ${String(error)}`);
}
return {list: [], map: {}};
}
}
export async function getMonsterDetailApi(): Promise<Record<string, MonsterDetail> | null> {
try {
const res = await axios.get<Record<string, MonsterDetail>>(
`https://api.hakush.in/hsr/data/monstervalue.json`,
{
headers: {
'Content-Type': 'application/json',
},
}
);
return res.data;
} catch (error: unknown) {
if (axios.isAxiosError(error)) {
console.log(`Error: ${error.response?.status} - ${error.message}`);
} else {
console.log(`Unexpected error: ${String(error)}`);
}
return null;
}
}

2
src/lib/api/index.ts Normal file
View File

@@ -0,0 +1,2 @@
export * from "./api";
export * from "./hakushi";

View File

@@ -0,0 +1,51 @@
import fs from 'fs';
import path from 'path';
import { ASDetail } from '@/types';
import { getASEventInfoApi } from '../api';
const DATA_DIR = path.join(process.cwd(), 'data');
const asFileCache: Record<string, Record<string, ASDetail>> = {};
export let asMap: Record<string, ASDetail> = {};
function getJsonFilePath(locale: string): string {
return path.join(DATA_DIR, `as.${locale}.json`);
}
function loadFromFileIfExists(locale: string): Record<string, ASDetail> | null {
if (asFileCache[locale]) return asFileCache[locale];
const filePath = getJsonFilePath(locale);
if (fs.existsSync(filePath)) {
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8')) as Record<string, ASDetail>;
asFileCache[locale] = data;
return data;
}
return null;
}
export async function loadAS(charIds: string[], locale: string): Promise<Record<string, ASDetail>> {
const fileData = loadFromFileIfExists(locale);
const fileIds = fileData ? Object.keys(fileData) : [];
if (fileData && charIds.every(id => fileIds.includes(id))) {
asMap = fileData;
return asMap;
}
const result: Record<string, ASDetail> = {};
await Promise.all(
charIds.map(async id => {
const info = await getASEventInfoApi(Number(id), locale);
if (info) result[id] = info;
})
);
fs.mkdirSync(DATA_DIR, { recursive: true });
const filePath = getJsonFilePath(locale);
fs.writeFileSync(filePath, JSON.stringify(result, null, 2), 'utf-8');
asFileCache[locale] = result;
asMap = result;
return result;
}

Some files were not shown because too many files have changed in this diff Show More