update wave, cycle, damage type ...
This commit is contained in:
@@ -4,11 +4,10 @@
|
|||||||
"description": "Analytics tool for Veritas"
|
"description": "Analytics tool for Veritas"
|
||||||
},
|
},
|
||||||
"DataAnalysisPage": {
|
"DataAnalysisPage": {
|
||||||
"useSkill": "Use skill",
|
"useSkill": "Use Ability",
|
||||||
"totalDamage": "Total damage",
|
"totalDamage": "Total damage",
|
||||||
"damagePerAV": "Damage per AV",
|
"damagePerAV": "Damage/Action value",
|
||||||
"totalAV": "Total action value",
|
"totalAV": "Total action value",
|
||||||
"damagerPerCycle": "Damage Per Cycle",
|
|
||||||
"skillType": "Skill Type",
|
"skillType": "Skill Type",
|
||||||
"skillName": "Skill Name",
|
"skillName": "Skill Name",
|
||||||
"actionValue": "Action Value",
|
"actionValue": "Action Value",
|
||||||
@@ -70,6 +69,40 @@
|
|||||||
"physical": "Physical",
|
"physical": "Physical",
|
||||||
"quantum": "Quantum",
|
"quantum": "Quantum",
|
||||||
"thunder": "Thunder",
|
"thunder": "Thunder",
|
||||||
"wind": "Wind"
|
"wind": "Wind",
|
||||||
|
"cycle": "Cycle",
|
||||||
|
"wave": "Wave",
|
||||||
|
"hp": "Hp",
|
||||||
|
"atk": "Atk",
|
||||||
|
"speed": "Speed",
|
||||||
|
"critRate": "Crit Rate",
|
||||||
|
"critDmg": "Crit Dmg",
|
||||||
|
"breakEffect": "Break Effect",
|
||||||
|
"effectRes": "Effect Res",
|
||||||
|
"energyRegenerationRate": "Energy Regeneration Rate",
|
||||||
|
"effectHitRate": "Effect Hit Rate",
|
||||||
|
"outgoingHealingBoost": "Outgoing Healing Boost",
|
||||||
|
"fireDmgBoost": "Fire damage boost",
|
||||||
|
"iceDmgBoost": "Ice damage Boost",
|
||||||
|
"imaginaryDmgBoost": "Imaginary damage boost",
|
||||||
|
"physicalDmgBoost": "Physical damage boost",
|
||||||
|
"quantumDmgBoost": "Quantum damage boost",
|
||||||
|
"thunderDmgBoost": "Thunder damage boost",
|
||||||
|
"windDmgBoost": "Wind damage boost",
|
||||||
|
"pursued": "Additional damage",
|
||||||
|
"true damage": "True damage",
|
||||||
|
"follow-up": "Follow-up Damage",
|
||||||
|
"elemental damage": "Break and Super break damage",
|
||||||
|
"dot": "Damage over time ",
|
||||||
|
"damagePerCycle": "Damage per Cycle",
|
||||||
|
"damagePerCycleAndWave": "Damage per Cycle and Wave",
|
||||||
|
"damagePerWave": "Damage per Wave",
|
||||||
|
"lastTurn": "Last turn",
|
||||||
|
"qte": "QTE Skill",
|
||||||
|
"mazenormal": "MazeNormal",
|
||||||
|
"level": "Level",
|
||||||
|
"relics": "Relics",
|
||||||
|
"eidolons": "Eidolons",
|
||||||
|
"lightcones": "Lightcones"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,32 +4,31 @@
|
|||||||
"description": "Veritasの分析ツール"
|
"description": "Veritasの分析ツール"
|
||||||
},
|
},
|
||||||
"DataAnalysisPage": {
|
"DataAnalysisPage": {
|
||||||
"useSkill": "スキル使用",
|
"useSkill": "スキルを使用",
|
||||||
"totalDamage": "総ダメージ",
|
"totalDamage": "総ダメージ",
|
||||||
"damagePerAV": "AVあたりのダメージ",
|
"damagePerAV": "ダメージ/行動値",
|
||||||
"totalAV": "総AV",
|
"totalAV": "総行動値",
|
||||||
"damagerPerCycle": "サイクルごとのダメージ",
|
|
||||||
"skillType": "スキルタイプ",
|
"skillType": "スキルタイプ",
|
||||||
"skillName": "スキル名",
|
"skillName": "スキル名",
|
||||||
"actionValue": "アクション値",
|
"actionValue": "行動値",
|
||||||
"character": "キャラクター",
|
"character": "キャラクター",
|
||||||
"id": "ID",
|
"id": "ID",
|
||||||
"path": "パス",
|
"path": "運命",
|
||||||
"damageByActionValue": "行動値によるダメージ",
|
|
||||||
"rarity": "レアリティ",
|
"rarity": "レアリティ",
|
||||||
"element": "エレメンタル",
|
"element": "属性",
|
||||||
"totalTurn": "総ターン",
|
"totalTurn": "総ターン数",
|
||||||
"technique": "テクニック",
|
"technique": "秘技",
|
||||||
"talent": "タレント",
|
"talent": "天賦",
|
||||||
"basic": "基本攻撃",
|
"basic": "通常攻撃",
|
||||||
"skill": "スキル",
|
"skill": "スキル",
|
||||||
"ultimate": "アルティメット",
|
"ultimate": "必殺技",
|
||||||
"servant": "サーヴァント",
|
"servant": "召喚",
|
||||||
"skillDamageBreakdown": "スキルダメージ内訳",
|
"skillDamageBreakdown": "スキルダメージ内訳",
|
||||||
"skillUsageDistribution": "スキル使用分布",
|
"skillUsageDistribution": "スキル使用分布",
|
||||||
"damageOverTime": "時間経過によるダメージ",
|
"damageOverTime": "継続ダメージ",
|
||||||
"damage": "ダメージ",
|
"damage": "ダメージ",
|
||||||
"cumulativeDamage": "累積ダメージ",
|
"cumulativeDamage": "累積ダメージ",
|
||||||
|
"damageByActionValue": "行動値別ダメージ",
|
||||||
"characterInformation": "キャラクター情報",
|
"characterInformation": "キャラクター情報",
|
||||||
"turnDetail": "ターン詳細",
|
"turnDetail": "ターン詳細",
|
||||||
"damageDetails": "ダメージ詳細",
|
"damageDetails": "ダメージ詳細",
|
||||||
@@ -37,28 +36,28 @@
|
|||||||
"chartInfo": "チャート情報",
|
"chartInfo": "チャート情報",
|
||||||
"actionBar": "アクションタイムライン",
|
"actionBar": "アクションタイムライン",
|
||||||
"lineupInfo": "編成情報",
|
"lineupInfo": "編成情報",
|
||||||
"loadData": "バトルデータをロード",
|
"loadData": "戦闘データを読み込む",
|
||||||
"exportData": "バトルデータをエクスポート",
|
"exportData": "戦闘データをエクスポート",
|
||||||
"connectSetting": "接続設定",
|
"connectSetting": "接続設定",
|
||||||
"connected": "接続済み",
|
"connected": "接続済み",
|
||||||
"unconnected": "接続していない",
|
"unconnected": "未接続",
|
||||||
"socketConnection": "ソケット接続",
|
"socketConnection": "ソケット接続",
|
||||||
"connectionType": "接続タイプ",
|
"connectionType": "接続タイプ",
|
||||||
"status": "ステータス",
|
"status": "ステータス",
|
||||||
"connect": "接続",
|
"connect": "接続する",
|
||||||
"checkGameConnect": "ゲーム接続を確認",
|
"checkGameConnect": "ゲーム接続を確認",
|
||||||
"other": "その他",
|
"other": "その他",
|
||||||
"host": "ホスト",
|
"host": "ホスト",
|
||||||
"port": "ポート",
|
"port": "ポート",
|
||||||
"hostPlaceHolder": "ホストを入力",
|
"hostPlaceHolder": "ホストを入力",
|
||||||
"portPlaceHolder": "ポート番号を入力",
|
"portPlaceHolder": "ポート番号を入力",
|
||||||
"noDamageDetail": "ダメージの詳細は利用できません",
|
"noDamageDetail": "ダメージ詳細がありません",
|
||||||
"noCharactersInLineup": "編成にキャラクターがいません",
|
"noCharactersInLineup": "編成にキャラクターがいません",
|
||||||
"noTurns": "まだターンがありません",
|
"noTurns": "まだターンがありません",
|
||||||
"type": "タイプ",
|
"type": "タイプ",
|
||||||
"warrior": "破壊",
|
"warrior": "壊滅",
|
||||||
"knight": "保護",
|
"knight": "存護",
|
||||||
"mage": "博識",
|
"mage": "知恵",
|
||||||
"priest": "豊穣",
|
"priest": "豊穣",
|
||||||
"rouge": "巡狩",
|
"rouge": "巡狩",
|
||||||
"shaman": "調和",
|
"shaman": "調和",
|
||||||
@@ -66,10 +65,44 @@
|
|||||||
"memory": "記憶",
|
"memory": "記憶",
|
||||||
"fire": "炎",
|
"fire": "炎",
|
||||||
"ice": "氷",
|
"ice": "氷",
|
||||||
"imaginary": "想像",
|
"imaginary": "虚数",
|
||||||
"physical": "物理",
|
"physical": "物理",
|
||||||
"quantum": "量子",
|
"quantum": "量子",
|
||||||
"thunder": "雷",
|
"thunder": "雷",
|
||||||
"wind": "風"
|
"wind": "風",
|
||||||
|
"cycle": "サイクル",
|
||||||
|
"wave": "ウェーブ",
|
||||||
|
"hp": "HP",
|
||||||
|
"atk": "攻撃力",
|
||||||
|
"speed": "速度",
|
||||||
|
"critRate": "会心率",
|
||||||
|
"critDmg": "会心ダメージ",
|
||||||
|
"breakEffect": "撃破特効",
|
||||||
|
"effectRes": "効果抵抗",
|
||||||
|
"energyRegenerationRate": "エネルギー回復効率",
|
||||||
|
"effectHitRate": "効果命中率",
|
||||||
|
"outgoingHealingBoost": "治癒量上昇",
|
||||||
|
"fireDmgBoost": "炎属性ダメージ上昇",
|
||||||
|
"iceDmgBoost": "氷属性ダメージ上昇",
|
||||||
|
"imaginaryDmgBoost": "虚数属性ダメージ上昇",
|
||||||
|
"physicalDmgBoost": "物理属性ダメージ上昇",
|
||||||
|
"quantumDmgBoost": "量子属性ダメージ上昇",
|
||||||
|
"thunderDmgBoost": "雷属性ダメージ上昇",
|
||||||
|
"windDmgBoost": "風属性ダメージ上昇",
|
||||||
|
"pursued": "追加ダメージ",
|
||||||
|
"true damage": "貫通ダメージ",
|
||||||
|
"follow-up": "追撃ダメージ",
|
||||||
|
"elemental damage": "撃破・超撃破ダメージ",
|
||||||
|
"dot": "継続ダメージ",
|
||||||
|
"damagePerCycle": "サイクル毎のダメージ",
|
||||||
|
"damagePerCycleAndWave": "サイクル・ウェーブ毎のダメージ",
|
||||||
|
"damagePerWave": "ウェーブ毎のダメージ",
|
||||||
|
"lastTurn": "最後のターン",
|
||||||
|
"qte": "QTEスキル",
|
||||||
|
"mazenormal": "通常迷宮",
|
||||||
|
"level": "レベル",
|
||||||
|
"relics": "遺物",
|
||||||
|
"eidolons": "星魂",
|
||||||
|
"lightcones": "光円錐"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,39 +5,38 @@
|
|||||||
},
|
},
|
||||||
"DataAnalysisPage": {
|
"DataAnalysisPage": {
|
||||||
"useSkill": "스킬 사용",
|
"useSkill": "스킬 사용",
|
||||||
"totalDamage": "총 피해",
|
"totalDamage": "총 피해량",
|
||||||
"damagePerAV": "AV당 피해",
|
"damagePerAV": "피해량/행동값",
|
||||||
"totalAV": "총 AV",
|
"totalAV": "총 행동값",
|
||||||
"damagerPerCycle": "주기당 피해",
|
|
||||||
"skillType": "스킬 유형",
|
"skillType": "스킬 유형",
|
||||||
"skillName": "스킬 이름",
|
"skillName": "스킬 이름",
|
||||||
"actionValue": "액션 값",
|
"actionValue": "행동값",
|
||||||
"character": "캐릭터",
|
"character": "캐릭터",
|
||||||
"id": "ID",
|
"id": "ID",
|
||||||
"path": "경로",
|
"path": "운명",
|
||||||
"rarity": "희귀도",
|
"rarity": "희귀도",
|
||||||
"damageByActionValue": "행동값에 따른 피해",
|
"element": "속성",
|
||||||
"element": "원소",
|
"totalTurn": "총 턴 수",
|
||||||
"totalTurn": "총 턴",
|
"technique": "비전",
|
||||||
"technique": "기술",
|
|
||||||
"talent": "재능",
|
"talent": "재능",
|
||||||
"basic": "기본 공격",
|
"basic": "기본 공격",
|
||||||
"skill": "스킬",
|
"skill": "스킬",
|
||||||
"ultimate": "궁극기",
|
"ultimate": "필살기",
|
||||||
"servant": "서번트",
|
"servant": "소환",
|
||||||
"skillDamageBreakdown": "스킬 피해 분석",
|
"skillDamageBreakdown": "스킬 피해 세부사항",
|
||||||
"skillUsageDistribution": "스킬 사용 분포",
|
"skillUsageDistribution": "스킬 사용 분포",
|
||||||
"damageOverTime": "시간 경과에 따른 피해",
|
"damageOverTime": "시간당 피해",
|
||||||
"damage": "피해",
|
"damage": "피해",
|
||||||
"cumulativeDamage": "누적 피해",
|
"cumulativeDamage": "누적 피해",
|
||||||
|
"damageByActionValue": "행동값별 피해",
|
||||||
"characterInformation": "캐릭터 정보",
|
"characterInformation": "캐릭터 정보",
|
||||||
"turnDetail": "턴 상세",
|
"turnDetail": "턴 상세 정보",
|
||||||
"damageDetails": "피해 상세",
|
"damageDetails": "피해 세부사항",
|
||||||
"cycleCount": "사이클 수",
|
"cycleCount": "사이클 수",
|
||||||
"chartInfo": "차트 정보",
|
"chartInfo": "차트 정보",
|
||||||
"actionBar": "액션 타임라인",
|
"actionBar": "행동 타임라인",
|
||||||
"lineupInfo": "라인업 정보",
|
"lineupInfo": "편성 정보",
|
||||||
"loadData": "전투 데이터 로드",
|
"loadData": "전투 데이터 불러오기",
|
||||||
"exportData": "전투 데이터 내보내기",
|
"exportData": "전투 데이터 내보내기",
|
||||||
"connectSetting": "연결 설정",
|
"connectSetting": "연결 설정",
|
||||||
"connected": "연결됨",
|
"connected": "연결됨",
|
||||||
@@ -52,13 +51,13 @@
|
|||||||
"port": "포트",
|
"port": "포트",
|
||||||
"hostPlaceHolder": "호스트 입력",
|
"hostPlaceHolder": "호스트 입력",
|
||||||
"portPlaceHolder": "포트 번호 입력",
|
"portPlaceHolder": "포트 번호 입력",
|
||||||
"noDamageDetail": "데미지 세부 정보가 없습니다",
|
"noDamageDetail": "피해 세부사항 없음",
|
||||||
"noCharactersInLineup": "라인업에 캐릭터가 없습니다",
|
"noCharactersInLineup": "편성에 캐릭터 없음",
|
||||||
"noTurns": "아직 턴이 없습니다",
|
"noTurns": "아직 턴이 없습니다",
|
||||||
"type": "타입 ",
|
"type": "유형",
|
||||||
"warrior": "파괴",
|
"warrior": "파괴",
|
||||||
"knight": "보존",
|
"knight": "보존",
|
||||||
"mage": "지식",
|
"mage": "지혜",
|
||||||
"priest": "풍요",
|
"priest": "풍요",
|
||||||
"rouge": "사냥",
|
"rouge": "사냥",
|
||||||
"shaman": "조화",
|
"shaman": "조화",
|
||||||
@@ -70,6 +69,40 @@
|
|||||||
"physical": "물리",
|
"physical": "물리",
|
||||||
"quantum": "양자",
|
"quantum": "양자",
|
||||||
"thunder": "번개",
|
"thunder": "번개",
|
||||||
"wind": "바람"
|
"wind": "바람",
|
||||||
|
"cycle": "사이클",
|
||||||
|
"wave": "웨이브",
|
||||||
|
"hp": "HP",
|
||||||
|
"atk": "공격력",
|
||||||
|
"speed": "속도",
|
||||||
|
"critRate": "치명타 확률",
|
||||||
|
"critDmg": "치명타 피해",
|
||||||
|
"breakEffect": "파괴 효과",
|
||||||
|
"effectRes": "효과 저항",
|
||||||
|
"energyRegenerationRate": "에너지 회복 속도",
|
||||||
|
"effectHitRate": "효과 적중률",
|
||||||
|
"outgoingHealingBoost": "치유량 증가",
|
||||||
|
"fireDmgBoost": "불 속성 피해 증가",
|
||||||
|
"iceDmgBoost": "얼음 속성 피해 증가",
|
||||||
|
"imaginaryDmgBoost": "허수 속성 피해 증가",
|
||||||
|
"physicalDmgBoost": "물리 속성 피해 증가",
|
||||||
|
"quantumDmgBoost": "양자 속성 피해 증가",
|
||||||
|
"thunderDmgBoost": "번개 속성 피해 증가",
|
||||||
|
"windDmgBoost": "바람 속성 피해 증가",
|
||||||
|
"pursued": "추가 피해",
|
||||||
|
"true damage": "관통 피해",
|
||||||
|
"follow-up": "추가 공격 피해",
|
||||||
|
"elemental damage": "파괴 및 초파괴 피해",
|
||||||
|
"dot": "지속 피해",
|
||||||
|
"damagePerCycle": "사이클당 피해",
|
||||||
|
"damagePerCycleAndWave": "사이클 및 웨이브당 피해",
|
||||||
|
"damagePerWave": "웨이브당 피해",
|
||||||
|
"lastTurn": "마지막 턴",
|
||||||
|
"qte": "QTE 스킬",
|
||||||
|
"mazenormal": "일반 미궁",
|
||||||
|
"level": "레벨",
|
||||||
|
"relics": "유물",
|
||||||
|
"eidolons": "에이돌론",
|
||||||
|
"lightcones": "광추"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,8 @@
|
|||||||
"DataAnalysisPage": {
|
"DataAnalysisPage": {
|
||||||
"useSkill": "Sử dụng kỹ năng",
|
"useSkill": "Sử dụng kỹ năng",
|
||||||
"totalDamage": "Tổng sát thương",
|
"totalDamage": "Tổng sát thương",
|
||||||
"damagePerAV": "Sát thương mỗi AV",
|
"damagePerAV": "Sát thương/giá trị hành động",
|
||||||
"totalAV": "Tổng AV",
|
"totalAV": "Tổng giá trị hành động",
|
||||||
"damagerPerCycle": "Sát thương mỗi vòng",
|
|
||||||
"skillType": "Loại kỹ năng",
|
"skillType": "Loại kỹ năng",
|
||||||
"skillName": "Tên kỹ năng",
|
"skillName": "Tên kỹ năng",
|
||||||
"actionValue": "Giá trị hành động",
|
"actionValue": "Giá trị hành động",
|
||||||
@@ -70,6 +69,40 @@
|
|||||||
"physical": "Vật Lý",
|
"physical": "Vật Lý",
|
||||||
"quantum": "Lượng Tử",
|
"quantum": "Lượng Tử",
|
||||||
"thunder": "Lôi",
|
"thunder": "Lôi",
|
||||||
"wind": "Phong"
|
"wind": "Phong",
|
||||||
|
"cycle": "Vòng",
|
||||||
|
"wave": "Đợt quái",
|
||||||
|
"hp": "Máu",
|
||||||
|
"atk": "Tấn công",
|
||||||
|
"speed": "Tốc độ",
|
||||||
|
"critRate": "Tỉ lệ chí mạng",
|
||||||
|
"critDmg": "Sát thương chí mạng",
|
||||||
|
"breakEffect": "Tấn công kích phá",
|
||||||
|
"effectRes": "Kháng hiệu ứng",
|
||||||
|
"energyRegenerationRate": "Hiệu quả nạp",
|
||||||
|
"effectHitRate": "Chinh xác hiệu ứng",
|
||||||
|
"outgoingHealingBoost": "Hiệu quả trị liệu",
|
||||||
|
"fireDmgBoost": "Tăng sát thương hỏa",
|
||||||
|
"iceDmgBoost": "Tăng sát thương băng",
|
||||||
|
"imaginaryDmgBoost": "Tăng sát thương số ảo",
|
||||||
|
"physicalDmgBoost": "Tăng sát thương vật lý",
|
||||||
|
"quantumDmgBoost": "Tăng sát thương lượng tử",
|
||||||
|
"thunderDmgBoost": "Tăng sát thương lôi",
|
||||||
|
"windDmgBoost": "Tăng sát thương phong",
|
||||||
|
"pursued": "Sát thương kèm theo",
|
||||||
|
"true damage": "Sát thương chuẩn",
|
||||||
|
"follow-up": "Sát thương theo sau",
|
||||||
|
"elemental damage": "Sát thương phá vỡ và siêu phá vỡ",
|
||||||
|
"dot": "Sát Thương Duy Trì",
|
||||||
|
"damagePerCycle": "Sát thương mỗi vòng",
|
||||||
|
"damagePerCycleAndWave": "Sát thương mỗi vòng và đợt",
|
||||||
|
"damagePerWave": "Sát thương mỗi đợt",
|
||||||
|
"lastTurn": "Lượt cuối",
|
||||||
|
"qte": "Kỹ năng QTE",
|
||||||
|
"mazenormal": "Mê cung (Thường)",
|
||||||
|
"level": "Cấp độ",
|
||||||
|
"relics": "Di vật",
|
||||||
|
"eidolons": "Tinh hồn",
|
||||||
|
"lightcones": "Nón Ánh sáng"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
"totalDamage": "总伤害",
|
"totalDamage": "总伤害",
|
||||||
"damagePerAV": "每行动值伤害",
|
"damagePerAV": "每行动值伤害",
|
||||||
"totalAV": "总行动值",
|
"totalAV": "总行动值",
|
||||||
"damagerPerCycle": "每轮伤害",
|
|
||||||
"skillType": "技能类型",
|
"skillType": "技能类型",
|
||||||
"skillName": "技能名称",
|
"skillName": "技能名称",
|
||||||
"actionValue": "行动值",
|
"actionValue": "行动值",
|
||||||
@@ -70,6 +69,40 @@
|
|||||||
"physical": "物理",
|
"physical": "物理",
|
||||||
"quantum": "量子",
|
"quantum": "量子",
|
||||||
"thunder": "雷",
|
"thunder": "雷",
|
||||||
"wind": "风"
|
"wind": "风",
|
||||||
|
"cycle": "回合",
|
||||||
|
"wave": "波次",
|
||||||
|
"hp": "生命值",
|
||||||
|
"atk": "攻击力",
|
||||||
|
"speed": "速度",
|
||||||
|
"critRate": "暴击率",
|
||||||
|
"critDmg": "暴击伤害",
|
||||||
|
"breakEffect": "击破特攻",
|
||||||
|
"effectRes": "效果抵抗",
|
||||||
|
"energyRegenerationRate": "能量回复效率",
|
||||||
|
"effectHitRate": "效果命中",
|
||||||
|
"outgoingHealingBoost": "治疗量加成",
|
||||||
|
"fireDmgBoost": "火属性伤害提高",
|
||||||
|
"iceDmgBoost": "冰属性伤害提高",
|
||||||
|
"imaginaryDmgBoost": "虚数属性伤害提高",
|
||||||
|
"physicalDmgBoost": "物理属性伤害提高",
|
||||||
|
"quantumDmgBoost": "量子属性伤害提高",
|
||||||
|
"thunderDmgBoost": "雷属性伤害提高",
|
||||||
|
"windDmgBoost": "风属性伤害提高",
|
||||||
|
"pursued": "附加伤害",
|
||||||
|
"true damage": "真实伤害",
|
||||||
|
"follow-up": "追加攻击",
|
||||||
|
"elemental damage": "击破与超击破伤害",
|
||||||
|
"dot": "持续伤害 ",
|
||||||
|
"damagePerCycle": "每轮伤害",
|
||||||
|
"damagePerCycleAndWave": "每轮/波伤害",
|
||||||
|
"damagePerWave": "每波伤害",
|
||||||
|
"lastTurn": "最后一回合",
|
||||||
|
"qte": "QTE技能",
|
||||||
|
"mazenormal": "普通迷宫",
|
||||||
|
"level": "等级",
|
||||||
|
"relics": "遗器",
|
||||||
|
"eidolons": "星魂",
|
||||||
|
"lightcones": "光锥"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,12 @@ const nextConfig: NextConfig = {
|
|||||||
protocol: 'http',
|
protocol: 'http',
|
||||||
hostname: 'localhost',
|
hostname: 'localhost',
|
||||||
pathname: '**',
|
pathname: '**',
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
protocol: 'https',
|
||||||
|
hostname: 'api.hakush.in',
|
||||||
|
pathname: '**',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
eslint: {
|
eslint: {
|
||||||
|
|||||||
45
package-lock.json
generated
45
package-lock.json
generated
@@ -12,6 +12,7 @@
|
|||||||
"chart.js": "^4.4.9",
|
"chart.js": "^4.4.9",
|
||||||
"chartjs-plugin-datalabels": "^2.2.0",
|
"chartjs-plugin-datalabels": "^2.2.0",
|
||||||
"framer-motion": "^12.7.4",
|
"framer-motion": "^12.7.4",
|
||||||
|
"html-to-image": "^1.11.13",
|
||||||
"next": "15.3.1",
|
"next": "15.3.1",
|
||||||
"next-intl": "^4.0.2",
|
"next-intl": "^4.0.2",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
@@ -31,6 +32,7 @@
|
|||||||
"daisyui": "^5.0.27",
|
"daisyui": "^5.0.27",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "15.3.1",
|
"eslint-config-next": "15.3.1",
|
||||||
|
"tailwind-scrollbar": "^4.0.2",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
@@ -1845,6 +1847,13 @@
|
|||||||
"undici-types": "~6.19.2"
|
"undici-types": "~6.19.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/prismjs": {
|
||||||
|
"version": "1.26.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz",
|
||||||
|
"integrity": "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/react": {
|
"node_modules/@types/react": {
|
||||||
"version": "19.1.2",
|
"version": "19.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.2.tgz",
|
||||||
@@ -4220,6 +4229,12 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/html-to-image": {
|
||||||
|
"version": "1.11.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.13.tgz",
|
||||||
|
"integrity": "sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/ignore": {
|
"node_modules/ignore": {
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||||
@@ -5765,6 +5780,20 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/prism-react-renderer": {
|
||||||
|
"version": "2.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.4.1.tgz",
|
||||||
|
"integrity": "sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/prismjs": "^1.26.0",
|
||||||
|
"clsx": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/prop-types": {
|
"node_modules/prop-types": {
|
||||||
"version": "15.8.1",
|
"version": "15.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||||
@@ -6556,6 +6585,22 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tailwind-scrollbar": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/tailwind-scrollbar/-/tailwind-scrollbar-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-wAQiIxAPqk0MNTPptVe/xoyWi27y+NRGnTwvn4PQnbvB9kp8QUBiGl/wsfoVBHnQxTmhXJSNt9NHTmcz9EivFA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prism-react-renderer": "^2.4.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.13.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"tailwindcss": "4.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tailwindcss": {
|
"node_modules/tailwindcss": {
|
||||||
"version": "4.1.4",
|
"version": "4.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.4.tgz",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"chart.js": "^4.4.9",
|
"chart.js": "^4.4.9",
|
||||||
"chartjs-plugin-datalabels": "^2.2.0",
|
"chartjs-plugin-datalabels": "^2.2.0",
|
||||||
"framer-motion": "^12.7.4",
|
"framer-motion": "^12.7.4",
|
||||||
|
"html-to-image": "^1.11.13",
|
||||||
"next": "15.3.1",
|
"next": "15.3.1",
|
||||||
"next-intl": "^4.0.2",
|
"next-intl": "^4.0.2",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
@@ -32,6 +33,7 @@
|
|||||||
"daisyui": "^5.0.27",
|
"daisyui": "^5.0.27",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "15.3.1",
|
"eslint-config-next": "15.3.1",
|
||||||
|
"tailwind-scrollbar": "^4.0.2",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,27 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
|
||||||
@plugin "daisyui" {
|
@plugin "daisyui" {
|
||||||
themes: winter --default, night --prefersdark, cupcake, coffee;
|
themes: winter --default, night --prefersdark, cupcake, coffee;
|
||||||
}
|
}
|
||||||
|
@plugin 'tailwind-scrollbar' {
|
||||||
|
nocompatible: true;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background-color: #9ca3af;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-button {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export default async function RootLayout({
|
|||||||
<NextIntlClientProvider messages={messages}>
|
<NextIntlClientProvider messages={messages}>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<ClientThemeWrapper>
|
<ClientThemeWrapper>
|
||||||
<div className="h-full min-h-screen">
|
<div className="h-full">
|
||||||
<Header></Header>
|
<Header></Header>
|
||||||
{children}
|
{children}
|
||||||
<Footer></Footer>
|
<Footer></Footer>
|
||||||
|
|||||||
@@ -18,9 +18,8 @@ export default function Home() {
|
|||||||
totalAV,
|
totalAV,
|
||||||
totalDamage,
|
totalDamage,
|
||||||
damagePerAV,
|
damagePerAV,
|
||||||
|
turnHistory
|
||||||
} = useBattleDataStore();
|
} = useBattleDataStore();
|
||||||
const [modeBar, setModeBar] = useState<0 | 1 | 2>(1);
|
|
||||||
const [modeLine, setModeLine] = useState<0 | 1>(1);
|
|
||||||
const [expandedCharts, setExpandedCharts] = useState<string[]>([]);
|
const [expandedCharts, setExpandedCharts] = useState<string[]>([]);
|
||||||
|
|
||||||
const toggleExpand = (chartId: string) => {
|
const toggleExpand = (chartId: string) => {
|
||||||
@@ -51,47 +50,44 @@ export default function Home() {
|
|||||||
<ActionBar />
|
<ActionBar />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-span-12 md:col-span-6 lg:col-span-8 xl:col-span-8 max-h-[90vh] flex flex-col h-full overflow-auto">
|
<div className="col-span-12 md:col-span-6 lg:col-span-8 xl:col-span-8 max-h-[92vh] flex flex-col h-full overflow-auto ">
|
||||||
<div className="grid grid-cols-3 gap-2 mb-3">
|
|
||||||
|
<div className="grid grid-cols-2 gap-2 mb-3">
|
||||||
<div className="p-2 text-base lg:text-lg xl:text-xl rounded bg-primary text-primary-content text-center shadow-md">
|
<div className="p-2 text-base lg:text-lg xl:text-xl rounded bg-primary text-primary-content text-center shadow-md">
|
||||||
{transI18n("totalDamage")}: {Number(totalDamage).toFixed(2)}
|
{transI18n("totalDamage")}
|
||||||
|
<div>{Number(totalDamage).toFixed(2)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-2 text-base lg:text-lg xl:text-xl rounded bg-secondary text-secondary-content text-center shadow-md">
|
<div className="p-2 text-base lg:text-lg xl:text-xl rounded bg-secondary text-secondary-content text-center shadow-md">
|
||||||
{transI18n("totalAV")}: {Number(totalAV).toFixed(2)}
|
{transI18n("totalAV")}
|
||||||
|
<div>{Number(totalAV).toFixed(2)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-2 text-base lg:text-lg xl:text-xl rounded bg-accent text-accent-content text-center shadow-md">
|
<div className="p-2 text-base lg:text-lg xl:text-xl rounded bg-accent text-accent-content text-center shadow-md">
|
||||||
{transI18n("damagePerAV")}: {Number(damagePerAV).toFixed(2)}
|
{transI18n("damagePerAV")}
|
||||||
|
<div>{Number(damagePerAV).toFixed(2)}</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-2 text-base lg:text-lg xl:text-xl rounded bg-warning text-warning-content text-center shadow-md">
|
||||||
|
{transI18n("totalTurn")}
|
||||||
|
<div>{turnHistory.filter(it => it.avatarId && it.avatarId != -1).length}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div className="rounded-lg p-2 shadow-md flex-grow">
|
<div className="rounded-lg p-2 shadow-md flex-grow">
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||||
|
|
||||||
{expandedCharts.includes('chart1') ? (
|
<div
|
||||||
<div key="chart1-expanded" className="lg:col-span-2 bg-base-200 rounded-lg p-2 shadow-md relative">
|
key={expandedCharts.includes('chart1') ? 'chart1-expanded' : 'chart1-normal'}
|
||||||
|
className={`bg-base-200 rounded-lg p-2 shadow-md relative ${expandedCharts.includes('chart1') ? 'lg:col-span-2' : ''}`}>
|
||||||
<div className="absolute top-2 left-2 z-10">
|
<div className="absolute top-2 left-2 z-10">
|
||||||
<button
|
<button
|
||||||
className="btn btn-sm btn-circle btn-ghost"
|
className="btn btn-sm btn-circle btn-ghost"
|
||||||
onClick={() => toggleExpand('chart1')}
|
onClick={() => toggleExpand('chart1')}
|
||||||
>
|
>
|
||||||
−
|
{expandedCharts.includes('chart1') ? '−' : '⤢'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<DamagePerAvatarForAll />
|
<DamagePerAvatarForAll />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<div key="chart1-normal" className="bg-base-200 rounded-lg p-2 shadow-md relative">
|
|
||||||
<div className="absolute top-2 left-2 z-10">
|
|
||||||
<button
|
|
||||||
className="btn btn-sm btn-circle btn-ghost"
|
|
||||||
onClick={() => toggleExpand('chart1')}
|
|
||||||
>
|
|
||||||
⤢
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<DamagePerAvatarForAll />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={`bg-base-200 rounded-lg p-2 shadow-md relative ${expandedCharts.includes('chart2') ? 'lg:col-span-2' : ''}`}
|
className={`bg-base-200 rounded-lg p-2 shadow-md relative ${expandedCharts.includes('chart2') ? 'lg:col-span-2' : ''}`}
|
||||||
@@ -105,45 +101,24 @@ export default function Home() {
|
|||||||
{expandedCharts.includes('chart2') ? '−' : '⤢'}
|
{expandedCharts.includes('chart2') ? '−' : '⤢'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end gap-2">
|
<MultiCharLineChart />
|
||||||
{[0, 1].map((m) => (
|
|
||||||
<button
|
|
||||||
key={m}
|
|
||||||
onClick={() => setModeLine(m as 0 | 1)}
|
|
||||||
className={`btn btn-sm ${modeLine === m ? "btn-accent" : "btn-ghost"}`}
|
|
||||||
>
|
|
||||||
{transI18n("type")} {m}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<MultiCharLineChart mode={modeLine} />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{expandedCharts.includes('chart3') ? (
|
|
||||||
<div key="chart3-expanded" className="lg:col-span-2 max-h-[70vh] bg-base-200 rounded-lg p-2 shadow-md relative">
|
<div
|
||||||
|
className={`bg-base-200 rounded-lg p-2 shadow-md relative ${expandedCharts.includes('chart3') ? 'lg:col-span-2' : ''}`}
|
||||||
|
key={expandedCharts.includes('chart3') ? 'chart3-expanded' : 'chart3-normal'}
|
||||||
|
>
|
||||||
<div className="absolute top-2 left-2 z-10">
|
<div className="absolute top-2 left-2 z-10">
|
||||||
<button
|
<button
|
||||||
className="btn btn-sm btn-circle btn-ghost"
|
className="btn btn-sm btn-circle btn-ghost"
|
||||||
onClick={() => toggleExpand('chart3')}
|
onClick={() => toggleExpand('chart3')}
|
||||||
>
|
>
|
||||||
−
|
{expandedCharts.includes('chart3') ? '−' : '⤢'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<DamagePercentChartForAll />
|
<DamagePercentChartForAll />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<div key="chart3-normal" className="bg-base-200 max-h-[40vh] rounded-lg p-2 shadow-md relative">
|
|
||||||
<div className="absolute top-2 left-2 z-10">
|
|
||||||
<button
|
|
||||||
className="btn btn-sm btn-circle btn-ghost"
|
|
||||||
onClick={() => toggleExpand('chart3')}
|
|
||||||
>
|
|
||||||
⤢
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<DamagePercentChartForAll />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={`bg-base-200 rounded-lg p-2 shadow-md relative ${expandedCharts.includes('chart4') ? 'lg:col-span-2' : ''}`}
|
className={`bg-base-200 rounded-lg p-2 shadow-md relative ${expandedCharts.includes('chart4') ? 'lg:col-span-2' : ''}`}
|
||||||
@@ -157,18 +132,7 @@ export default function Home() {
|
|||||||
{expandedCharts.includes('chart4') ? '−' : '⤢'}
|
{expandedCharts.includes('chart4') ? '−' : '⤢'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end gap-2">
|
<DamagePerCycleForAll />
|
||||||
{[0, 1, 2].map((m) => (
|
|
||||||
<button
|
|
||||||
key={m}
|
|
||||||
onClick={() => setModeBar(m as 0 | 1 | 2)}
|
|
||||||
className={`btn btn-sm ${modeBar === m ? "btn-accent" : "btn-ghost"}`}
|
|
||||||
>
|
|
||||||
{transI18n("type")} {m}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<DamagePerCycleForAll mode={modeBar} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,18 +2,19 @@
|
|||||||
import useAvatarDataStore from "@/stores/avatarDataStore";
|
import useAvatarDataStore from "@/stores/avatarDataStore";
|
||||||
import useBattleDataStore from "@/stores/battleDataStore";
|
import useBattleDataStore from "@/stores/battleDataStore";
|
||||||
import useLocaleStore from "@/stores/localeStore";
|
import useLocaleStore from "@/stores/localeStore";
|
||||||
import { AvatarType } from "@/types";
|
import {attackTypeToString, AvatarHakushiType} from "@/types";
|
||||||
import { TurnBattleInfo } from "@/types/mics";
|
import { SkillBattleInfo } from "@/types/mics";
|
||||||
import { useEffect, useState, useRef } from "react";
|
import { useEffect, useState, useRef } from "react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { getNameChar } from "@/helper";
|
import { getNameChar } from "@/helper";
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
export default function ActionBar() {
|
export default function ActionBar() {
|
||||||
const [selectTurn, setSelectTurn] = useState<TurnBattleInfo | null>(null);
|
const [selectTurn, setSelectTurn] = useState<SkillBattleInfo | null>(null);
|
||||||
const [selectAvatar, setSelectAvatar] = useState<AvatarType | null>(null);
|
const [selectAvatar, setSelectAvatar] = useState<AvatarHakushiType | null>(null);
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
const { turnHistory } = useBattleDataStore();
|
const { skillHistory, turnHistory, cycleIndex, waveIndex, maxWave } = useBattleDataStore();
|
||||||
const { listAvatar } = useAvatarDataStore();
|
const { listAvatar } = useAvatarDataStore();
|
||||||
const { locale } = useLocaleStore();
|
const { locale } = useLocaleStore();
|
||||||
const transI18n = useTranslations("DataAnalysisPage");
|
const transI18n = useTranslations("DataAnalysisPage");
|
||||||
@@ -27,7 +28,7 @@ export default function ActionBar() {
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleShow = (modalId: string, avatar: AvatarType, turn: TurnBattleInfo) => {
|
const handleShow = (modalId: string, avatar: AvatarHakushiType, turn: SkillBattleInfo) => {
|
||||||
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
|
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
|
||||||
if (modal) {
|
if (modal) {
|
||||||
setSelectAvatar(avatar);
|
setSelectAvatar(avatar);
|
||||||
@@ -61,10 +62,10 @@ export default function ActionBar() {
|
|||||||
|
|
||||||
// Scroll to the bottom when new turns are added
|
// Scroll to the bottom when new turns are added
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (turnListRef.current && turnHistory.length > 0) {
|
if (turnListRef.current && skillHistory.length > 0) {
|
||||||
turnListRef.current.scrollTop = turnListRef.current.scrollHeight;
|
turnListRef.current.scrollTop = turnListRef.current.scrollHeight;
|
||||||
}
|
}
|
||||||
}, [turnHistory.length]);
|
}, [skillHistory.length]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 md:p-1 rounded-lg shadow-lg w-full h-full">
|
<div className="p-4 md:p-1 rounded-lg shadow-lg w-full h-full">
|
||||||
@@ -76,49 +77,31 @@ export default function ActionBar() {
|
|||||||
>
|
>
|
||||||
{transI18n("actionBar")}
|
{transI18n("actionBar")}
|
||||||
</motion.h2>
|
</motion.h2>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-1 xl:grid-cols-2 items-center justify-items-center w-full gap-2 mb-2 mx-2">
|
||||||
|
<div className="badge badge-soft badge-info gap-2 text-lg py-3">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="size-6">
|
||||||
|
<path fillRule="evenodd" d="M3 2.25a.75.75 0 0 1 .75.75v.54l1.838-.46a9.75 9.75 0 0 1 6.725.738l.108.054A8.25 8.25 0 0 0 18 4.524l3.11-.732a.75.75 0 0 1 .917.81 47.784 47.784 0 0 0 .005 10.337.75.75 0 0 1-.574.812l-3.114.733a9.75 9.75 0 0 1-6.594-.77l-.108-.054a8.25 8.25 0 0 0-5.69-.625l-2.202.55V21a.75.75 0 0 1-1.5 0V3A.75.75 0 0 1 3 2.25Z" clipRule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
<span>{waveIndex}/{maxWave}</span>
|
||||||
|
</div>
|
||||||
|
<div className="badge badge-soft badge-error gap-2 text-lg py-3 items-center">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512" fill="currentColor" className="size-6">
|
||||||
|
<path d="M0 32C0 14.3 14.3 0 32 0L64 0 320 0l32 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l0 11c0 42.4-16.9 83.1-46.9 113.1L237.3 256l67.9 67.9c30 30 46.9 70.7 46.9 113.1l0 11c17.7 0 32 14.3 32 32s-14.3 32-32 32l-32 0L64 512l-32 0c-17.7 0-32-14.3-32-32s14.3-32 32-32l0-11c0-42.4 16.9-83.1 46.9-113.1L146.7 256 78.9 188.1C48.9 158.1 32 117.4 32 75l0-11C14.3 64 0 49.7 0 32zM96 64l0 11c0 25.5 10.1 49.9 28.1 67.9L192 210.7l67.9-67.9c18-18 28.1-42.4 28.1-67.9l0-11L96 64zm0 384l192 0 0-11c0-25.5-10.1-49.9-28.1-67.9L192 301.3l-67.9 67.9c-18 18-28.1 42.4-28.1 67.9l0 11z" />
|
||||||
|
</svg>
|
||||||
|
<span>{cycleIndex}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
ref={turnListRef}
|
ref={turnListRef}
|
||||||
className="flex md:block px-2 md:px-0 w-full pt-2 border-t-2 border-accent overflow-x-auto md:overflow-x-hidden md:overflow-y-auto max-h-[90vh] custom-scrollbar"
|
className="flex md:block px-2 md:px-0 w-full pt-2 border-t-2 border-accent overflow-x-auto md:overflow-x-hidden md:overflow-y-auto max-h-[90vh] lg:max-h-[80vh]"
|
||||||
>
|
>
|
||||||
<style jsx>{`
|
|
||||||
.custom-scrollbar {
|
|
||||||
scrollbar-width: thin;
|
|
||||||
scrollbar-color: hsl(var(--p)) hsl(var(--b3));
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-scrollbar::-webkit-scrollbar {
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-scrollbar::-webkit-scrollbar-track {
|
|
||||||
background: hsl(var(--b3));
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
|
||||||
background: hsl(var(--p));
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: hsl(var(--pf));
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-scrollbar::-webkit-scrollbar-button {
|
|
||||||
display: none;
|
|
||||||
height: 0;
|
|
||||||
width: 0;
|
|
||||||
}
|
|
||||||
`}</style>
|
|
||||||
|
|
||||||
<div className="flex flex-nowrap md:grid md:grid-cols-1 gap-2 w-fit md:w-full">
|
<div className="flex flex-nowrap md:grid md:grid-cols-1 gap-2 w-fit md:w-full">
|
||||||
{turnHistory.length === 0 ? (
|
{skillHistory.length === 0 ? (
|
||||||
<div className="flex items-center justify-center h-full w-full">
|
<div className="flex items-center justify-center h-full w-full">
|
||||||
<p className="text-base-content opacity-50">{transI18n("noTurns")}</p>
|
<p className="text-base-content opacity-50">{transI18n("noTurns")}</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
turnHistory.map((turn, index) => {
|
skillHistory.map((turn, index) => {
|
||||||
const data = listAvatar.find(it => it.id === turn.avatarId.toString());
|
const data = listAvatar.find(it => it.id === turn.avatarId.toString());
|
||||||
if (!data) return null;
|
if (!data) return null;
|
||||||
const text = getNameChar(locale, data);
|
const text = getNameChar(locale, data);
|
||||||
@@ -135,10 +118,12 @@ export default function ActionBar() {
|
|||||||
>
|
>
|
||||||
<div className="avatar">
|
<div className="avatar">
|
||||||
<div className="w-12 h-12 rounded-full border-2 flex items-center justify-center bg-base-300 border-cyan-400 border-l-4">
|
<div className="w-12 h-12 rounded-full border-2 flex items-center justify-center bg-base-300 border-cyan-400 border-l-4">
|
||||||
<img
|
|
||||||
|
<Image
|
||||||
src={`https://api.hakush.in/hsr/UI/avatarshopicon/${data.id}.webp`}
|
src={`https://api.hakush.in/hsr/UI/avatarshopicon/${data.id}.webp`}
|
||||||
alt={text}
|
alt={text}
|
||||||
loading="lazy"
|
width={32}
|
||||||
|
height={32}
|
||||||
className="w-8 h-8 object-contain"
|
className="w-8 h-8 object-contain"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -148,7 +133,7 @@ export default function ActionBar() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 justify-center gap-2 py-2 w-full">
|
<div className="grid grid-cols-1 justify-center gap-2 py-2 w-full">
|
||||||
<div className="bg-local text-primary text-xs max-w-full">
|
<div className="bg-local text-primary text-xs max-w-full">
|
||||||
{`${transI18n("useSkill")}: ${transI18n(turn.skillType.toLowerCase())}`}
|
{`${transI18n("useSkill")}: ${transI18n(attackTypeToString(turn.skillType).toLowerCase())}`}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-primary text-xs max-w-full">
|
<div className="text-primary text-xs max-w-full">
|
||||||
{`${transI18n("totalDamage")}: ${turn.totalDamage.toFixed(2)}`}
|
{`${transI18n("totalDamage")}: ${turn.totalDamage.toFixed(2)}`}
|
||||||
@@ -189,7 +174,7 @@ export default function ActionBar() {
|
|||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.5 }}
|
transition={{ duration: 0.5 }}
|
||||||
className="mt-4 w-full custom-scrollbar"
|
className="mt-4 w-full"
|
||||||
>
|
>
|
||||||
<div className="bg-base-200 rounded-lg p-4 shadow-md mb-4">
|
<div className="bg-base-200 rounded-lg p-4 shadow-md mb-4">
|
||||||
<h4 className="text-lg font-semibold mb-2 text-pink-500">{transI18n("characterInformation")}</h4>
|
<h4 className="text-lg font-semibold mb-2 text-pink-500">{transI18n("characterInformation")}</h4>
|
||||||
@@ -205,10 +190,11 @@ export default function ActionBar() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-center items-center">
|
<div className="flex justify-center items-center">
|
||||||
<img
|
<Image
|
||||||
loading="lazy"
|
|
||||||
src={`https://api.hakush.in/hsr/UI/avatarshopicon/${selectAvatar.id}.webp`}
|
src={`https://api.hakush.in/hsr/UI/avatarshopicon/${selectAvatar.id}.webp`}
|
||||||
alt={getNameChar(locale, selectAvatar)}
|
alt={getNameChar(locale, selectAvatar)}
|
||||||
|
width={80}
|
||||||
|
height={80}
|
||||||
className="h-20 w-20 object-cover rounded-full border-2 border-purple-500 shadow-lg shadow-purple-500/20"
|
className="h-20 w-20 object-cover rounded-full border-2 border-purple-500 shadow-lg shadow-purple-500/20"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -218,7 +204,7 @@ export default function ActionBar() {
|
|||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||||
<div className="bg-base-200 rounded-lg p-4 shadow-md">
|
<div className="bg-base-200 rounded-lg p-4 shadow-md">
|
||||||
<h4 className="text-lg font-semibold mb-2 text-cyan-500 border-b border-cyan-300/30 pb-1">{transI18n("skillType")}</h4>
|
<h4 className="text-lg font-semibold mb-2 text-cyan-500 border-b border-cyan-300/30 pb-1">{transI18n("skillType")}</h4>
|
||||||
<p className="mt-2">{transI18n(selectTurn?.skillType.toLowerCase())}</p>
|
<p className="mt-2">{transI18n(attackTypeToString(selectTurn?.skillType).toLowerCase())}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-base-200 rounded-lg p-4 shadow-md">
|
<div className="bg-base-200 rounded-lg p-4 shadow-md">
|
||||||
<h4 className="text-lg font-semibold mb-2 text-cyan-500 border-b border-cyan-300/30 pb-1">{transI18n("skillName")}</h4>
|
<h4 className="text-lg font-semibold mb-2 text-cyan-500 border-b border-cyan-300/30 pb-1">{transI18n("skillName")}</h4>
|
||||||
@@ -226,10 +212,21 @@ export default function ActionBar() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||||
|
<div className="bg-base-200 rounded-lg p-4 shadow-md">
|
||||||
|
<h4 className="text-lg font-semibold mb-2 text-cyan-500 border-b border-cyan-300/30 pb-1">{transI18n("cycle")}</h4>
|
||||||
|
<p className="mt-2">{turnHistory[selectTurn?.turnBattleId].cycleIndex}</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-base-200 rounded-lg p-4 shadow-md">
|
||||||
|
<h4 className="text-lg font-semibold mb-2 text-cyan-500 border-b border-cyan-300/30 pb-1">{transI18n("wave")}</h4>
|
||||||
|
<p className="mt-2">{turnHistory[selectTurn?.turnBattleId].waveIndex}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||||
<div className="bg-base-200 rounded-lg p-4 shadow-md">
|
<div className="bg-base-200 rounded-lg p-4 shadow-md">
|
||||||
<h4 className="text-lg font-semibold mb-2 text-cyan-500 border-b border-cyan-300/30 pb-1">{transI18n("actionValue")}</h4>
|
<h4 className="text-lg font-semibold mb-2 text-cyan-500 border-b border-cyan-300/30 pb-1">{transI18n("actionValue")}</h4>
|
||||||
<p className="mt-2">{selectTurn?.actionValue.toFixed(2)}</p>
|
<p className="mt-2">{turnHistory[selectTurn?.turnBattleId].actionValue.toFixed(2)}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-base-200 rounded-lg p-4 shadow-md">
|
<div className="bg-base-200 rounded-lg p-4 shadow-md">
|
||||||
<h4 className="text-lg font-semibold mb-2 text-purple-500 border-b border-purple-300/30 pb-1">{transI18n("totalDamage")}</h4>
|
<h4 className="text-lg font-semibold mb-2 text-purple-500 border-b border-purple-300/30 pb-1">{transI18n("totalDamage")}</h4>
|
||||||
@@ -239,15 +236,26 @@ export default function ActionBar() {
|
|||||||
|
|
||||||
<div className="bg-base-200 rounded-lg p-4 shadow-md mb-4">
|
<div className="bg-base-200 rounded-lg p-4 shadow-md mb-4">
|
||||||
<h4 className="text-lg font-semibold mb-2 text-cyan-500 border-b border-cyan-300/30 pb-1">{transI18n("damageDetails")}</h4>
|
<h4 className="text-lg font-semibold mb-2 text-cyan-500 border-b border-cyan-300/30 pb-1">{transI18n("damageDetails")}</h4>
|
||||||
{selectTurn?.damageDetail && selectTurn.damageDetail.length > 0 ? (
|
{selectTurn?.damageDetail?.length > 0 ? (
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-2 mt-3">
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-3 mt-3">
|
||||||
{selectTurn.damageDetail.map((detail, idx) => (
|
{selectTurn.damageDetail.map((detail, idx) => (
|
||||||
<p key={idx} className="py-1 px-2 rounded bg-base-300 text-sm">{detail.toFixed(2)}</p>
|
<div
|
||||||
|
key={idx}
|
||||||
|
className="flex flex-col items-start gap-1 p-3 rounded-lg shadow bg-base-200"
|
||||||
|
>
|
||||||
|
<span className="text-lg font-semibold text-primary">
|
||||||
|
{detail.damage.toFixed(2)}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs uppercase text-gray-500 tracking-wide">
|
||||||
|
{transI18n(attackTypeToString(detail?.damage_type).toLowerCase())}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="mt-2 italic opacity-70">{transI18n("noDamageDetail")}</p>
|
<p className="mt-2 italic opacity-70">{transI18n("noDamageDetail")}</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
import { getNameChar } from '@/helper';
|
import { getNameChar } from '@/helper';
|
||||||
import useLocaleStore from '@/stores/localeStore';
|
import useLocaleStore from '@/stores/localeStore';
|
||||||
import { AvatarType } from '@/types';
|
import { AvatarHakushiType } from '@/types';
|
||||||
|
|
||||||
interface CharacterCardProps {
|
interface CharacterCardProps {
|
||||||
data: AvatarType
|
data: AvatarHakushiType
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseRuby(text: string): string {
|
export function parseRuby(text: string): string {
|
||||||
|
|||||||
665
src/components/card/showCaseCard.tsx
Normal file
665
src/components/card/showCaseCard.tsx
Normal file
@@ -0,0 +1,665 @@
|
|||||||
|
"use client";
|
||||||
|
import React, { useState, useRef} from 'react';
|
||||||
|
|
||||||
|
export default function ShowCaseInfo() {
|
||||||
|
const [imageUrl, setImageUrl] = useState<string>('https://api.hakush.in/hsr/UI/avatardrawcard/1001.webp');
|
||||||
|
const [position, setPosition] = useState<{ x: number; y: number }>({ x: 0, y: 100 });
|
||||||
|
const ref = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col justify-start m-2">
|
||||||
|
|
||||||
|
<div className="overflow-auto">
|
||||||
|
<div ref={ref} className="relative min-h-[650px] w-[1400px] rounded-3xl overflow-hidden">
|
||||||
|
<img
|
||||||
|
src="https://cdn.jsdelivr.net/gh/picklejason/hsr-showcase@main/public/blur-bg.png"
|
||||||
|
alt="Showcase Background"
|
||||||
|
className="absolute inset-0 w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
<div className="absolute bottom-2 left-4 z-10">
|
||||||
|
<span className="shadow-black [text-shadow:1px_1px_2px_var(--tw-shadow-color)]"></span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<div
|
||||||
|
className="relative min-h-[650px] w-[28%]"
|
||||||
|
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="flex h-[650px] items-center"
|
||||||
|
style={{ cursor: 'grab' }}
|
||||||
|
>
|
||||||
|
<div className="overflow-hidden w-full h-full"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={imageUrl}
|
||||||
|
className="scale-[1.8] cursor-pointer object-cover"
|
||||||
|
alt="Character Preview"
|
||||||
|
style={{
|
||||||
|
position: 'relative',
|
||||||
|
top: `${position.y}px`,
|
||||||
|
left: `${position.x}px`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="absolute right-0 top-0 mr-[-15px] pt-3">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
{[
|
||||||
|
"https://worker-sparkling-dawn-a1c1.srv2.workers.dev/hsr.honeyhunterworld.com/img/eidolon/memory-of-you-eidolon_icon_small.webp",
|
||||||
|
"https://worker-sparkling-dawn-a1c1.srv2.workers.dev/hsr.honeyhunterworld.com/img/eidolon/memory-of-it-eidolon_icon_small.webp",
|
||||||
|
"https://worker-sparkling-dawn-a1c1.srv2.workers.dev/hsr.honeyhunterworld.com/img/eidolon/memory-of-everything-eidolon_icon_small.webp",
|
||||||
|
"https://worker-sparkling-dawn-a1c1.srv2.workers.dev/hsr.honeyhunterworld.com/img/eidolon/never-forfeit-again-eidolon_icon_small.webp",
|
||||||
|
"https://worker-sparkling-dawn-a1c1.srv2.workers.dev/hsr.honeyhunterworld.com/img/eidolon/never-forget-again-eidolon_icon_small.webp",
|
||||||
|
"https://worker-sparkling-dawn-a1c1.srv2.workers.dev/hsr.honeyhunterworld.com/img/eidolon/just-like-this-always-eidolon_icon_small.webp"
|
||||||
|
].map((src, index) => (
|
||||||
|
<div key={index} className="relative my-1 flex rounded-full border-2 border-neutral-300 bg-neutral-800">
|
||||||
|
<img src={src} alt="Rank Icon" className="h-auto w-10" />
|
||||||
|
<div className="absolute flex h-full w-full items-center justify-center rounded-full bg-neutral-800/70">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" className="size-5">
|
||||||
|
<path fillRule="evenodd" d="M10 1a4.5 4.5 0 0 0-4.5 4.5V9H5a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-6a2 2 0 0 0-2-2h-.5V5.5A4.5 4.5 0 0 0 10 1Zm3 8V5.5a3 3 0 1 0-6 0V9h6Z" clipRule="evenodd"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="relative flex min-h-[650px] w-[72%] flex-row items-center gap-3.5 rounded-r-3xl pl-10">
|
||||||
|
<img
|
||||||
|
src="https://cdn.jsdelivr.net/gh/picklejason/hsr-showcase@main/public/fade-bg.png"
|
||||||
|
alt="Background"
|
||||||
|
className="absolute inset-0 w-full h-full object-cover rounded-r-3xl"
|
||||||
|
style={{ zIndex: -1 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex h-[650px] w-1/3 flex-col justify-between py-3">
|
||||||
|
<div className="flex h-full flex-col justify-between">
|
||||||
|
<div>
|
||||||
|
<div className="flex flex-row items-center justify-between">
|
||||||
|
<span className="text-4xl">March 7th</span>
|
||||||
|
<img src="https://api.hakush.in/hsr/UI/element/ice.webp" alt="Element Icon" className="h-auto w-14" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row items-center gap-2">
|
||||||
|
<img src="https://api.hakush.in/hsr/UI/pathicon/knight.webp" alt="Path Icon" className="h-auto w-8" />
|
||||||
|
<span className="text-xl">Preservation</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-2xl">Lv. 80</span>
|
||||||
|
<span className="text-xl"> / </span>
|
||||||
|
<span className="text-xl text-neutral-400">80</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="relative mx-4 flex h-[225px] w-auto flex-row items-center justify-evenly">
|
||||||
|
<div className="absolute mb-5">
|
||||||
|
<img src="https://api.hakush.in/hsr/UI/pathicon/knight.webp" alt="Path Icon" className="h-40 w-40 opacity-20" />
|
||||||
|
</div>
|
||||||
|
<div className="flex h-full w-1/3 flex-col justify-center gap-8">
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<div className="relative flex flex-col items-center">
|
||||||
|
<img src="https://worker-sparkling-dawn-a1c1.srv2.workers.dev/hsr.honeyhunterworld.com/img/skill/frigid-cold-arrow-skill_icon.webp" alt="Skill Icon" className="h-auto w-12 rounded-full border-2 border-neutral-500 bg-neutral-800" />
|
||||||
|
<span className="black-blur absolute bottom-4 text-sm">6 / 6</span>
|
||||||
|
<span className="z-10 mt-1.5 truncate text-sm">Basic ATK</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<div className="relative flex flex-col items-center">
|
||||||
|
<img src="https://worker-sparkling-dawn-a1c1.srv2.workers.dev/hsr.honeyhunterworld.com/img/skill/the-power-of-cuteness-skill_icon.webp" alt="Skill Icon" className="h-auto w-12 rounded-full border-2 border-neutral-500 bg-neutral-800" />
|
||||||
|
<span className="black-blur absolute bottom-4 text-sm">10 / 10</span>
|
||||||
|
<span className="z-10 mt-1.5 truncate text-sm">Skill</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-1/3 justify-center">
|
||||||
|
<div className="relative flex flex-col items-center">
|
||||||
|
<img src="https://worker-sparkling-dawn-a1c1.srv2.workers.dev/hsr.honeyhunterworld.com/img/skill/glacial-cascade-skill_icon.webp" alt="Skill Icon" className="h-auto w-12 rounded-full border-2 border-neutral-500 bg-neutral-800" />
|
||||||
|
<span className="black-blur absolute bottom-4 text-sm">10 / 10</span>
|
||||||
|
<span className="z-10 mt-1.5 truncate text-sm">Ultimate</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex h-full w-1/3 flex-col justify-center gap-8">
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<div className="relative flex flex-col items-center">
|
||||||
|
<img src="https://worker-sparkling-dawn-a1c1.srv2.workers.dev/hsr.honeyhunterworld.com/img/skill/girl-power-skill_icon.webp" alt="Skill Icon" className="h-auto w-12 rounded-full border-2 border-neutral-500 bg-neutral-800" />
|
||||||
|
<span className="black-blur absolute bottom-4 text-sm">8 / 8</span>
|
||||||
|
<span className="z-10 mt-1.5 truncate text-sm">Talent</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<div className="relative flex flex-col items-center">
|
||||||
|
<img src="https://worker-sparkling-dawn-a1c1.srv2.workers.dev/hsr.honeyhunterworld.com/img/skill/freezing-beauty-skill_icon.webp" alt="Skill Icon" className="h-auto w-12 rounded-full border-2 border-neutral-500 bg-neutral-800" />
|
||||||
|
<span className="black-blur absolute bottom-4 text-sm">5 / 5</span>
|
||||||
|
<span className="z-10 mt-1.5 truncate text-sm">Technique</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<div className="flex w-full flex-row justify-evenly">
|
||||||
|
{/* First Column */}
|
||||||
|
<div className="flex flex-col justify-center items-center" style={{ gap: '0.25rem' }}>
|
||||||
|
<img
|
||||||
|
src="https://worker-sparkling-dawn-a1c1.srv2.workers.dev/hsr.honeyhunterworld.com/img/trace/purify-trace_icon.webp"
|
||||||
|
alt="Icon 1001101"
|
||||||
|
className="bg-neutral-800 h-auto w-5 rounded-full"
|
||||||
|
/>
|
||||||
|
<div className="flex flex-row justify-center items-center" style={{ gap: '0.25rem' }}>
|
||||||
|
<img
|
||||||
|
src="https://api.hakush.in/hsr/UI/trace/IconDefence.webp"
|
||||||
|
alt="Icon 1001101"
|
||||||
|
className="bg-neutral-800 h-auto w-3 rounded-full"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src="https://api.hakush.in/hsr/UI/trace/IconIceAddedRatio.webp"
|
||||||
|
alt="Icon 1001101"
|
||||||
|
className="bg-neutral-800 h-auto w-3 rounded-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Second Column */}
|
||||||
|
<div className="flex flex-col justify-center items-center" style={{ gap: '0.25rem' }}>
|
||||||
|
<img
|
||||||
|
src="https://worker-sparkling-dawn-a1c1.srv2.workers.dev/hsr.honeyhunterworld.com/img/trace/reinforce-trace_icon.webp"
|
||||||
|
alt="Icon 1001102"
|
||||||
|
className="bg-neutral-800 h-auto w-5 rounded-full"
|
||||||
|
/>
|
||||||
|
<div className="flex flex-row justify-center items-center" style={{ gap: '0.25rem' }}>
|
||||||
|
<img
|
||||||
|
src="https://api.hakush.in/hsr/UI/trace/IconIceAddedRatio.webp"
|
||||||
|
alt="Icon 1001102"
|
||||||
|
className="bg-neutral-800 h-auto w-3 rounded-full"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src="https://api.hakush.in/hsr/UI/trace/IconStatusResistance.webp"
|
||||||
|
alt="Icon 1001102"
|
||||||
|
className="bg-neutral-800 h-auto w-3 rounded-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Third Column */}
|
||||||
|
<div className="flex flex-col justify-center items-center" style={{ gap: '0.25rem' }}>
|
||||||
|
<img
|
||||||
|
src="https://worker-sparkling-dawn-a1c1.srv2.workers.dev/hsr.honeyhunterworld.com/img/trace/ice-spell-trace_icon.webp"
|
||||||
|
alt="Icon 1001103"
|
||||||
|
className="bg-neutral-800 h-auto w-5 rounded-full"
|
||||||
|
/>
|
||||||
|
<div className="flex flex-row justify-center items-center" style={{ gap: '0.25rem' }}>
|
||||||
|
<img
|
||||||
|
src="https://api.hakush.in/hsr/UI/trace/IconIceAddedRatio.webp"
|
||||||
|
alt="Icon 1001103"
|
||||||
|
className="bg-neutral-800 h-auto w-3 rounded-full"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src="https://api.hakush.in/hsr/UI/trace/IconIceAddedRatio.webp"
|
||||||
|
alt="Icon 1001103"
|
||||||
|
className="bg-neutral-800 h-auto w-3 rounded-full"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src="https://api.hakush.in/hsr/UI/trace/IconDefence.webp"
|
||||||
|
alt="Icon 1001103"
|
||||||
|
className="bg-neutral-800 h-auto w-3 rounded-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Fourth Column */}
|
||||||
|
<div className="flex flex-col justify-center items-center" style={{ gap: '0.25rem' }}>
|
||||||
|
<div className="flex flex-row justify-center items-center" style={{ gap: '0.25rem' }}>
|
||||||
|
<img
|
||||||
|
src="https://api.hakush.in/hsr/UI/trace/IconIceAddedRatio.webp"
|
||||||
|
alt="Icon 1001201"
|
||||||
|
className="bg-neutral-800 h-auto w-3 rounded-full"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src="https://api.hakush.in/hsr/UI/trace/IconStatusResistance.webp"
|
||||||
|
alt="Icon 1001201"
|
||||||
|
className="bg-neutral-800 h-auto w-3 rounded-full"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src="https://api.hakush.in/hsr/UI/trace/IconDefence.webp"
|
||||||
|
alt="Icon 1001201"
|
||||||
|
className="bg-neutral-800 h-auto w-3 rounded-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-row items-center justify-center">
|
||||||
|
<div className="relative flex flex-col items-center">
|
||||||
|
<img
|
||||||
|
src="https://api.hakush.in/hsr/UI/lightconemaxfigures/24000.webp"
|
||||||
|
alt="Light Cone Preview"
|
||||||
|
className="h-auto w-32"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src="https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/icon/deco/Star5.png"
|
||||||
|
alt="Light Cone Rarity Icon"
|
||||||
|
className="absolute bottom-0 left-1 h-auto w-36"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-3/5 flex-col items-center gap-2 text-center">
|
||||||
|
<span className="text-xl">On the Fall of an Aeon</span>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="h-6 w-6 rounded-full font-normal bg-[#f6ce71] text-black">
|
||||||
|
V
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-lg">Lv. 80</span>
|
||||||
|
<span> / </span>
|
||||||
|
<span className="text-neutral-400">80</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row gap-1.5">
|
||||||
|
<div className="black-blur flex flex-row items-center rounded pr-1">
|
||||||
|
<img
|
||||||
|
src="https://srtools.pages.dev/icon/property/IconAttack.png"
|
||||||
|
alt="Attribute Icon"
|
||||||
|
className="h-auto w-6 p-1"
|
||||||
|
/>
|
||||||
|
<span className="text-sm">529</span>
|
||||||
|
</div>
|
||||||
|
<div className="black-blur flex flex-row items-center rounded pr-1">
|
||||||
|
<img
|
||||||
|
src="https://srtools.pages.dev/icon/property/IconDefence.png"
|
||||||
|
alt="Attribute Icon"
|
||||||
|
className="h-auto w-6 p-1"
|
||||||
|
/>
|
||||||
|
<span className="text-sm">397</span>
|
||||||
|
</div>
|
||||||
|
<div className="black-blur flex flex-row items-center rounded pr-1">
|
||||||
|
<img
|
||||||
|
src="https://srtools.pages.dev/icon/property/IconMaxHP.png"
|
||||||
|
alt="Attribute Icon"
|
||||||
|
className="h-auto w-6 p-1"
|
||||||
|
/>
|
||||||
|
<span className="text-sm">1058</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex h-[650px] w-1/3 flex-col justify-between py-3">
|
||||||
|
<div className="flex w-full flex-col justify-between gap-y-0.5 text-base h-[500px]">
|
||||||
|
<div className="flex flex-row items-center justify-between">
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<img src="https://srtools.pages.dev/icon/property/IconMaxHP.png" alt="Stat Icon" className="h-auto w-10 p-2" />
|
||||||
|
<span className="font-bold">HP</span>
|
||||||
|
</div>
|
||||||
|
<div className="ml-3 mr-3 flex-grow border rounded opacity-50" />
|
||||||
|
<div className="flex cursor-default flex-col text-right font-bold">2942</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-row items-center justify-between">
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<img src="https://srtools.pages.dev/icon/property/IconAttack.png" alt="Stat Icon" className="h-auto w-10 p-2" />
|
||||||
|
<span className="font-bold">ATK</span>
|
||||||
|
</div>
|
||||||
|
<div className="ml-3 mr-3 flex-grow border rounded opacity-50" />
|
||||||
|
<div className="flex cursor-default flex-col text-right font-bold">3212</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-row items-center justify-between">
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<img src="https://srtools.pages.dev/icon/property/IconDefence.png" alt="Stat Icon" className="h-auto w-10 p-2" />
|
||||||
|
<span className="font-bold">DEF</span>
|
||||||
|
</div>
|
||||||
|
<div className="ml-3 mr-3 flex-grow border rounded opacity-50" />
|
||||||
|
<div className="flex cursor-default flex-col text-right font-bold">1255</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-row items-center justify-between">
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<img src="https://srtools.pages.dev/icon/property/IconSpeed.png" alt="Stat Icon" className="h-auto w-10 p-2" />
|
||||||
|
<span className="font-bold">SPD</span>
|
||||||
|
</div>
|
||||||
|
<div className="ml-3 mr-3 flex-grow border rounded opacity-50" />
|
||||||
|
<div className="flex cursor-default flex-col text-right font-bold">106</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-row items-center justify-between">
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<img src="https://srtools.pages.dev/icon/property/IconCriticalChance.png" alt="Stat Icon" className="h-auto w-10 p-2" />
|
||||||
|
<span className="font-bold">CRIT Rate</span>
|
||||||
|
</div>
|
||||||
|
<div className="ml-3 mr-3 flex-grow border rounded opacity-50" />
|
||||||
|
<div className="flex cursor-default flex-col text-right font-bold">16.7%</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-row items-center justify-between">
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<img src="https://srtools.pages.dev/icon/property/IconCriticalDamage.png" alt="Stat Icon" className="h-auto w-10 p-2" />
|
||||||
|
<span className="font-bold">CRIT DMG</span>
|
||||||
|
</div>
|
||||||
|
<div className="ml-3 mr-3 flex-grow border rounded opacity-50" />
|
||||||
|
<div className="flex cursor-default flex-col text-right font-bold">79.2%</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-row items-center justify-between">
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<img src="https://srtools.pages.dev/icon/property/IconBreakUp.png" alt="Stat Icon" className="h-auto w-10 p-2" />
|
||||||
|
<span className="font-bold">Break Effect</span>
|
||||||
|
</div>
|
||||||
|
<div className="ml-3 mr-3 flex-grow border rounded opacity-50" />
|
||||||
|
<div className="flex cursor-default flex-col text-right font-bold">186.9%</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-row items-center justify-between">
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<img src="https://srtools.pages.dev/icon/property/IconStatusResistance.png" alt="Stat Icon" className="h-auto w-10 p-2" />
|
||||||
|
<span className="font-bold">Effect RES</span>
|
||||||
|
</div>
|
||||||
|
<div className="ml-3 mr-3 flex-grow border rounded opacity-50" />
|
||||||
|
<div className="flex cursor-default flex-col text-right font-bold">4.0%</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-row items-center justify-between">
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<img src="https://srtools.pages.dev/icon/property/IconEnergyRecovery.png" alt="Stat Icon" className="h-auto w-10 p-2" />
|
||||||
|
<span className="font-bold">Energy Regeneration Rate</span>
|
||||||
|
</div>
|
||||||
|
<div className="ml-3 mr-3 flex-grow border rounded opacity-50" />
|
||||||
|
<div className="flex cursor-default flex-col text-right font-bold">0.0%</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-row items-center justify-between">
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<img src="https://srtools.pages.dev/icon/property/IconStatusProbability.png" alt="Stat Icon" className="h-auto w-10 p-2" />
|
||||||
|
<span className="font-bold">Effect Hit Rate</span>
|
||||||
|
</div>
|
||||||
|
<div className="ml-3 mr-3 flex-grow border rounded opacity-50" />
|
||||||
|
<div className="flex cursor-default flex-col text-right font-bold">20.3%</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-row items-center justify-between">
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<img src="https://srtools.pages.dev/icon/property/IconHealRatio.png" alt="Stat Icon" className="h-auto w-10 p-2" />
|
||||||
|
<span className="font-bold">Outgoing Healing Boost</span>
|
||||||
|
</div>
|
||||||
|
<div className="ml-3 mr-3 flex-grow border rounded opacity-50" />
|
||||||
|
<div className="flex cursor-default flex-col text-right font-bold">0.0%</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-row items-center justify-between">
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<img src="https://srtools.pages.dev/icon/property/IconIceAddedRatio.png" alt="Stat Icon" className="h-auto w-10 p-2" />
|
||||||
|
<span className="font-bold">Ice DMG Boost</span>
|
||||||
|
</div>
|
||||||
|
<div className="ml-3 mr-3 flex-grow border rounded opacity-50" />
|
||||||
|
<div className="flex cursor-default flex-col text-right font-bold">6.4%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<div className="flex flex-col items-center gap-1">
|
||||||
|
<div className="flex w-full flex-row justify-between text-left">
|
||||||
|
<span>Prisoner in Deep Confinement</span>
|
||||||
|
<div>
|
||||||
|
<span className="black-blur bg-black flex w-5 justify-center rounded px-1.5 py-0.5">2</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full flex-row justify-between text-left">
|
||||||
|
<span>Watchmaker, Master of Dream Machinations</span>
|
||||||
|
<div>
|
||||||
|
<span className="black-blur bg-black flex w-5 justify-center rounded px-1.5 py-0.5">2</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full flex-row justify-between text-left">
|
||||||
|
<span>Talia: Kingdom of Banditry</span>
|
||||||
|
<div>
|
||||||
|
<span className="black-blur bg-black flex w-5 justify-center rounded px-1.5 py-0.5">2</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-1/3">
|
||||||
|
<div className="flex h-[650px] flex-col justify-between py-3 text-lg">
|
||||||
|
<div className="black-blur relative flex flex-row items-center rounded-s-lg border-l-2 p-1 border-yellow-600">
|
||||||
|
<div className="flex">
|
||||||
|
<img src="https://api.hakush.in/hsr/UI/relicfigures/IconRelic_116_1.webp" alt="Relic Icon" className="h-auto w-20" />
|
||||||
|
<img src="https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/icon/deco/Star5.png" alt="Relic Rarity Icon" className="absolute bottom-1 h-auto w-20" />
|
||||||
|
</div>
|
||||||
|
<div className="mx-1 flex w-1/6 flex-col items-center justify-center">
|
||||||
|
<img src="https://srtools.pages.dev/icon/property/IconMaxHP.png" alt="Main Affix Icon" className="h-auto w-9" />
|
||||||
|
<span className="text-base text-[#f1a23c]">705</span>
|
||||||
|
<span className="black-blur rounded px-1 text-xs">+15</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ opacity: 0.5, height: '80px', borderLeftWidth: '1px' }}></div>
|
||||||
|
<div className="m-auto grid w-1/2 grid-cols-2 gap-2">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<img src="https://srtools.pages.dev/icon/property/IconAttack.png" alt="Sub Affix Icon" className="h-auto w-7" />
|
||||||
|
<span className="text-sm">+7.8%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<img src="https://srtools.pages.dev/icon/property/IconCriticalChance.png" alt="Sub Affix Icon" className="h-auto w-7" />
|
||||||
|
<span className="text-sm">+3.2%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<img src="https://srtools.pages.dev/icon/property/IconCriticalDamage.png" alt="Sub Affix Icon" className="h-auto w-7" />
|
||||||
|
<span className="text-sm">+17.5%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<img src="https://srtools.pages.dev/icon/property/IconBreakUp.png" alt="Sub Affix Icon" className="h-auto w-7" />
|
||||||
|
<span className="text-sm">+15.6%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="black-blur relative flex flex-row items-center rounded-s-lg border-l-2 p-1 border-yellow-600">
|
||||||
|
<div className="flex">
|
||||||
|
<img src="https://api.hakush.in/hsr/UI/relicfigures/IconRelic_118_2.webp" alt="Relic Icon" className="h-auto w-20" />
|
||||||
|
<img src="https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/icon/deco/Star5.png" alt="Relic Rarity Icon" className="absolute bottom-1 h-auto w-20" />
|
||||||
|
</div>
|
||||||
|
<div className="mx-1 flex w-1/6 flex-col items-center justify-center">
|
||||||
|
<img src="https://srtools.pages.dev/icon/property/IconAttack.png" alt="Main Affix Icon" className="h-auto w-9" />
|
||||||
|
<span className="text-base text-[#f1a23c]">352</span>
|
||||||
|
<span className="black-blur rounded px-1 text-xs">+15</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ opacity: 0.5, height: '80px', borderLeftWidth: '1px' }}></div>
|
||||||
|
<div className="m-auto grid w-1/2 grid-cols-2 gap-2">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<img src="https://srtools.pages.dev/icon/property/IconAttack.png" alt="Sub Affix Icon" className="h-auto w-7" />
|
||||||
|
<span className="text-sm">+6.9%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<img src="https://srtools.pages.dev/icon/property/IconCriticalDamage.png" alt="Sub Affix Icon" className="h-auto w-7" />
|
||||||
|
<span className="text-sm">+11.7%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<img src="https://srtools.pages.dev/icon/property/IconStatusProbability.png" alt="Sub Affix Icon" className="h-auto w-7" />
|
||||||
|
<span className="text-sm">+3.9%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<img src="https://srtools.pages.dev/icon/property/IconBreakUp.png" alt="Sub Affix Icon" className="h-auto w-7" />
|
||||||
|
<span className="text-sm">+23.3%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="black-blur relative flex flex-row items-center rounded-s-lg border-l-2 p-1 border-yellow-600">
|
||||||
|
<div className="flex">
|
||||||
|
<img src="https://api.hakush.in/hsr/UI/relicfigures/IconRelic_116_3.webp" alt="Relic Icon" className="h-auto w-20" />
|
||||||
|
<img src="https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/icon/deco/Star5.png" alt="Relic Rarity Icon" className="absolute bottom-1 h-auto w-20" />
|
||||||
|
</div>
|
||||||
|
<div className="mx-1 flex w-1/6 flex-col items-center justify-center">
|
||||||
|
<img src="https://srtools.pages.dev/icon/property/IconAttack.png" alt="Main Affix Icon" className="h-auto w-9" />
|
||||||
|
<span className="text-base text-[#f1a23c]">43.2%</span>
|
||||||
|
<span className="black-blur rounded px-1 text-xs">+15</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ opacity: 0.5, height: '80px', borderLeftWidth: '1px' }}></div>
|
||||||
|
<div className="m-auto grid w-1/2 grid-cols-2 gap-2">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<img src="https://srtools.pages.dev/icon/property/IconDefence.png" alt="Sub Affix Icon" className="h-auto w-7" />
|
||||||
|
<span className="text-sm">+57</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<img src="https://srtools.pages.dev/icon/property/IconMaxHP.png" alt="Sub Affix Icon" className="h-auto w-7" />
|
||||||
|
<span className="text-sm">+3.9%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<img src="https://srtools.pages.dev/icon/property/IconCriticalChance.png" alt="Sub Affix Icon" className="h-auto w-7" />
|
||||||
|
<span className="text-sm">+2.6%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<img src="https://srtools.pages.dev/icon/property/IconBreakUp.png" alt="Sub Affix Icon" className="h-auto w-7" />
|
||||||
|
<span className="text-sm">+19.4%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="black-blur relative flex flex-row items-center rounded-s-lg border-l-2 p-1 border-yellow-600">
|
||||||
|
<div className="flex">
|
||||||
|
<img src="https://api.hakush.in/hsr/UI/relicfigures/IconRelic_118_4.webp" alt="Relic Icon" className="h-auto w-20" />
|
||||||
|
<img src="https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/icon/deco/Star5.png" alt="Relic Rarity Icon" className="absolute bottom-1 h-auto w-20" />
|
||||||
|
</div>
|
||||||
|
<div className="mx-1 flex w-1/6 flex-col items-center justify-center">
|
||||||
|
<img src="https://srtools.pages.dev/icon/property/IconAttack.png" alt="Main Affix Icon" className="h-auto w-9" />
|
||||||
|
<span className="text-base text-[#f1a23c]">18.9%</span>
|
||||||
|
<span className="black-blur rounded px-1 text-xs">+15</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ opacity: 0.5, height: '80px', borderLeftWidth: '1px' }}></div>
|
||||||
|
<div className="m-auto grid w-1/2 grid-cols-2 gap-2">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<img src="https://srtools.pages.dev/icon/property/IconMaxHP.png" alt="Sub Affix Icon" className="h-auto w-7" />
|
||||||
|
<span className="text-sm">+5.2%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<img src="https://srtools.pages.dev/icon/property/IconCriticalDamage.png" alt="Sub Affix Icon" className="h-auto w-7" />
|
||||||
|
<span className="text-sm">+4.3%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<img src="https://srtools.pages.dev/icon/property/IconCriticalChance.png" alt="Sub Affix Icon" className="h-auto w-7" />
|
||||||
|
<span className="text-sm">+2.7%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<img src="https://srtools.pages.dev/icon/property/IconDefence.png" alt="Sub Affix Icon" className="h-auto w-7" />
|
||||||
|
<span className="text-sm">+34</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="black-blur relative flex flex-row items-center rounded-s-lg border-l-2 p-1 border-yellow-600">
|
||||||
|
<div className="flex">
|
||||||
|
<img src="https://api.hakush.in/hsr/UI/relicfigures/IconRelic_118_4.webp" alt="Relic Icon" className="h-auto w-20" />
|
||||||
|
<img src="https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/icon/deco/Star5.png" alt="Relic Rarity Icon" className="absolute bottom-1 h-auto w-20" />
|
||||||
|
</div>
|
||||||
|
<div className="mx-1 flex w-1/6 flex-col items-center justify-center">
|
||||||
|
<img src="https://srtools.pages.dev/icon/property/IconAttack.png" alt="Main Affix Icon" className="h-auto w-9" />
|
||||||
|
<span className="text-base text-[#f1a23c]">18.9%</span>
|
||||||
|
<span className="black-blur rounded px-1 text-xs">+15</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ opacity: 0.5, height: '80px', borderLeftWidth: '1px' }}></div>
|
||||||
|
<div className="m-auto grid w-1/2 grid-cols-2 gap-2">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<img src="https://srtools.pages.dev/icon/property/IconMaxHP.png" alt="Sub Affix Icon" className="h-auto w-7" />
|
||||||
|
<span className="text-sm">+5.2%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<img src="https://srtools.pages.dev/icon/property/IconCriticalDamage.png" alt="Sub Affix Icon" className="h-auto w-7" />
|
||||||
|
<span className="text-sm">+4.3%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<img src="https://srtools.pages.dev/icon/property/IconCriticalChance.png" alt="Sub Affix Icon" className="h-auto w-7" />
|
||||||
|
<span className="text-sm">+2.7%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<img src="https://srtools.pages.dev/icon/property/IconDefence.png" alt="Sub Affix Icon" className="h-auto w-7" />
|
||||||
|
<span className="text-sm">+34</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="black-blur relative flex flex-row items-center rounded-s-lg border-l-2 p-1 border-yellow-600">
|
||||||
|
<div className="flex">
|
||||||
|
<img src="https://api.hakush.in/hsr/UI/relicfigures/IconRelic_118_4.webp" alt="Relic Icon" className="h-auto w-20" />
|
||||||
|
<img src="https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/icon/deco/Star5.png" alt="Relic Rarity Icon" className="absolute bottom-1 h-auto w-20" />
|
||||||
|
</div>
|
||||||
|
<div className="mx-1 flex w-1/6 flex-col items-center justify-center">
|
||||||
|
<img src="https://srtools.pages.dev/icon/property/IconAttack.png" alt="Main Affix Icon" className="h-auto w-9" />
|
||||||
|
<span className="text-base text-[#f1a23c]">18.9%</span>
|
||||||
|
<span className="black-blur rounded px-1 text-xs">+15</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ opacity: 0.5, height: '80px', borderLeftWidth: '1px' }}></div>
|
||||||
|
<div className="m-auto grid w-1/2 grid-cols-2 gap-2">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<img src="https://srtools.pages.dev/icon/property/IconMaxHP.png" alt="Sub Affix Icon" className="h-auto w-7" />
|
||||||
|
<span className="text-sm">+5.2%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<img src="https://srtools.pages.dev/icon/property/IconCriticalDamage.png" alt="Sub Affix Icon" className="h-auto w-7" />
|
||||||
|
<span className="text-sm">+4.3%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<img src="https://srtools.pages.dev/icon/property/IconCriticalChance.png" alt="Sub Affix Icon" className="h-auto w-7" />
|
||||||
|
<span className="text-sm">+2.7%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<img src="https://srtools.pages.dev/icon/property/IconDefence.png" alt="Sub Affix Icon" className="h-auto w-7" />
|
||||||
|
<span className="text-sm">+34</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -13,16 +13,21 @@ import { useDamageLinesForAll } from '@/hooks/useDamageLinesForAll';
|
|||||||
import useAvatarDataStore from '@/stores/avatarDataStore';
|
import useAvatarDataStore from '@/stores/avatarDataStore';
|
||||||
import useLocaleStore from '@/stores/localeStore';
|
import useLocaleStore from '@/stores/localeStore';
|
||||||
import { getNameChar } from '@/helper';
|
import { getNameChar } from '@/helper';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
ChartJS.register(LineElement, CategoryScale, LinearScale, PointElement, Tooltip, Legend);
|
ChartJS.register(LineElement, CategoryScale, LinearScale, PointElement, Tooltip, Legend);
|
||||||
|
|
||||||
const colors = ['#f87171', '#34d399', '#60a5fa', '#facc15', '#a78bfa', '#fb923c'];
|
const colors = ['#f87171', '#34d399', '#60a5fa', '#facc15', '#a78bfa', '#fb923c'];
|
||||||
|
|
||||||
export default function MultiCharLineChart({ mode = 0 }: { mode?: 0 | 1 }) {
|
export default function MultiCharLineChart() {
|
||||||
|
const [mode, setMode] = useState<1 | 2>(2);
|
||||||
const dataByAvatar = useDamageLinesForAll(mode);
|
const dataByAvatar = useDamageLinesForAll(mode);
|
||||||
const avatarIds = Object.keys(dataByAvatar).map(Number);
|
const avatarIds = Object.keys(dataByAvatar).map(Number);
|
||||||
const { listAvatar } = useAvatarDataStore()
|
const { listAvatar } = useAvatarDataStore()
|
||||||
const { locale } = useLocaleStore();
|
const { locale } = useLocaleStore();
|
||||||
|
const transI18n = useTranslations("DataAnalysisPage")
|
||||||
|
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
datasets: avatarIds.map((id, idx) => ({
|
datasets: avatarIds.map((id, idx) => ({
|
||||||
@@ -54,12 +59,27 @@ export default function MultiCharLineChart({ mode = 0 }: { mode?: 0 | 1 }) {
|
|||||||
beginAtZero: true,
|
beginAtZero: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: {
|
|
||||||
datalabels: {
|
|
||||||
display: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return <Line data={data} options={options} />;
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="mb-4 flex items-start gap-2 justify-end">
|
||||||
|
{[
|
||||||
|
{ mode: 1, label: `${transI18n("type")} 1`, className: "btn-primary" },
|
||||||
|
{ mode: 2, label: `${transI18n("type")} 2`, className: "btn-warning" },
|
||||||
|
].map(({ mode: m, label, className }) => (
|
||||||
|
<button
|
||||||
|
key={m}
|
||||||
|
onClick={() => setMode(m as 1 | 2)}
|
||||||
|
className={`btn btn-sm ${mode === m ? className : "btn-ghost"}`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Line data={data} options={options} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
import { Line } from "react-chartjs-2";
|
import { Line } from "react-chartjs-2";
|
||||||
import { ChartOptions } from "chart.js";
|
import { ChartOptions } from "chart.js";
|
||||||
import { useDamageLineForOne } from "@/hooks/useDamageLineForOne";
|
import { useDamageLineForOne } from "@/hooks/useDamageLineForOne";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Chart as ChartJS,
|
Chart as ChartJS,
|
||||||
LineElement,
|
LineElement,
|
||||||
@@ -15,6 +14,7 @@ import {
|
|||||||
Filler,
|
Filler,
|
||||||
} from "chart.js";
|
} from "chart.js";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
ChartJS.register(
|
ChartJS.register(
|
||||||
LineElement,
|
LineElement,
|
||||||
@@ -27,11 +27,9 @@ ChartJS.register(
|
|||||||
Filler
|
Filler
|
||||||
);
|
);
|
||||||
|
|
||||||
ChartJS.defaults.set('plugins.datalabels', {
|
|
||||||
color: '#FE777B'
|
|
||||||
});
|
|
||||||
|
|
||||||
export function DamageLineForOne({ avatarId, mode = 0 }: { avatarId: number; mode?: 0 | 1 }) {
|
export function DamageLineForOne({ avatarId }: { avatarId: number; }) {
|
||||||
|
const [mode, setMode] = useState<0 | 1>(1);
|
||||||
const dataRaw = useDamageLineForOne(avatarId, mode);
|
const dataRaw = useDamageLineForOne(avatarId, mode);
|
||||||
const transI18n = useTranslations("DataAnalysisPage");
|
const transI18n = useTranslations("DataAnalysisPage");
|
||||||
const data = {
|
const data = {
|
||||||
@@ -52,9 +50,6 @@ export function DamageLineForOne({ avatarId, mode = 0 }: { avatarId: number; mod
|
|||||||
responsive: true,
|
responsive: true,
|
||||||
plugins: {
|
plugins: {
|
||||||
legend: { display: true },
|
legend: { display: true },
|
||||||
datalabels: {
|
|
||||||
display: false,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
scales: {
|
scales: {
|
||||||
x: { title: { display: true, text: transI18n("actionValue") } },
|
x: { title: { display: true, text: transI18n("actionValue") } },
|
||||||
@@ -63,5 +58,23 @@ export function DamageLineForOne({ avatarId, mode = 0 }: { avatarId: number; mod
|
|||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return <Line data={data} options={options} />;
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="mb-4 flex items-start gap-2 justify-end">
|
||||||
|
{[
|
||||||
|
{ mode: 1, label: `${transI18n("type")} 1`, className: "btn-primary" },
|
||||||
|
{ mode: 2, label: `${transI18n("type")} 2`, className: "btn-warning" },
|
||||||
|
].map(({ mode: m, label, className }) => (
|
||||||
|
<button
|
||||||
|
key={m}
|
||||||
|
onClick={() => setMode(m as 0 | 1)}
|
||||||
|
className={`btn btn-sm ${mode === m ? className : "btn-ghost"}`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<Line data={data} options={options} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
"use client";
|
'use client';
|
||||||
|
import { useState } from 'react';
|
||||||
import { getNameChar } from '@/helper';
|
import { getNameChar } from '@/helper';
|
||||||
import useAvatarDataStore from '@/stores/avatarDataStore';
|
import useAvatarDataStore from '@/stores/avatarDataStore';
|
||||||
import useBattleDataStore from '@/stores/battleDataStore';
|
import useBattleDataStore from '@/stores/battleDataStore';
|
||||||
@@ -9,11 +10,11 @@ import {
|
|||||||
CategoryScale,
|
CategoryScale,
|
||||||
LinearScale,
|
LinearScale,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Legend,
|
Legend
|
||||||
} from 'chart.js';
|
} from 'chart.js';
|
||||||
import { useTranslations } from 'next-intl';
|
|
||||||
import { Bar } from 'react-chartjs-2';
|
import { Bar } from 'react-chartjs-2';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
import {attackTypeToString} from "@/types";
|
||||||
|
|
||||||
ChartJS.register(BarElement, CategoryScale, LinearScale, Tooltip, Legend);
|
ChartJS.register(BarElement, CategoryScale, LinearScale, Tooltip, Legend);
|
||||||
|
|
||||||
@@ -25,56 +26,153 @@ const colorPalette = [
|
|||||||
'rgba(153, 102, 255, 0.6)',
|
'rgba(153, 102, 255, 0.6)',
|
||||||
'rgba(255, 159, 64, 0.6)',
|
'rgba(255, 159, 64, 0.6)',
|
||||||
'rgba(199, 199, 199, 0.6)',
|
'rgba(199, 199, 199, 0.6)',
|
||||||
|
'rgba(255, 99, 255, 0.6)',
|
||||||
|
'rgba(0, 200, 83, 0.6)',
|
||||||
|
'rgba(255, 112, 67, 0.6)',
|
||||||
|
'rgba(63, 81, 181, 0.6)',
|
||||||
|
'rgba(0, 188, 212, 0.6)',
|
||||||
|
'rgba(233, 30, 99, 0.6)',
|
||||||
|
'rgba(124, 179, 66, 0.6)',
|
||||||
|
'rgba(255, 235, 59, 0.6)',
|
||||||
|
'rgba(158, 158, 158, 0.6)',
|
||||||
|
'rgba(121, 85, 72, 0.6)',
|
||||||
|
'rgba(96, 125, 139, 0.6)',
|
||||||
|
'rgba(100, 181, 246, 0.6)',
|
||||||
|
'rgba(255, 171, 145, 0.6)',
|
||||||
|
'rgba(174, 213, 129, 0.6)',
|
||||||
|
'rgba(255, 245, 157, 0.6)'
|
||||||
];
|
];
|
||||||
|
|
||||||
const borderPalette = colorPalette.map((color) => color.replace('0.6', '1'));
|
const borderPalette = colorPalette.map((c) => c.replace('0.6', '1'));
|
||||||
|
|
||||||
export default function DamagePerAvatarForAll() {
|
export default function DamagePerAvatarForAll() {
|
||||||
const transI18n = useTranslations("DataAnalysisPage");
|
const transI18n = useTranslations("DataAnalysisPage");
|
||||||
const { lineup, turnHistory } = useBattleDataStore()
|
const { lineup, skillHistory } = useBattleDataStore();
|
||||||
const { listAvatar } = useAvatarDataStore()
|
const { listAvatar } = useAvatarDataStore();
|
||||||
const { locale } = useLocaleStore();
|
const { locale } = useLocaleStore();
|
||||||
const damageByAvatar = lineup.map((avatar) => {
|
|
||||||
const turns = turnHistory.filter((turn) => turn.avatarId === avatar.avatarId);
|
const [mode, setMode] = useState<number>(2);
|
||||||
const totalDmg = turns.reduce((sum, turn) => sum + turn.totalDamage, 0);
|
|
||||||
const char = listAvatar.find(it => it.id === avatar.avatarId.toString())
|
const avatarMap = lineup.map((avatar) => {
|
||||||
if (!char) return
|
const char = listAvatar.find(it => it.id === avatar.avatarId.toString());
|
||||||
|
if (!char) return undefined;
|
||||||
return {
|
return {
|
||||||
avatarId: avatar.avatarId,
|
avatarId: avatar.avatarId,
|
||||||
damage: totalDmg,
|
|
||||||
avatarName: getNameChar(locale, char)
|
avatarName: getNameChar(locale, char)
|
||||||
};
|
};
|
||||||
});
|
}).filter(Boolean) as { avatarId: number, avatarName: string }[];
|
||||||
|
|
||||||
const data = {
|
const labels = avatarMap.map(a => a.avatarName);
|
||||||
labels: damageByAvatar.map((item) => item?.avatarName),
|
|
||||||
datasets: [
|
let datasets: Array<{
|
||||||
|
label: string;
|
||||||
|
data: number[];
|
||||||
|
backgroundColor: string | string[];
|
||||||
|
borderColor: string | string[];
|
||||||
|
borderWidth: number;
|
||||||
|
stack?: string;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
if (mode === 1) {
|
||||||
|
datasets = [
|
||||||
{
|
{
|
||||||
label: transI18n("totalDamage"),
|
label: transI18n("totalDamage"),
|
||||||
data: damageByAvatar.map((item) => item?.damage),
|
data: avatarMap.map(({ avatarId }) => {
|
||||||
backgroundColor: damageByAvatar.map((_, idx) => colorPalette[idx % colorPalette.length]),
|
const total = skillHistory
|
||||||
borderColor: damageByAvatar.map((_, idx) => borderPalette[idx % borderPalette.length]),
|
.filter(skill => skill.avatarId === avatarId)
|
||||||
|
.reduce((sum, s) => sum + s.totalDamage, 0);
|
||||||
|
return total;
|
||||||
|
}),
|
||||||
|
backgroundColor: avatarMap.map((_, i) => colorPalette[i % colorPalette.length]),
|
||||||
|
borderColor: avatarMap.map((_, i) => borderPalette[i % borderPalette.length]),
|
||||||
|
borderWidth: 1
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode === 2) {
|
||||||
|
const damageTypesSet = new Set<string>();
|
||||||
|
avatarMap.forEach(({ avatarId }) => {
|
||||||
|
skillHistory.filter(s => s.avatarId === avatarId).forEach(s =>
|
||||||
|
s.damageDetail?.forEach(d => d.damage_type && damageTypesSet.add(transI18n(attackTypeToString(d?.damage_type).toLowerCase())))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const damageTypes = Array.from(damageTypesSet);
|
||||||
|
|
||||||
|
datasets = damageTypes.map((dt, i) => ({
|
||||||
|
label: dt,
|
||||||
|
data: avatarMap.map(({ avatarId }) => {
|
||||||
|
const total = skillHistory
|
||||||
|
.filter(skill => skill.avatarId === avatarId)
|
||||||
|
.flatMap(s => s.damageDetail || [])
|
||||||
|
.filter(d => transI18n(attackTypeToString(d?.damage_type).toLowerCase()) === dt)
|
||||||
|
.reduce((sum, d) => sum + d.damage, 0);
|
||||||
|
return total;
|
||||||
|
}),
|
||||||
|
backgroundColor: colorPalette[i % colorPalette.length],
|
||||||
|
borderColor: borderPalette[i % borderPalette.length],
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
},
|
stack: transI18n('damage')
|
||||||
],
|
}));
|
||||||
};
|
}
|
||||||
|
|
||||||
|
if (mode === 3) {
|
||||||
|
const skillTypesSet = new Set<string>();
|
||||||
|
skillHistory.forEach(s => skillTypesSet.add(transI18n(attackTypeToString(s?.skillType)?.toLowerCase())));
|
||||||
|
const skillTypes = Array.from(skillTypesSet);
|
||||||
|
|
||||||
|
datasets = skillTypes.map((st, i) => ({
|
||||||
|
label: st,
|
||||||
|
data: avatarMap.map(({ avatarId }) => {
|
||||||
|
const total = skillHistory
|
||||||
|
.filter(skill => skill.avatarId === avatarId && transI18n(attackTypeToString(skill?.skillType).toLowerCase()) === st)
|
||||||
|
.reduce((sum, s) => sum + s.totalDamage, 0);
|
||||||
|
return total;
|
||||||
|
}),
|
||||||
|
backgroundColor: colorPalette[i % colorPalette.length],
|
||||||
|
borderColor: borderPalette[i % borderPalette.length],
|
||||||
|
borderWidth: 1,
|
||||||
|
stack: transI18n('skill')
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
scales: {
|
scales: {
|
||||||
y: {
|
y: {
|
||||||
beginAtZero: true,
|
beginAtZero: true,
|
||||||
|
stacked: mode !== 1,
|
||||||
ticks: {
|
ticks: {
|
||||||
precision: 0,
|
precision: 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
x: {
|
||||||
plugins: {
|
stacked: mode !== 1,
|
||||||
datalabels: {
|
|
||||||
display: false,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return <Bar data={data} options={options} />;
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="mb-4 flex items-start gap-2 justify-end">
|
||||||
|
{[
|
||||||
|
{ mode: 1, label: `${transI18n("type")} 1`, className: "btn-primary" },
|
||||||
|
{ mode: 2, label: `${transI18n("type")} 2`, className: "btn-warning" },
|
||||||
|
{ mode: 3, label: `${transI18n("type")} 3`, className: "btn-accent" },
|
||||||
|
].map(({ mode: m, label, className }) => (
|
||||||
|
<button
|
||||||
|
key={m}
|
||||||
|
onClick={() => setMode(m)}
|
||||||
|
className={`btn btn-sm ${mode === m ? className : "btn-ghost"}`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Bar data={{ labels, datasets }} options={options}/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,36 +2,33 @@
|
|||||||
import { Bar } from "react-chartjs-2";
|
import { Bar } from "react-chartjs-2";
|
||||||
import { ChartOptions } from "chart.js";
|
import { ChartOptions } from "chart.js";
|
||||||
import { useDamagePerCycleForAll } from "@/hooks/useDamagePerCycle";
|
import { useDamagePerCycleForAll } from "@/hooks/useDamagePerCycle";
|
||||||
|
|
||||||
import { Chart as ChartJS, BarElement, ArcElement, RadialLinearScale } from "chart.js";
|
import { Chart as ChartJS, BarElement, ArcElement, RadialLinearScale } from "chart.js";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
ChartJS.register(BarElement, ArcElement, RadialLinearScale);
|
ChartJS.register(BarElement, ArcElement, RadialLinearScale);
|
||||||
export default function DamagePerCycleForAll({
|
export default function DamagePerCycleForAll() {
|
||||||
mode,
|
const [mode, setMode] = useState<0 | 1 | 2>(0);
|
||||||
}: {
|
|
||||||
mode: 0 | 1 | 2;
|
|
||||||
}) {
|
|
||||||
const dataRaw = useDamagePerCycleForAll(mode);
|
const dataRaw = useDamagePerCycleForAll(mode);
|
||||||
const transI18n = useTranslations("DataAnalysisPage");
|
const transI18n = useTranslations("DataAnalysisPage");
|
||||||
const data = {
|
const data = {
|
||||||
labels: dataRaw.map(d => d.x),
|
labels: dataRaw.map(d => d.x),
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
label: mode === 0 ? `${transI18n("damagerPerCycle")} (100av)` : mode === 1 ? `${transI18n("damagerPerCycle")} (150av | 100v)` : `${transI18n("damagerPerCycle")} (150av | 150av | 100v)`,
|
label: mode === 0 ? `${transI18n("damagePerCycleAndWave")}` : mode === 1 ? `${transI18n("damagePerCycle")}` : `${transI18n("damagePerWave")}`,
|
||||||
data: dataRaw.map(d => d.y),
|
data: dataRaw.map(d => d.y),
|
||||||
backgroundColor: "rgba(255,99,132,0.6)",
|
backgroundColor: dataRaw.map((_, i) =>
|
||||||
|
['#f87171', '#34d399', '#60a5fa', '#facc15', '#a78bfa', '#fb923c', '#f472b6'][i % 7]
|
||||||
|
),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const options: ChartOptions<"bar"> = {
|
const options: ChartOptions<"bar"> = {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
plugins: {
|
plugins: {
|
||||||
legend: { display: true },
|
legend: { display: true },
|
||||||
datalabels: {
|
|
||||||
display: false,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
scales: {
|
scales: {
|
||||||
x: { title: { display: true, text: transI18n("cycleCount") } },
|
x: { title: { display: true, text: transI18n("cycleCount") } },
|
||||||
@@ -40,5 +37,24 @@ export default function DamagePerCycleForAll({
|
|||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return <Bar data={data} options={options} />;
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="mb-4 flex items-start gap-2 justify-end">
|
||||||
|
{[
|
||||||
|
{ mode: 0, label: `${transI18n("type")} 1`, className: "btn-primary" },
|
||||||
|
{ mode: 1, label: `${transI18n("type")} 2`, className: "btn-warning" },
|
||||||
|
{ mode: 2, label: `${transI18n("type")} 3`, className: "btn-accent" },
|
||||||
|
].map(({ mode: m, label, className }) => (
|
||||||
|
<button
|
||||||
|
key={m}
|
||||||
|
onClick={() => setMode(m as 0 | 1 | 2)}
|
||||||
|
className={`btn btn-sm ${mode === m ? className : "btn-ghost"}`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<Bar data={data} options={options} />;
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,24 +5,26 @@ import { useDamagePerCycleForOne } from "@/hooks/useDamagePerCycle";
|
|||||||
|
|
||||||
import { Chart as ChartJS, BarElement, ArcElement, RadialLinearScale } from "chart.js";
|
import { Chart as ChartJS, BarElement, ArcElement, RadialLinearScale } from "chart.js";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
ChartJS.register(BarElement, ArcElement, RadialLinearScale);
|
ChartJS.register(BarElement, ArcElement, RadialLinearScale);
|
||||||
export function DamagePerCycleForOne({
|
export function DamagePerCycleForOne({
|
||||||
avatarId,
|
avatarId,
|
||||||
mode,
|
|
||||||
}: {
|
}: {
|
||||||
avatarId: number;
|
avatarId: number;
|
||||||
mode: 0 | 1 | 2;
|
|
||||||
}) {
|
}) {
|
||||||
|
const [mode, setMode] = useState<0 | 1 | 2>(0);
|
||||||
const dataRaw = useDamagePerCycleForOne(avatarId, mode);
|
const dataRaw = useDamagePerCycleForOne(avatarId, mode);
|
||||||
const transI18n = useTranslations("DataAnalysisPage");
|
const transI18n = useTranslations("DataAnalysisPage");
|
||||||
const data = {
|
const data = {
|
||||||
labels: dataRaw.map(d => d.x),
|
labels: dataRaw.map(d => d.x),
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
label: mode === 0 ? `${transI18n("damagerPerCycle")} (100av)` : mode === 1 ? `${transI18n("damagerPerCycle")} (150av | 100v)` : `${transI18n("damagerPerCycle")} (150av | 150av | 100v)`,
|
label: mode === 0 ? `${transI18n("damagePerCycleAndWave")}` : mode === 1 ? `${transI18n("damagePerCycle")}` : `${transI18n("damagePerWave")}`,
|
||||||
data: dataRaw.map(d => d.y),
|
data: dataRaw.map(d => d.y),
|
||||||
backgroundColor: "rgba(255,99,132,0.6)",
|
backgroundColor: dataRaw.map((_, i) =>
|
||||||
|
['#f87171', '#34d399', '#60a5fa', '#facc15', '#a78bfa', '#fb923c', '#f472b6'][i % 7]
|
||||||
|
),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -31,9 +33,6 @@ export function DamagePerCycleForOne({
|
|||||||
responsive: true,
|
responsive: true,
|
||||||
plugins: {
|
plugins: {
|
||||||
legend: { display: true },
|
legend: { display: true },
|
||||||
datalabels: {
|
|
||||||
display: false,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
scales: {
|
scales: {
|
||||||
x: { title: { display: true, text: transI18n("cycleCount") } },
|
x: { title: { display: true, text: transI18n("cycleCount") } },
|
||||||
@@ -42,5 +41,24 @@ export function DamagePerCycleForOne({
|
|||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return <Bar data={data} options={options} />;
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="mb-4 flex items-start gap-2 justify-end">
|
||||||
|
{[
|
||||||
|
{ mode: 0, label: `${transI18n("type")} 1`, className: "btn-primary" },
|
||||||
|
{ mode: 1, label: `${transI18n("type")} 2`, className: "btn-warning" },
|
||||||
|
{ mode: 2, label: `${transI18n("type")} 3`, className: "btn-warning" },
|
||||||
|
].map(({ mode: m, label, className }) => (
|
||||||
|
<button
|
||||||
|
key={m}
|
||||||
|
onClick={() => setMode(m as 0 | 1 | 2)}
|
||||||
|
className={`btn btn-sm ${mode === m ? className : "btn-ghost"}`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<Bar data={data} options={options} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,22 +11,40 @@ import useAvatarDataStore from '@/stores/avatarDataStore';
|
|||||||
import useLocaleStore from '@/stores/localeStore';
|
import useLocaleStore from '@/stores/localeStore';
|
||||||
import { getNameChar } from '@/helper';
|
import { getNameChar } from '@/helper';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useDamagePercentByType } from '@/hooks/useDamagePercentForDamgeType';
|
||||||
|
|
||||||
ChartJS.register(ArcElement, Tooltip, Legend);
|
ChartJS.register(ArcElement, Tooltip, Legend);
|
||||||
|
|
||||||
const colors = ['#f87171', '#34d399', '#60a5fa', '#facc15', '#a78bfa', '#fb923c', '#f472b6'];
|
const colors = [
|
||||||
|
'#f87171', '#34d399', '#60a5fa', '#facc15',
|
||||||
|
'#a78bfa', '#fb923c', '#f472b6', '#10b981',
|
||||||
|
'#fcd34d', '#c084fc', '#f97316', '#ec4899',
|
||||||
|
'#4ade80', '#fbbf24', '#8b5cf6', '#f43f5e'
|
||||||
|
];
|
||||||
|
|
||||||
export default function DamagePercentChartForAll() {
|
export default function DamagePercentChartForAll() {
|
||||||
const data = useDamagePercentPerAvatar();
|
const [mode, setMode] = useState<1 | 2>(1);
|
||||||
const { listAvatar } = useAvatarDataStore()
|
const damageByAvatar = useDamagePercentPerAvatar();
|
||||||
|
const damageByType = useDamagePercentByType();
|
||||||
|
const { listAvatar } = useAvatarDataStore();
|
||||||
const { locale } = useLocaleStore();
|
const { locale } = useLocaleStore();
|
||||||
const transI18n = useTranslations("DataAnalysisPage");
|
const transI18n = useTranslations("DataAnalysisPage");
|
||||||
|
|
||||||
const chartData = {
|
const chartData = {
|
||||||
labels: data.map(d => getNameChar(locale, listAvatar.find(it => it.id == d.avatarId.toString()))),
|
labels: (mode === 1
|
||||||
|
? damageByAvatar.map(d =>
|
||||||
|
getNameChar(locale, listAvatar.find(it => it.id == d.avatarId.toString()))
|
||||||
|
)
|
||||||
|
: damageByType.map(d => transI18n(d.type.toLowerCase()))
|
||||||
|
),
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
label: '% ' + transI18n("damage"),
|
label: '% ' + transI18n("damage"),
|
||||||
data: data.map(d => d.percent.toFixed(2)),
|
data: (mode === 1
|
||||||
|
? damageByAvatar.map(d => d.percent.toFixed(2))
|
||||||
|
: damageByType.map(d => d.percent.toFixed(2))
|
||||||
|
),
|
||||||
backgroundColor: colors,
|
backgroundColor: colors,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
},
|
},
|
||||||
@@ -47,5 +65,27 @@ export default function DamagePercentChartForAll() {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return <Pie data={chartData} options={options} />;
|
return (
|
||||||
|
<div className='w-full'>
|
||||||
|
<div className="mb-4 flex items-start gap-2 justify-end">
|
||||||
|
{[
|
||||||
|
{ mode: 1, label: `${transI18n("type")} 1`, className: "btn-primary" },
|
||||||
|
{ mode: 2, label: `${transI18n("type")} 2`, className: "btn-warning" },
|
||||||
|
].map(({ mode: m, label, className }) => (
|
||||||
|
<button
|
||||||
|
key={m}
|
||||||
|
onClick={() => setMode(m as 1 | 2)}
|
||||||
|
className={`btn btn-sm ${mode === m ? className : "btn-ghost"}`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<Pie data={chartData} options={options} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +1,32 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { getNameChar } from '@/helper';
|
import { useState } from 'react';
|
||||||
import { useSkillDamageByAvatar } from '@/hooks/useSkillDamageStats';
|
|
||||||
import useAvatarDataStore from '@/stores/avatarDataStore';
|
|
||||||
import useLocaleStore from '@/stores/localeStore';
|
|
||||||
import {
|
|
||||||
Chart as ChartJS,
|
|
||||||
BarElement,
|
|
||||||
CategoryScale,
|
|
||||||
LinearScale,
|
|
||||||
Tooltip,
|
|
||||||
Legend,
|
|
||||||
} from 'chart.js';
|
|
||||||
import ChartDataLabels from 'chartjs-plugin-datalabels';
|
|
||||||
import { useTranslations } from 'next-intl';
|
|
||||||
import { Bar } from 'react-chartjs-2';
|
import { Bar } from 'react-chartjs-2';
|
||||||
|
import { Chart as ChartJS, BarElement, CategoryScale, LinearScale, Tooltip, Legend } from 'chart.js';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
import { useSkillDamageForAvatar } from '@/hooks/useSkillDamageForAvatar';
|
||||||
|
import { useDamageByTypeForAvatar } from '@/hooks/useDamageTypeForAvatar';
|
||||||
|
|
||||||
ChartJS.register(BarElement, CategoryScale, LinearScale, Tooltip, Legend, ChartDataLabels);
|
|
||||||
|
ChartJS.register(BarElement, CategoryScale, LinearScale, Tooltip, Legend);
|
||||||
|
|
||||||
export default function SkillBarChart({ avatarId }: { avatarId: number }) {
|
export default function SkillBarChart({ avatarId }: { avatarId: number }) {
|
||||||
const { labels, damageValues } = useSkillDamageByAvatar(avatarId);
|
const [mode, setMode] = useState<1 | 2>(2);
|
||||||
const transI18n = useTranslations("DataAnalysisPage");
|
|
||||||
|
const transI18n = useTranslations('DataAnalysisPage');
|
||||||
|
const { labels: skillLabels, damageValues: skillDamageValues } = useSkillDamageForAvatar(avatarId);
|
||||||
|
const { labels: typeLabels, damageValues: typeDamageValues } = useDamageByTypeForAvatar(avatarId);
|
||||||
|
|
||||||
const colors = ['#f87171', '#facc15', '#34d399', '#60a5fa', '#a78bfa', '#fb923c'];
|
const colors = ['#f87171', '#facc15', '#34d399', '#60a5fa', '#a78bfa', '#fb923c'];
|
||||||
const { listAvatar } = useAvatarDataStore()
|
|
||||||
const { locale } = useLocaleStore();
|
|
||||||
const data = {
|
const data = {
|
||||||
labels: labels.map(it => transI18n(it.toLowerCase())),
|
labels: mode === 1 ? skillLabels.map(it => transI18n(it)) : typeLabels.map(it => transI18n(it)),
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
label: getNameChar(locale, listAvatar.find(it => it.id === avatarId.toString())),
|
label: mode === 1
|
||||||
data: damageValues,
|
? transI18n('skill')
|
||||||
backgroundColor: colors.slice(0, labels.length),
|
: transI18n('damage'),
|
||||||
|
data: mode === 1 ? skillDamageValues : typeDamageValues,
|
||||||
|
backgroundColor: colors.slice(0, (mode === 1 ? skillLabels : typeLabels).length),
|
||||||
borderColor: '#111',
|
borderColor: '#111',
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
},
|
},
|
||||||
@@ -48,11 +45,26 @@ export default function SkillBarChart({ avatarId }: { avatarId: number }) {
|
|||||||
legend: {
|
legend: {
|
||||||
display: false,
|
display: false,
|
||||||
},
|
},
|
||||||
datalabels: {
|
|
||||||
display: false,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return <Bar data={data} options={options} />;
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="mb-4 flex items-start gap-2 justify-end">
|
||||||
|
{[
|
||||||
|
{ mode: 1, label: `${transI18n("type")} 1`, className: "btn-primary" },
|
||||||
|
{ mode: 2, label: `${transI18n("type")} 2`, className: "btn-warning" },
|
||||||
|
].map(({ mode: m, label, className }) => (
|
||||||
|
<button
|
||||||
|
key={m}
|
||||||
|
onClick={() => setMode(m as 1 | 2)}
|
||||||
|
className={`btn btn-sm ${mode === m ? className : "btn-ghost"}`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<Bar data={data} options={options} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,32 @@
|
|||||||
"use client";
|
'use client';
|
||||||
import { useSkillDamageByAvatar } from '@/hooks/useSkillDamageStats';
|
import { useState } from 'react';
|
||||||
|
import { Pie } from 'react-chartjs-2';
|
||||||
import {
|
import {
|
||||||
Chart as ChartJS,
|
Chart as ChartJS,
|
||||||
ArcElement,
|
ArcElement,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Legend,
|
Legend,
|
||||||
} from 'chart.js';
|
} from 'chart.js';
|
||||||
import { Pie } from 'react-chartjs-2';
|
|
||||||
import ChartDataLabels from 'chartjs-plugin-datalabels';
|
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
|
import { useSkillDamageForAvatar } from '@/hooks/useSkillDamageForAvatar';
|
||||||
|
import { useDamageByTypeForAvatar } from '@/hooks/useDamageTypeForAvatar';
|
||||||
|
|
||||||
ChartJS.register(ArcElement, Tooltip, Legend, ChartDataLabels);
|
ChartJS.register(ArcElement, Tooltip, Legend);
|
||||||
|
|
||||||
|
const COLORS = [
|
||||||
|
'#f87171', '#facc15', '#34d399', '#60a5fa', '#a78bfa', '#fb923c',
|
||||||
|
'#f472b6', '#38bdf8', '#a3e635', '#fdba74', '#c084fc', '#4ade80',
|
||||||
|
'#fcd34d', '#818cf8', '#f43f5e', '#14b8a6', '#e879f9', '#86efac',
|
||||||
|
];
|
||||||
|
|
||||||
export default function SkillPieChart({ avatarId }: { avatarId: number }) {
|
export default function SkillPieChart({ avatarId }: { avatarId: number }) {
|
||||||
const { labels, damageValues } = useSkillDamageByAvatar(avatarId);
|
const [mode, setMode] = useState<1 | 2>(2);
|
||||||
const transI18n = useTranslations("DataAnalysisPage");
|
const transI18n = useTranslations("DataAnalysisPage");
|
||||||
|
|
||||||
|
const skillData = useSkillDamageForAvatar(avatarId);
|
||||||
|
const typeData = useDamageByTypeForAvatar(avatarId);
|
||||||
|
|
||||||
|
const { labels, damageValues } = mode === 1 ? skillData : typeData;
|
||||||
const total = damageValues.reduce((sum, val) => sum + val, 0);
|
const total = damageValues.reduce((sum, val) => sum + val, 0);
|
||||||
|
|
||||||
const labelsWithPercent = labels.map((label, index) => {
|
const labelsWithPercent = labels.map((label, index) => {
|
||||||
@@ -28,32 +40,36 @@ export default function SkillPieChart({ avatarId }: { avatarId: number }) {
|
|||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
data: damageValues,
|
data: damageValues,
|
||||||
backgroundColor: [
|
backgroundColor: COLORS.slice(0, labels.length),
|
||||||
'#f87171', // red
|
|
||||||
'#facc15', // yellow
|
|
||||||
'#34d399', // green
|
|
||||||
'#60a5fa', // blue
|
|
||||||
'#a78bfa', // purple
|
|
||||||
'#fb923c', // orange
|
|
||||||
],
|
|
||||||
borderColor: '#fff',
|
borderColor: '#fff',
|
||||||
borderWidth: 2,
|
borderWidth: 1,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className="w-full ">
|
||||||
|
<div className="mb-4 flex items-start gap-2 justify-end">
|
||||||
|
{[
|
||||||
|
{ mode: 1, label: `${transI18n("type")} 1`, className: "btn-primary" },
|
||||||
|
{ mode: 2, label: `${transI18n("type")} 2`, className: "btn-warning" },
|
||||||
|
].map(({ mode: m, label, className }) => (
|
||||||
|
<button
|
||||||
|
key={m}
|
||||||
|
onClick={() => setMode(m as 1 | 2)}
|
||||||
|
className={`btn btn-sm ${mode === m ? className : "btn-ghost"}`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
<Pie
|
<Pie
|
||||||
data={data}
|
data={data}
|
||||||
options={{
|
options={{
|
||||||
responsive: true,
|
responsive: true,
|
||||||
plugins: {
|
cutout: '30%',
|
||||||
legend: { position: 'right' },
|
|
||||||
datalabels: {
|
|
||||||
display: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { connectSocket, disconnectSocket, getSocket, isSocketConnected } from "@
|
|||||||
import useBattleDataStore from "@/stores/battleDataStore";
|
import useBattleDataStore from "@/stores/battleDataStore";
|
||||||
import useLocaleStore from "@/stores/localeStore";
|
import useLocaleStore from "@/stores/localeStore";
|
||||||
import useSocketStore from "@/stores/socketSettingStore";
|
import useSocketStore from "@/stores/socketSettingStore";
|
||||||
import { BattleDataStateJson } from "@/types/mics";
|
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
@@ -24,15 +23,7 @@ const themes = [
|
|||||||
export default function Header() {
|
export default function Header() {
|
||||||
const { changeTheme } = useChangeTheme()
|
const { changeTheme } = useChangeTheme()
|
||||||
const { locale, setLocale } = useLocaleStore()
|
const { locale, setLocale } = useLocaleStore()
|
||||||
const {
|
const { loadBattleDataFromJSON } = useBattleDataStore()
|
||||||
totalAV,
|
|
||||||
totalDamage,
|
|
||||||
damagePerAV,
|
|
||||||
turnHistory,
|
|
||||||
lineup,
|
|
||||||
loadBattleDataFromJSON,
|
|
||||||
} = useBattleDataStore()
|
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const transI18n = useTranslations("DataAnalysisPage")
|
const transI18n = useTranslations("DataAnalysisPage")
|
||||||
const { host, port, status, connectionType, setHost, setPort, setStatus, setConnectionType } = useSocketStore();
|
const { host, port, status, connectionType, setHost, setPort, setStatus, setConnectionType } = useSocketStore();
|
||||||
@@ -201,7 +192,7 @@ export default function Header() {
|
|||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium"
|
className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium"
|
||||||
onClick={() => exportBattleData({ totalAV, totalDamage, turnHistory, damagePerAV, lineup } as BattleDataStateJson)}
|
onClick={() => exportBattleData()}
|
||||||
>
|
>
|
||||||
{transI18n("exportData")}
|
{transI18n("exportData")}
|
||||||
</button>
|
</button>
|
||||||
@@ -261,7 +252,7 @@ export default function Header() {
|
|||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium"
|
className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium"
|
||||||
onClick={() => exportBattleData({ totalAV, totalDamage, turnHistory, damagePerAV, lineup } as BattleDataStateJson)}
|
onClick={() => exportBattleData()}
|
||||||
>
|
>
|
||||||
{transI18n("exportData")}
|
{transI18n("exportData")}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import useBattleDataStore from "@/stores/battleDataStore";
|
|||||||
import CharacterCard from "../card/characterCard";
|
import CharacterCard from "../card/characterCard";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { AvatarType } from "@/types";
|
import { AvatarHakushiType } from "@/types";
|
||||||
import useLocaleStore from "@/stores/localeStore";
|
import useLocaleStore from "@/stores/localeStore";
|
||||||
import { getNameChar } from '@/helper/getNameChar';
|
import { getNameChar } from '@/helper/getNameChar';
|
||||||
import SkillBarChart from "../chart/skillBarChart";
|
import SkillBarChart from "../chart/skillBarChart";
|
||||||
@@ -13,19 +13,17 @@ import { motion } from "framer-motion";
|
|||||||
import { DamageLineForOne } from "../chart/damageLineForOne";
|
import { DamageLineForOne } from "../chart/damageLineForOne";
|
||||||
import { DamagePerCycleForOne } from "../chart/damagePerCycleForOne";
|
import { DamagePerCycleForOne } from "../chart/damagePerCycleForOne";
|
||||||
import { useCalcTotalDmgAvatar, useCalcTotalTurnAvatar } from "@/hooks/useCalcAvatarData";
|
import { useCalcTotalDmgAvatar, useCalcTotalTurnAvatar } from "@/hooks/useCalcAvatarData";
|
||||||
|
import Image from "next/image";
|
||||||
|
// import ShowCaseInfo from "../card/showCaseCard";
|
||||||
|
|
||||||
export default function LineupBar() {
|
export default function LineupBar() {
|
||||||
const [selectedCharacter, setSelectedCharacter] = useState<AvatarType | null>(null);
|
const [selectedCharacter, setSelectedCharacter] = useState<AvatarHakushiType | null>(null);
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
|
||||||
const transI18n = useTranslations("DataAnalysisPage");
|
const transI18n = useTranslations("DataAnalysisPage");
|
||||||
const { lineup } = useBattleDataStore();
|
const { lineup, turnHistory, dataAvatar } = useBattleDataStore();
|
||||||
const { listAvatar } = useAvatarDataStore();
|
const { listAvatar } = useAvatarDataStore();
|
||||||
const { locale } = useLocaleStore();
|
const { locale } = useLocaleStore();
|
||||||
const [modeBar, setModeBar] = useState<0 | 1 | 2>(1);
|
|
||||||
const [modeLine, setModeLine] = useState<0 | 1>(1);
|
|
||||||
|
|
||||||
|
|
||||||
const totalDamage = useCalcTotalDmgAvatar(selectedCharacter ? Number(selectedCharacter.id) : 0);
|
const totalDamage = useCalcTotalDmgAvatar(selectedCharacter ? Number(selectedCharacter.id) : 0);
|
||||||
const totalTurn = useCalcTotalTurnAvatar(selectedCharacter ? Number(selectedCharacter.id) : 0)
|
const totalTurn = useCalcTotalTurnAvatar(selectedCharacter ? Number(selectedCharacter.id) : 0)
|
||||||
|
|
||||||
@@ -34,7 +32,7 @@ export default function LineupBar() {
|
|||||||
lineup.some(av => av.avatarId.toString() === item.id)
|
lineup.some(av => av.avatarId.toString() === item.id)
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleShow = (modalId: string, item: AvatarType) => {
|
const handleShow = (modalId: string, item: AvatarHakushiType) => {
|
||||||
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
|
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
|
||||||
if (modal) {
|
if (modal) {
|
||||||
setSelectedCharacter(item);
|
setSelectedCharacter(item);
|
||||||
@@ -76,62 +74,47 @@ export default function LineupBar() {
|
|||||||
{transI18n("lineupInfo")}
|
{transI18n("lineupInfo")}
|
||||||
</motion.h2>
|
</motion.h2>
|
||||||
|
|
||||||
<div className="relative h-full pt-2 max-h-[90vh] border-t-2 border-accent">
|
<div className="flex justify-center mb-2">
|
||||||
|
<div className="badge badge-accent gap-2 text-lg items-center px-4 py-2 whitespace-nowrap max-w-full">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="size-6 shrink-0">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M17.982 18.725A7.488 7.488 0 0 0 12 15.75a7.488 7.488 0 0 0-5.982 2.975m11.963 0a9 9 0 1 0-11.963 0m11.963 0A8.966 8.966 0 0 1 12 21a8.966 8.966 0 0 1-5.982-2.275M15 9.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<span className="text-sm truncate">{transI18n("lastTurn")}: {getNameChar(locale, listAvatar.find(it => it.id === turnHistory.findLast(i => i?.avatarId)?.avatarId?.toString()))}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div className="relative h-full pt-2 max-h-[90vh] lg:max-h-[80vh] border-t-2 border-accent">
|
||||||
{lineupAvatars.length === 0 ? (
|
{lineupAvatars.length === 0 ? (
|
||||||
<div className="h-full flex justify-center items-start">
|
<div className="h-full flex justify-center items-start">
|
||||||
<p className="text-base-content opacity-50">{transI18n("noCharactersInLineup")}</p>
|
<p className="text-base-content opacity-50">{transI18n("noCharactersInLineup")}</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="h-full w-full overflow-x-auto md:overflow-x-hidden md:overflow-y-auto custom-scrollbar rounded-lg">
|
<div className="h-full w-full overflow-x-auto md:overflow-x-hidden md:overflow-y-auto rounded-lg">
|
||||||
<style jsx>{`
|
|
||||||
.custom-scrollbar {
|
|
||||||
scrollbar-width: thin;
|
|
||||||
scrollbar-color: hsl(var(--p)) hsl(var(--b3));
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-scrollbar::-webkit-scrollbar {
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-scrollbar::-webkit-scrollbar-track {
|
|
||||||
background: hsl(var(--b3));
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
|
||||||
background: hsl(var(--p));
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: hsl(var(--pf));
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-scrollbar::-webkit-scrollbar-button {
|
|
||||||
display: none;
|
|
||||||
height: 0;
|
|
||||||
width: 0;
|
|
||||||
}
|
|
||||||
`}</style>
|
|
||||||
|
|
||||||
<div className="flex flex-nowrap md:grid md:grid-cols-1 w-fit md:w-full justify-items-center items-start gap-2">
|
<div className="flex flex-nowrap md:grid md:grid-cols-1 w-fit md:w-full justify-items-center items-start gap-2">
|
||||||
{lineupAvatars.map((item, index) => (
|
{lineupAvatars.map((item, index) => {
|
||||||
|
const lastTurnAvatarId = turnHistory.findLast(i => i?.avatarId)?.avatarId || -1;
|
||||||
|
const isLastTurn = item.id === lastTurnAvatarId.toString();
|
||||||
|
|
||||||
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={item.id}
|
key={item.id}
|
||||||
initial={{ opacity: 0, scale: 0.9 }}
|
initial={{ opacity: 0, scale: 0.9 }}
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
transition={{ duration: 0.3, delay: index * 0.1 }}
|
transition={{ duration: 0.3, delay: index * 0.1 }}
|
||||||
whileHover={{ scale: 1.05 }}
|
whileHover={{ scale: 1.05 }}
|
||||||
className="cursor-pointer flex-shrink-0 md:w-full justify-items-center"
|
|
||||||
onClick={() => handleShow("character_detail_modal", item)}
|
onClick={() => handleShow("character_detail_modal", item)}
|
||||||
|
className={`cursor-pointer flex-shrink-0 justify-items-center ${isLastTurn ? "shadow-[inset_0_0_10px_2px_rgba(59,130,246,0.7),_0_0_20px_5px_rgba(59,130,246,0.3)]" : ""
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<CharacterCard data={item} />
|
<CharacterCard data={item} />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
))}
|
);
|
||||||
</div>
|
})}
|
||||||
</div>
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Character Detail Modal */}
|
{/* Character Detail Modal */}
|
||||||
@@ -165,7 +148,7 @@ export default function LineupBar() {
|
|||||||
<h4 className="text-lg font-semibold mb-2 text-pink-500">{transI18n("characterInformation")}</h4>
|
<h4 className="text-lg font-semibold mb-2 text-pink-500">{transI18n("characterInformation")}</h4>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2">
|
<div className="grid grid-cols-1 sm:grid-cols-2">
|
||||||
<div className="flex flex-col space-y-2 relative">
|
<div className="grid grid-cols-1 lg:grid-cols-2 justify-items-start items-center gap-6">
|
||||||
<p>
|
<p>
|
||||||
{transI18n("id")}: <span className="font-bold">{selectedCharacter.id}</span>
|
{transI18n("id")}: <span className="font-bold">{selectedCharacter.id}</span>
|
||||||
</p>
|
</p>
|
||||||
@@ -173,11 +156,13 @@ export default function LineupBar() {
|
|||||||
<span>{transI18n("path")}:</span>
|
<span>{transI18n("path")}:</span>
|
||||||
<span className="font-bold">{transI18n(selectedCharacter.baseType.toLowerCase())}</span>
|
<span className="font-bold">{transI18n(selectedCharacter.baseType.toLowerCase())}</span>
|
||||||
{selectedCharacter.baseType && (
|
{selectedCharacter.baseType && (
|
||||||
<img
|
<Image
|
||||||
loading="lazy"
|
|
||||||
src={`https://api.hakush.in/hsr/UI/pathicon/${selectedCharacter.baseType.toLowerCase()}.webp`}
|
src={`https://api.hakush.in/hsr/UI/pathicon/${selectedCharacter.baseType.toLowerCase()}.webp`}
|
||||||
className="w-6 h-6"
|
className="w-6 h-6"
|
||||||
alt={selectedCharacter.baseType.toLowerCase()}
|
alt={selectedCharacter.baseType.toLowerCase()}
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
@@ -186,31 +171,77 @@ export default function LineupBar() {
|
|||||||
</p>
|
</p>
|
||||||
<p className="flex items-center space-x-2">
|
<p className="flex items-center space-x-2">
|
||||||
<span>{transI18n("element")}:</span>
|
<span>{transI18n("element")}:</span>
|
||||||
<span className="font-bold">{transI18n(selectedCharacter.damageType.toLowerCase())}</span>
|
<Image
|
||||||
{selectedCharacter.damageType && (
|
|
||||||
<img
|
|
||||||
loading="lazy"
|
|
||||||
src={`https://api.hakush.in/hsr/UI/element/${selectedCharacter.damageType.toLowerCase()}.webp`}
|
src={`https://api.hakush.in/hsr/UI/element/${selectedCharacter.damageType.toLowerCase()}.webp`}
|
||||||
className="w-6 h-6"
|
className="w-6 h-6"
|
||||||
alt={selectedCharacter.damageType.toLowerCase()}
|
alt={selectedCharacter.damageType.toLowerCase()}
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
{(() => {
|
||||||
|
const avatar = dataAvatar.find(it => it.avatar_id.toString() === selectedCharacter.id);
|
||||||
|
if (!avatar) return null;
|
||||||
|
const relicIds = avatar.relics.map(item => {
|
||||||
|
const relicIdStr = item.relic_id.toString().slice(1);
|
||||||
|
const slot = relicIdStr.slice(-1);
|
||||||
|
return `${item.relic_set_id}_${slot}`;
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p>
|
||||||
|
{transI18n("level")}: <span className="font-bold">{avatar.level}</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{transI18n("eidolons")}: <span className="font-bold">{avatar?.data?.rank}</span>
|
||||||
|
</p>
|
||||||
|
<p className="flex items-center space-x-2">
|
||||||
|
<span>{transI18n("lightcones")}:</span>
|
||||||
|
<Image
|
||||||
|
src={`https://api.hakush.in/hsr/UI/lightconemediumicon/${avatar?.Lightcone?.item_id}.webp`}
|
||||||
|
className="w-12 h-12"
|
||||||
|
alt={avatar?.Lightcone?.item_id?.toString() || ""}
|
||||||
|
width={200}
|
||||||
|
height={200}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
<p className="flex items-center space-x-2 w-full">
|
||||||
|
<span>{transI18n("relics")}:</span>
|
||||||
|
<div className="grid grid-cols-3 md:flex md:flex-row w-full">
|
||||||
|
{relicIds.map(it => (
|
||||||
|
<Image
|
||||||
|
key={it}
|
||||||
|
src={`https://api.hakush.in/hsr/UI/relicfigures/IconRelic_${it}.webp`}
|
||||||
|
className="w-12 h-12"
|
||||||
|
alt={avatar?.Lightcone?.item_id?.toString() || ""}
|
||||||
|
width={200}
|
||||||
|
height={200}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-center items-center">
|
|
||||||
{selectedCharacter && (
|
</div>
|
||||||
<img
|
<Image
|
||||||
src={`https://api.hakush.in/hsr/UI/avatarshopicon/${selectedCharacter.id}.webp`}
|
src={`https://api.hakush.in/hsr/UI/avatarshopicon/${selectedCharacter.id}.webp`}
|
||||||
alt={getNameChar(locale, selectedCharacter)}
|
alt={getNameChar(locale, selectedCharacter)}
|
||||||
className="h-32 w-32 object-cover rounded-full border-2 border-purple-500"
|
className="h-32 w-32 object-cover rounded-full border-2 border-purple-500"
|
||||||
|
width={128}
|
||||||
|
height={128}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/* <div className="md:col-span-2 bg-base-200 rounded-lg p-4 shadow-md">
|
||||||
|
<h4 className="text-lg font-semibold mb-2 text-pink-500">{transI18n("characterInformation")}</h4>
|
||||||
|
<ShowCaseInfo></ShowCaseInfo>
|
||||||
|
</div> */}
|
||||||
<div className="bg-base-200 rounded-lg p-4 shadow-md">
|
<div className="bg-base-200 rounded-lg p-4 shadow-md">
|
||||||
<p className="mt-2 font-bold text-lg text-cyan-500">{transI18n("totalTurn")}: <span className="text-base-content">{totalTurn.toFixed(2)}</span></p>
|
<p className="mt-2 font-bold text-lg text-cyan-500">{transI18n("totalTurn")}: <span className="text-base-content">{totalTurn.toFixed(2)}</span></p>
|
||||||
</div>
|
</div>
|
||||||
@@ -218,52 +249,30 @@ export default function LineupBar() {
|
|||||||
<h4 className="text-lg font-semibold mb-2 text-purple-500">{transI18n("totalDamage")}: <span className="text-base-content">{totalDamage.toFixed(2)}</span></h4>
|
<h4 className="text-lg font-semibold mb-2 text-purple-500">{transI18n("totalDamage")}: <span className="text-base-content">{totalDamage.toFixed(2)}</span></h4>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-base-200 rounded-lg p-4 shadow-md">
|
||||||
<div className="bg-base-200 rounded-lg p-4 shadow-md max-h-11/12">
|
|
||||||
<h4 className="text-lg font-semibold mb-4 text-purple-500">{transI18n("skillDamageBreakdown")}</h4>
|
<h4 className="text-lg font-semibold mb-4 text-purple-500">{transI18n("skillDamageBreakdown")}</h4>
|
||||||
<SkillBarChart avatarId={Number(selectedCharacter.id) ?? 0} />
|
<SkillBarChart avatarId={Number(selectedCharacter.id) ?? 0} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-base-200 rounded-lg p-4 shadow-md max-h-11/12">
|
<div className="bg-base-200 rounded-lg p-4 shadow-md">
|
||||||
<div className="flex justify-between items-center mb-4">
|
<div className="flex justify-between items-center mb-4">
|
||||||
<h4 className="text-lg font-semibold mb-4 text-cyan-500">{transI18n("damageOverTime")}</h4>
|
<h4 className="text-lg font-semibold mb-4 text-cyan-500">{transI18n("damageOverTime")}</h4>
|
||||||
<div className="flex gap-2">
|
|
||||||
{[0, 1].map((m) => (
|
|
||||||
<button
|
|
||||||
key={m}
|
|
||||||
onClick={() => setModeLine(m as 0 | 1)}
|
|
||||||
className={`btn btn-sm ${modeLine === m ? "btn-accent" : "btn-ghost"}`}
|
|
||||||
>
|
|
||||||
{transI18n("type")} {m}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DamageLineForOne avatarId={Number(selectedCharacter.id) ?? 0} mode={modeLine} />
|
<DamageLineForOne avatarId={Number(selectedCharacter.id) ?? 0} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-base-200 rounded-lg p-4 shadow-md max-h-11/12">
|
<div className="bg-base-200 rounded-lg p-4 shadow-md">
|
||||||
<h4 className="text-lg font-semibold mb-4 text-cyan-500">{transI18n("skillUsageDistribution")}</h4>
|
<h4 className="text-lg font-semibold mb-4 text-cyan-500">{transI18n("skillUsageDistribution")}</h4>
|
||||||
<SkillPieChart avatarId={Number(selectedCharacter.id) ?? 0} />
|
<SkillPieChart avatarId={Number(selectedCharacter.id) ?? 0} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-base-200 rounded-lg p-4 shadow-md max-h-11/12">
|
<div className="bg-base-200 rounded-lg p-4 shadow-md">
|
||||||
<div className="flex justify-between items-center mb-4">
|
<div className="flex justify-between items-center mb-4">
|
||||||
<h4 className="text-lg font-semibold text-purple-500">{transI18n("damagerPerCycle")}</h4>
|
<h4 className="text-lg font-semibold text-purple-500">{transI18n("damagePerCycle")}</h4>
|
||||||
<div className="flex gap-2">
|
|
||||||
{[0, 1, 2].map((m) => (
|
|
||||||
<button
|
|
||||||
key={m}
|
|
||||||
onClick={() => setModeBar(m as 0 | 1 | 2)}
|
|
||||||
className={`btn btn-sm ${modeBar === m ? "btn-accent" : "btn-ghost"}`}
|
|
||||||
>
|
|
||||||
{transI18n("type")} {m}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<DamagePerCycleForOne avatarId={Number(selectedCharacter.id) ?? 0} />
|
||||||
<DamagePerCycleForOne avatarId={Number(selectedCharacter.id) ?? 0} mode={modeBar} />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
@@ -1,9 +1,36 @@
|
|||||||
|
import useBattleDataStore from "@/stores/battleDataStore";
|
||||||
import { BattleDataStateJson } from "@/types/mics";
|
import { BattleDataStateJson } from "@/types/mics";
|
||||||
|
|
||||||
export const exportBattleData = (
|
export const exportBattleData = (
|
||||||
data: BattleDataStateJson,
|
|
||||||
filename = 'battle_data.json'
|
filename = 'battle_data.json'
|
||||||
) => {
|
) => {
|
||||||
|
const { lineup,
|
||||||
|
turnHistory,
|
||||||
|
skillHistory,
|
||||||
|
totalAV,
|
||||||
|
totalDamage,
|
||||||
|
damagePerAV,
|
||||||
|
cycleIndex,
|
||||||
|
waveIndex,
|
||||||
|
dataAvatar,
|
||||||
|
maxWave,
|
||||||
|
maxCycle
|
||||||
|
} = useBattleDataStore.getState();
|
||||||
|
|
||||||
|
const data: BattleDataStateJson = {
|
||||||
|
lineup,
|
||||||
|
turnHistory,
|
||||||
|
skillHistory,
|
||||||
|
dataAvatar,
|
||||||
|
totalAV,
|
||||||
|
totalDamage,
|
||||||
|
damagePerAV,
|
||||||
|
cycleIndex,
|
||||||
|
waveIndex,
|
||||||
|
maxWave,
|
||||||
|
maxCycle
|
||||||
|
}
|
||||||
|
|
||||||
const dataStr = JSON.stringify(data, null, 2);
|
const dataStr = JSON.stringify(data, null, 2);
|
||||||
const blob = new Blob([dataStr], { type: 'application/json' });
|
const blob = new Blob([dataStr], { type: 'application/json' });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { listCurrentLanguage } from "@/lib/constant";
|
import { listCurrentLanguage } from "@/lib/constant";
|
||||||
import { AvatarType } from "@/types";
|
import { AvatarHakushiType } from "@/types";
|
||||||
|
|
||||||
|
|
||||||
export function getNameChar(locale: string, data: AvatarType | undefined): string {
|
export function getNameChar(locale: string, data: AvatarHakushiType | undefined): string {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import useBattleDataStore from "@/stores/battleDataStore";
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
|
||||||
export function useCalcTotalDmgAvatar(avatarId: number) {
|
export function useCalcTotalDmgAvatar(avatarId: number) {
|
||||||
const { turnHistory } = useBattleDataStore.getState();
|
const { skillHistory } = useBattleDataStore.getState();
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
return turnHistory
|
return skillHistory
|
||||||
.filter(t => t.avatarId === avatarId)
|
.filter(t => t.avatarId === avatarId)
|
||||||
.reduce((sum, turn) => sum + turn.totalDamage, 0);
|
.reduce((sum, turn) => sum + turn.totalDamage, 0);
|
||||||
}, [avatarId, turnHistory]);
|
}, [avatarId, skillHistory]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,16 +2,16 @@ import useBattleDataStore from "@/stores/battleDataStore";
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
|
||||||
export function useDamageLineForOne(avatarId: number, mode: 0 | 1 = 0) {
|
export function useDamageLineForOne(avatarId: number, mode: 0 | 1 = 0) {
|
||||||
const { turnHistory } = useBattleDataStore.getState();
|
const { skillHistory, turnHistory } = useBattleDataStore.getState();
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
const map = new Map<number, number>();
|
const map = new Map<number, number>();
|
||||||
|
|
||||||
for (const turn of turnHistory) {
|
for (const skill of skillHistory) {
|
||||||
if (turn.avatarId !== avatarId) continue;
|
if (skill.avatarId !== avatarId) continue;
|
||||||
|
const actionValue = turnHistory[skill.turnBattleId].actionValue
|
||||||
const prev = map.get(turn.actionValue) || 0;
|
const prev = map.get(actionValue) || 0;
|
||||||
map.set(turn.actionValue, prev + turn.totalDamage);
|
map.set(actionValue, prev + skill.totalDamage);
|
||||||
}
|
}
|
||||||
|
|
||||||
const points = Array.from(map.entries())
|
const points = Array.from(map.entries())
|
||||||
@@ -27,5 +27,5 @@ export function useDamageLineForOne(avatarId: number, mode: 0 | 1 = 0) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return points.map(p => ({ x: p.x, y: Number(p.y.toFixed(2)) }));
|
return points.map(p => ({ x: p.x, y: Number(p.y.toFixed(2)) }));
|
||||||
}, [avatarId, turnHistory, mode]);
|
}, [avatarId, skillHistory, turnHistory, mode]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
import useBattleDataStore from "@/stores/battleDataStore";
|
import useBattleDataStore from "@/stores/battleDataStore";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
|
||||||
export function useDamageLinesForAll(mode: 0 | 1 = 0) {
|
export function useDamageLinesForAll(mode: 1 | 2 = 1) {
|
||||||
const { turnHistory } = useBattleDataStore.getState();
|
const { turnHistory, skillHistory } = useBattleDataStore.getState();
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
const avatarMap = new Map<number, Map<number, number>>();
|
const avatarMap = new Map<number, Map<number, number>>();
|
||||||
|
|
||||||
for (const turn of turnHistory) {
|
for (const skill of skillHistory) {
|
||||||
if (!avatarMap.has(turn.avatarId)) {
|
if (!avatarMap.has(skill.avatarId)) {
|
||||||
avatarMap.set(turn.avatarId, new Map());
|
avatarMap.set(skill.avatarId, new Map());
|
||||||
}
|
}
|
||||||
|
|
||||||
const charMap = avatarMap.get(turn.avatarId)!;
|
const charMap = avatarMap.get(skill.avatarId)!;
|
||||||
const prev = charMap.get(turn.actionValue) || 0;
|
const actionValue = turnHistory[skill.turnBattleId].actionValue
|
||||||
charMap.set(turn.actionValue, prev + turn.totalDamage);
|
const prev = charMap.get(actionValue) || 0;
|
||||||
|
charMap.set(actionValue, prev + skill.totalDamage);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result: Record<number, { x: number; y: number }[]> = {};
|
const result: Record<number, { x: number; y: number }[]> = {};
|
||||||
@@ -27,7 +28,7 @@ export function useDamageLinesForAll(mode: 0 | 1 = 0) {
|
|||||||
if (mode === 1) {
|
if (mode === 1) {
|
||||||
let cumulative = 0;
|
let cumulative = 0;
|
||||||
result[avatarId] = points.map(p => {
|
result[avatarId] = points.map(p => {
|
||||||
cumulative += p.y;
|
cumulative += Number(p.y);
|
||||||
return { x: p.x, y: Number(cumulative.toFixed(2)) };
|
return { x: p.x, y: Number(cumulative.toFixed(2)) };
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -39,5 +40,5 @@ export function useDamageLinesForAll(mode: 0 | 1 = 0) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}, [turnHistory, mode]);
|
}, [turnHistory, skillHistory, mode]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,79 +1,69 @@
|
|||||||
import useBattleDataStore from "@/stores/battleDataStore";
|
import useBattleDataStore from "@/stores/battleDataStore";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
|
||||||
type Mode = 0 | 1 | 2;
|
type Mode = 0 | 1 | 2;
|
||||||
|
|
||||||
function getChunkIndex(av: number, mode: Mode): number {
|
|
||||||
let sum = 0;
|
|
||||||
let idx = 0;
|
|
||||||
|
|
||||||
if (mode === 0) {
|
|
||||||
while (sum <= av) {
|
|
||||||
sum += 100;
|
|
||||||
idx++;
|
|
||||||
}
|
|
||||||
} else if (mode === 1) {
|
|
||||||
if (av < 150) return 0;
|
|
||||||
sum = 150;
|
|
||||||
idx = 1;
|
|
||||||
while (sum <= av) {
|
|
||||||
sum += 100;
|
|
||||||
idx++;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (av < 150) return 0;
|
|
||||||
if (av < 300) return 1;
|
|
||||||
sum = 300;
|
|
||||||
idx = 2;
|
|
||||||
while (sum <= av) {
|
|
||||||
sum += 100;
|
|
||||||
idx++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return idx - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useDamagePerCycleForOne(avatarId: number, mode: Mode) {
|
export function useDamagePerCycleForOne(avatarId: number, mode: Mode) {
|
||||||
const { turnHistory } = useBattleDataStore.getState();
|
const { skillHistory, turnHistory, maxCycle } = useBattleDataStore.getState();
|
||||||
|
const transI18n = useTranslations("DataAnalysisPage");
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
const damageMap = new Map<number, number>();
|
const damageMap = new Map<string, number>();
|
||||||
|
|
||||||
turnHistory
|
skillHistory
|
||||||
.filter(t => t.avatarId === avatarId)
|
.filter(s => s.avatarId === avatarId)
|
||||||
.forEach(t => {
|
.forEach(s => {
|
||||||
const idx = getChunkIndex(t.actionValue, mode);
|
const turn = turnHistory[s.turnBattleId];
|
||||||
damageMap.set(idx, (damageMap.get(idx) || 0) + t.totalDamage);
|
if (!turn) return;
|
||||||
|
|
||||||
|
let key = '';
|
||||||
|
if (mode === 0) {
|
||||||
|
key = `${transI18n('cycle')} ${maxCycle-turn.cycleIndex} - ${transI18n('wave')} ${turn.waveIndex}`;
|
||||||
|
} else if (mode === 1) {
|
||||||
|
key = `${transI18n('cycle')} ${maxCycle-turn.cycleIndex}`;
|
||||||
|
} else if (mode === 2) {
|
||||||
|
key = `${transI18n('wave')} ${turn.waveIndex}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
damageMap.set(key, (damageMap.get(key) || 0) + s.totalDamage);
|
||||||
});
|
});
|
||||||
|
|
||||||
const maxIndex = Math.max(...Array.from(damageMap.keys()), 0);
|
const result = Array.from(damageMap.entries())
|
||||||
|
.map(([x, y]) => ({ x, y }))
|
||||||
|
.sort((a, b) => a.x.localeCompare(b.x, undefined, { numeric: true }));
|
||||||
|
|
||||||
return Array.from({ length: maxIndex + 1 }).map((_, i) => ({
|
return result;
|
||||||
x: `${i + 1}`,
|
}, [avatarId, mode, skillHistory, turnHistory, transI18n]);
|
||||||
y: damageMap.get(i) || 0,
|
|
||||||
}));
|
|
||||||
}, [avatarId, mode, turnHistory]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function useDamagePerCycleForAll(mode: Mode) {
|
export function useDamagePerCycleForAll(mode: Mode) {
|
||||||
const { turnHistory } = useBattleDataStore.getState();
|
const { skillHistory, turnHistory, maxCycle } = useBattleDataStore.getState();
|
||||||
|
const transI18n = useTranslations("DataAnalysisPage");
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
const damageMap = new Map<number, number>();
|
const damageMap = new Map<string, number>();
|
||||||
|
|
||||||
turnHistory.forEach(t => {
|
skillHistory.forEach(s => {
|
||||||
const idx = getChunkIndex(t.actionValue, mode);
|
const turn = turnHistory[s.turnBattleId];
|
||||||
damageMap.set(idx, (damageMap.get(idx) || 0) + t.totalDamage);
|
if (!turn) return;
|
||||||
|
|
||||||
|
let key = '';
|
||||||
|
if (mode === 0) {
|
||||||
|
key = `${transI18n('cycle')} ${maxCycle-turn.cycleIndex} - ${transI18n('wave')} ${turn.waveIndex}`;
|
||||||
|
} else if (mode === 1) {
|
||||||
|
key = `${transI18n('cycle')} ${maxCycle-turn.cycleIndex}`;
|
||||||
|
} else if (mode === 2) {
|
||||||
|
key = `${transI18n('wave')} ${turn.waveIndex}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
damageMap.set(key, (damageMap.get(key) || 0) + s.totalDamage);
|
||||||
});
|
});
|
||||||
|
|
||||||
const maxIndex = Math.max(...Array.from(damageMap.keys()), 0);
|
const result = Array.from(damageMap.entries())
|
||||||
|
.map(([x, y]) => ({ x, y }))
|
||||||
|
.sort((a, b) => a.x.localeCompare(b.x, undefined, { numeric: true }));
|
||||||
|
|
||||||
return Array.from({ length: maxIndex + 1 }).map((_, i) => ({
|
return result;
|
||||||
x: `${i + 1}`,
|
}, [mode, skillHistory, turnHistory, transI18n]);
|
||||||
y: damageMap.get(i) || 0,
|
|
||||||
}));
|
|
||||||
}, [mode, turnHistory]);
|
|
||||||
}
|
}
|
||||||
26
src/hooks/useDamagePercentForDamgeType.ts
Normal file
26
src/hooks/useDamagePercentForDamgeType.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import useBattleDataStore from "@/stores/battleDataStore";
|
||||||
|
import {attackTypeToString} from "@/types";
|
||||||
|
|
||||||
|
export function useDamagePercentByType() {
|
||||||
|
const { skillHistory } = useBattleDataStore.getState();
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
const dmgByType = new Map<string, number>();
|
||||||
|
|
||||||
|
skillHistory.forEach(skill => {
|
||||||
|
skill.damageDetail.forEach(detail => {
|
||||||
|
const type = detail.damage_type;
|
||||||
|
if (!type) return;
|
||||||
|
dmgByType.set(attackTypeToString(type), (dmgByType.get(attackTypeToString(type)) || 0) + detail.damage);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalDmg = Array.from(dmgByType.values()).reduce((sum, val) => sum + val, 0);
|
||||||
|
|
||||||
|
return Array.from(dmgByType.entries()).map(([type, dmg]) => ({
|
||||||
|
type,
|
||||||
|
percent: totalDmg > 0 ? (dmg / totalDmg) * 100 : 0,
|
||||||
|
}));
|
||||||
|
}, [skillHistory]);
|
||||||
|
}
|
||||||
@@ -2,12 +2,12 @@ import useBattleDataStore from "@/stores/battleDataStore";
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
|
||||||
export function useDamagePercentPerAvatar() {
|
export function useDamagePercentPerAvatar() {
|
||||||
const { turnHistory } = useBattleDataStore.getState();
|
const { skillHistory } = useBattleDataStore.getState();
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
const dmgByAvatar = new Map<number, number>();
|
const dmgByAvatar = new Map<number, number>();
|
||||||
|
|
||||||
turnHistory.forEach(t => {
|
skillHistory.forEach(t => {
|
||||||
dmgByAvatar.set(t.avatarId, (dmgByAvatar.get(t.avatarId) || 0) + t.totalDamage);
|
dmgByAvatar.set(t.avatarId, (dmgByAvatar.get(t.avatarId) || 0) + t.totalDamage);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -17,5 +17,5 @@ export function useDamagePercentPerAvatar() {
|
|||||||
avatarId,
|
avatarId,
|
||||||
percent: totalDmg > 0 ? (dmg / totalDmg) * 100 : 0,
|
percent: totalDmg > 0 ? (dmg / totalDmg) * 100 : 0,
|
||||||
}));
|
}));
|
||||||
}, [turnHistory]);
|
}, [skillHistory]);
|
||||||
}
|
}
|
||||||
|
|||||||
26
src/hooks/useDamageTypeForAvatar.ts
Normal file
26
src/hooks/useDamageTypeForAvatar.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
"use client";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import useBattleDataStore from "@/stores/battleDataStore";
|
||||||
|
import {attackTypeToString} from "@/types";
|
||||||
|
|
||||||
|
export function useDamageByTypeForAvatar(avatarId: number) {
|
||||||
|
const { skillHistory } = useBattleDataStore.getState();
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
const dmgMap = new Map<string, number>();
|
||||||
|
|
||||||
|
for (const skill of skillHistory) {
|
||||||
|
if (skill.avatarId !== avatarId) continue;
|
||||||
|
|
||||||
|
for (const damage of skill.damageDetail) {
|
||||||
|
const type = attackTypeToString(damage?.damage_type)?.toLowerCase() || "";
|
||||||
|
dmgMap.set(type, (dmgMap.get(type) || 0) + damage.damage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const labels = Array.from(dmgMap.keys());
|
||||||
|
const damageValues = Array.from(dmgMap.values());
|
||||||
|
|
||||||
|
return { labels, damageValues };
|
||||||
|
}, [avatarId, skillHistory]);
|
||||||
|
}
|
||||||
24
src/hooks/useSkillDamageForAvatar.ts
Normal file
24
src/hooks/useSkillDamageForAvatar.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
"use client";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import useBattleDataStore from "@/stores/battleDataStore";
|
||||||
|
import {attackTypeToString} from "@/types";
|
||||||
|
|
||||||
|
export function useSkillDamageForAvatar(avatarId: number) {
|
||||||
|
const { skillHistory } = useBattleDataStore.getState();
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
const dmgMap = new Map<string, number>();
|
||||||
|
|
||||||
|
for (const skill of skillHistory) {
|
||||||
|
if (skill.avatarId !== avatarId) continue;
|
||||||
|
|
||||||
|
const type = attackTypeToString(skill.skillType).toLowerCase();
|
||||||
|
dmgMap.set(type, (dmgMap.get(type) || 0) + skill.totalDamage);
|
||||||
|
}
|
||||||
|
|
||||||
|
const labels = Array.from(dmgMap.keys());
|
||||||
|
const damageValues = Array.from(dmgMap.values());
|
||||||
|
|
||||||
|
return { labels, damageValues };
|
||||||
|
}, [avatarId, skillHistory]);
|
||||||
|
}
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import { useMemo } from "react";
|
|
||||||
import useBattleDataStore from "@/stores/battleDataStore";
|
|
||||||
|
|
||||||
export function useSkillDamageByAvatar(avatarId: number) {
|
|
||||||
const { turnHistory } = useBattleDataStore.getState();
|
|
||||||
|
|
||||||
return useMemo(() => {
|
|
||||||
const skillTypes = ['technique', 'talent', 'basic', 'skill', 'ultimate', 'servant'];
|
|
||||||
const dmgMap = new Map<string, number>();
|
|
||||||
skillTypes.forEach((type) => dmgMap.set(type, 0));
|
|
||||||
|
|
||||||
for (const turn of turnHistory) {
|
|
||||||
const type = turn.skillType.toLowerCase();
|
|
||||||
if (turn.avatarId === avatarId && dmgMap.has(type)) {
|
|
||||||
dmgMap.set(type, dmgMap.get(type)! + turn.totalDamage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const labels = Array.from(dmgMap.keys());
|
|
||||||
const damageValues = Array.from(dmgMap.values());
|
|
||||||
|
|
||||||
return { labels, damageValues };
|
|
||||||
}, [avatarId, turnHistory]);
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import useSocketStore from "@/stores/socketSettingStore";
|
import useSocketStore from "@/stores/socketSettingStore";
|
||||||
import { AvatarHakushiType, AvatarType } from "@/types/avatar";
|
import { AvatarHakushiType, AvatarHakushiRawType } from "@/types/avatar";
|
||||||
|
|
||||||
|
|
||||||
export async function checkConnectTcpApi(): Promise<boolean> {
|
export async function checkConnectTcpApi(): Promise<boolean> {
|
||||||
@@ -22,7 +22,7 @@ export async function checkConnectTcpApi(): Promise<boolean> {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getCharacterListApi(): Promise<AvatarType[]> {
|
export async function getCharacterListApi(): Promise<AvatarHakushiType[]> {
|
||||||
const res = await fetch('/api/hakushin', {
|
const res = await fetch('/api/hakushin', {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -35,14 +35,14 @@ export async function getCharacterListApi(): Promise<AvatarType[]> {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const data: Map<string, AvatarHakushiType> = new Map(Object.entries(await res.json()));
|
const data: Map<string, AvatarHakushiRawType> = new Map(Object.entries(await res.json()));
|
||||||
|
|
||||||
|
|
||||||
return Array.from(data.entries()).map(([id, it]) => convertAvatar(id, it));
|
return Array.from(data.entries()).map(([id, it]) => convertAvatar(id, it));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function convertAvatar(id: string, item: AvatarHakushiType): AvatarType {
|
function convertAvatar(id: string, item: AvatarHakushiRawType): AvatarHakushiType {
|
||||||
const lang = new Map<string, string>([
|
const lang = new Map<string, string>([
|
||||||
['en', item.en],
|
['en', item.en],
|
||||||
['kr', item.kr],
|
['kr', item.kr],
|
||||||
@@ -50,7 +50,7 @@ function convertAvatar(id: string, item: AvatarHakushiType): AvatarType {
|
|||||||
['jp', item.jp]
|
['jp', item.jp]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const result: AvatarType = {
|
const result: AvatarHakushiType = {
|
||||||
release: item.release,
|
release: item.release,
|
||||||
icon: item.icon,
|
icon: item.icon,
|
||||||
rank: item.rank,
|
rank: item.rank,
|
||||||
|
|||||||
@@ -2,14 +2,11 @@ import { io, Socket } from "socket.io-client";
|
|||||||
import useSocketStore from "@/stores/socketSettingStore";
|
import useSocketStore from "@/stores/socketSettingStore";
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import useBattleDataStore from "@/stores/battleDataStore";
|
import useBattleDataStore from "@/stores/battleDataStore";
|
||||||
|
import { BattleBeginType } from "@/types";
|
||||||
|
|
||||||
let socket: Socket | null = null;
|
let socket: Socket | null = null;
|
||||||
|
|
||||||
|
|
||||||
const onBattleBegin = () => {
|
|
||||||
notify("Battle Started!", "info")
|
|
||||||
}
|
|
||||||
|
|
||||||
const notify = (msg: string, type: 'info' | 'success' | 'error' = 'info') => {
|
const notify = (msg: string, type: 'info' | 'success' | 'error' = 'info') => {
|
||||||
if (type === 'success') toast.success(msg);
|
if (type === 'success') toast.success(msg);
|
||||||
else if (type === 'error') toast.error(msg);
|
else if (type === 'error') toast.error(msg);
|
||||||
@@ -25,7 +22,11 @@ export const connectSocket = (): Socket => {
|
|||||||
onKillService,
|
onKillService,
|
||||||
onDamageService,
|
onDamageService,
|
||||||
onBattleEndService,
|
onBattleEndService,
|
||||||
onTurnBeginService
|
onTurnBeginService,
|
||||||
|
onBattleBeginService,
|
||||||
|
onCreateBattleService,
|
||||||
|
onUpdateCycleService,
|
||||||
|
OnUpdateWaveService
|
||||||
} = useBattleDataStore.getState();
|
} = useBattleDataStore.getState();
|
||||||
|
|
||||||
let url = `${host}:${port}`;
|
let url = `${host}:${port}`;
|
||||||
@@ -72,21 +73,24 @@ export const connectSocket = (): Socket => {
|
|||||||
setStatus(true);
|
setStatus(true);
|
||||||
notify(`Kết nối thành công với Socket ID: ${socket?.id}`, 'success');
|
notify(`Kết nối thành công với Socket ID: ${socket?.id}`, 'success');
|
||||||
};
|
};
|
||||||
|
const onBattleBegin = (data: BattleBeginType) => {
|
||||||
|
notify("Battle Started!", "info")
|
||||||
|
onBattleBeginService(data)
|
||||||
|
}
|
||||||
|
|
||||||
if (isSocketConnected()) onConnect();
|
if (isSocketConnected()) onConnect();
|
||||||
|
|
||||||
socket.on("SetBattleLineup", (json) => onSetBattleLineupService(JSON.parse(json)));
|
socket.on("OnSetBattleLineup", (json) => onSetBattleLineupService(JSON.parse(json)));
|
||||||
socket.on("TurnEnd", (json) => onTurnEndService(JSON.parse(json)));
|
|
||||||
socket.on("OnTurnEnd", (json) => onTurnEndService(JSON.parse(json)));
|
socket.on("OnTurnEnd", (json) => onTurnEndService(JSON.parse(json)));
|
||||||
socket.on("OnUseSkill", (json) => onUseSkillService(JSON.parse(json)));
|
socket.on("OnUseSkill", (json) => onUseSkillService(JSON.parse(json)));
|
||||||
socket.on("OnKill", (json) => onKillService(JSON.parse(json)));
|
socket.on("OnKill", (json) => onKillService(JSON.parse(json)));
|
||||||
socket.on("OnDamage", (json) => onDamageService(JSON.parse(json)));
|
socket.on("OnDamage", (json) => onDamageService(JSON.parse(json)));
|
||||||
socket.on('BattleBegin', () => onBattleBegin());
|
socket.on('OnBattleBegin', (json) => onBattleBegin(JSON.parse(json)));
|
||||||
socket.on('OnBattleBegin', () => onBattleBegin());
|
|
||||||
socket.on('TurnBegin', (json) => onTurnBeginService(JSON.parse(json)));
|
|
||||||
socket.on('OnTurnBegin', (json) => onTurnBeginService(JSON.parse(json)));
|
socket.on('OnTurnBegin', (json) => onTurnBeginService(JSON.parse(json)));
|
||||||
socket.on('BattleEnd', (json) => onBattleEndService(JSON.parse(json)));
|
|
||||||
socket.on('OnBattleEnd', (json) => onBattleEndService(JSON.parse(json)));
|
socket.on('OnBattleEnd', (json) => onBattleEndService(JSON.parse(json)));
|
||||||
|
socket.on('OnUpdateCycle', (json) => onUpdateCycleService(JSON.parse(json)));
|
||||||
|
socket.on('OnUpdateWave', (json) => OnUpdateWaveService(JSON.parse(json)));
|
||||||
|
socket.on('OnCreateBattle', (json) => onCreateBattleService(JSON.parse(json)));
|
||||||
|
|
||||||
socket.on("Error", (msg: string) => {
|
socket.on("Error", (msg: string) => {
|
||||||
console.error("Server Error:", msg);
|
console.error("Server Error:", msg);
|
||||||
@@ -103,21 +107,28 @@ export const disconnectSocket = (): void => {
|
|||||||
onKillService,
|
onKillService,
|
||||||
onDamageService,
|
onDamageService,
|
||||||
onBattleEndService,
|
onBattleEndService,
|
||||||
onTurnBeginService
|
onTurnBeginService,
|
||||||
|
onBattleBeginService,
|
||||||
|
onCreateBattleService,
|
||||||
|
onUpdateCycleService,
|
||||||
|
OnUpdateWaveService
|
||||||
} = useBattleDataStore.getState();
|
} = useBattleDataStore.getState();
|
||||||
|
const onBattleBegin = (data: BattleBeginType) => {
|
||||||
|
notify("Battle Started!", "info")
|
||||||
|
onBattleBeginService(data)
|
||||||
|
}
|
||||||
if (socket) {
|
if (socket) {
|
||||||
socket.off("SetBattleLineup", (json) => onSetBattleLineupService(JSON.parse(json)));
|
socket.off("OnSetBattleLineup", (json) => onSetBattleLineupService(JSON.parse(json)));
|
||||||
socket.off("TurnEnd", (json) => onTurnEndService(JSON.parse(json)));
|
|
||||||
socket.off("OnTurnEnd", (json) => onTurnEndService(JSON.parse(json)));
|
socket.off("OnTurnEnd", (json) => onTurnEndService(JSON.parse(json)));
|
||||||
socket.off("OnUseSkill", (json) => onUseSkillService(JSON.parse(json)));
|
socket.off("OnUseSkill", (json) => onUseSkillService(JSON.parse(json)));
|
||||||
socket.off("OnKill", (json) => onKillService(JSON.parse(json)));
|
socket.off("OnKill", (json) => onKillService(JSON.parse(json)));
|
||||||
socket.off("OnDamage", (json) => onDamageService(JSON.parse(json)));
|
socket.off("OnDamage", (json) => onDamageService(JSON.parse(json)));
|
||||||
socket.off('BattleBegin', () => onBattleBegin());
|
socket.off('OnBattleBegin', (json) => onBattleBegin(JSON.parse(json)));
|
||||||
socket.off('OnBattleBegin', () => onBattleBegin());
|
|
||||||
socket.off('TurnBegin', (json) => onTurnBeginService(JSON.parse(json)));
|
|
||||||
socket.off('OnTurnBegin', (json) => onTurnBeginService(JSON.parse(json)));
|
socket.off('OnTurnBegin', (json) => onTurnBeginService(JSON.parse(json)));
|
||||||
socket.off('BattleEnd', (json) => onBattleEndService(JSON.parse(json)));
|
|
||||||
socket.off('OnBattleEnd', (json) => onBattleEndService(JSON.parse(json)));
|
socket.off('OnBattleEnd', (json) => onBattleEndService(JSON.parse(json)));
|
||||||
|
socket.off('OnUpdateCycle', (json) => onUpdateCycleService(JSON.parse(json)));
|
||||||
|
socket.off('OnUpdateWave', (json) => OnUpdateWaveService(JSON.parse(json)));
|
||||||
|
socket.off('OnCreateBattle', (json) => onCreateBattleService(JSON.parse(json)));
|
||||||
socket.offAny();
|
socket.offAny();
|
||||||
socket.disconnect();
|
socket.disconnect();
|
||||||
useSocketStore.getState().setStatus(false);
|
useSocketStore.getState().setStatus(false);
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { AvatarType } from '@/types';
|
import { AvatarHakushiType } from '@/types';
|
||||||
import { create } from 'zustand'
|
import { create } from 'zustand'
|
||||||
|
|
||||||
|
|
||||||
interface AvatarDataState {
|
interface AvatarDataState {
|
||||||
listAvatar: AvatarType[];
|
listAvatar: AvatarHakushiType[];
|
||||||
setListAvatar: (list: AvatarType[]) => void;
|
setListAvatar: (list: AvatarHakushiType[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useAvatarDataStore = create<AvatarDataState>((set) => ({
|
const useAvatarDataStore = create<AvatarDataState>((set) => ({
|
||||||
listAvatar: [],
|
listAvatar: [],
|
||||||
setListAvatar: (list: AvatarType[]) => set({ listAvatar: list }),
|
setListAvatar: (list: AvatarHakushiType[]) => set({ listAvatar: list }),
|
||||||
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
import { AttackResultType, AvatarSkillType, BattleEndType, KillType, LineUpType, TurnBeginType, TurnEndType } from '@/types';
|
import { AttackResultType, AvatarAnalysisJson, AvatarSkillType, BattleBeginType, BattleEndType, DamageDetailType, KillType, LineUpType, TurnBeginType, TurnEndType, UpdateCycleType, UpdateWaveType } from '@/types';
|
||||||
import { AvatarBattleInfo, BattleDataStateJson, TurnBattleInfo } from '@/types/mics';
|
import { AvatarBattleInfo, BattleDataStateJson, SkillBattleInfo, TurnBattleInfo } from '@/types/mics';
|
||||||
import { create } from 'zustand'
|
import { create } from 'zustand'
|
||||||
|
|
||||||
|
|
||||||
interface BattleDataState {
|
interface BattleDataState {
|
||||||
lineup: AvatarBattleInfo[];
|
lineup: AvatarBattleInfo[];
|
||||||
turnHistory: TurnBattleInfo[]
|
turnHistory: TurnBattleInfo[]
|
||||||
|
skillHistory: SkillBattleInfo[]
|
||||||
|
dataAvatar: AvatarAnalysisJson[]
|
||||||
totalAV: number;
|
totalAV: number;
|
||||||
totalDamage: number;
|
totalDamage: number;
|
||||||
damagePerAV: number;
|
damagePerAV: number;
|
||||||
|
cycleIndex: number;
|
||||||
|
waveIndex: number;
|
||||||
|
maxWave: number;
|
||||||
|
maxCycle: number
|
||||||
|
|
||||||
onSetBattleLineupService: (data: LineUpType) => void;
|
onSetBattleLineupService: (data: LineUpType) => void;
|
||||||
onTurnEndService: (data: TurnEndType) => void;
|
onTurnEndService: (data: TurnEndType) => void;
|
||||||
@@ -16,47 +22,71 @@ interface BattleDataState {
|
|||||||
onUseSkillService: (data: AvatarSkillType) => void;
|
onUseSkillService: (data: AvatarSkillType) => void;
|
||||||
onKillService: (data: KillType) => void
|
onKillService: (data: KillType) => void
|
||||||
onDamageService: (data: AttackResultType) => void;
|
onDamageService: (data: AttackResultType) => void;
|
||||||
onBattleStartService: () => void;
|
onBattleBeginService: (data: BattleBeginType) => void;
|
||||||
onTurnBeginService: (data: TurnBeginType) => void;
|
onTurnBeginService: (data: TurnBeginType) => void;
|
||||||
|
onCreateBattleService: (data: AvatarAnalysisJson[]) => void;
|
||||||
|
OnUpdateWaveService: (data: UpdateWaveType) => void;
|
||||||
|
onUpdateCycleService: (data: UpdateCycleType) => void;
|
||||||
loadBattleDataFromJSON: (data: BattleDataStateJson) => void;
|
loadBattleDataFromJSON: (data: BattleDataStateJson) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useBattleDataStore = create<BattleDataState>((set, get) => ({
|
const useBattleDataStore = create<BattleDataState>((set, get) => ({
|
||||||
lineup: [],
|
lineup: [],
|
||||||
turnHistory: [],
|
turnHistory: [],
|
||||||
|
skillHistory: [],
|
||||||
|
dataAvatar: [],
|
||||||
totalAV: 0,
|
totalAV: 0,
|
||||||
totalDamage: 0,
|
totalDamage: 0,
|
||||||
damagePerAV: 0,
|
damagePerAV: 0,
|
||||||
|
cycleIndex: 0,
|
||||||
|
waveIndex: 1,
|
||||||
|
maxWave: Infinity,
|
||||||
|
maxCycle: Infinity,
|
||||||
|
|
||||||
loadBattleDataFromJSON: (data: BattleDataStateJson) => {
|
loadBattleDataFromJSON: (data: BattleDataStateJson) => {
|
||||||
set({
|
set({
|
||||||
lineup: data.lineup,
|
lineup: data.lineup,
|
||||||
turnHistory: data.turnHistory,
|
turnHistory: data.turnHistory,
|
||||||
|
skillHistory: data.skillHistory,
|
||||||
|
dataAvatar: data.dataAvatar,
|
||||||
totalAV: data.totalAV,
|
totalAV: data.totalAV,
|
||||||
totalDamage: data.totalDamage,
|
totalDamage: data.totalDamage,
|
||||||
damagePerAV: data.damagePerAV,
|
damagePerAV: data.damagePerAV,
|
||||||
|
cycleIndex: data.cycleIndex,
|
||||||
|
waveIndex: data.waveIndex,
|
||||||
|
maxWave: data.maxWave,
|
||||||
|
maxCycle: data.maxCycle
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
onBattleStartService: () => {
|
onCreateBattleService: (data: AvatarAnalysisJson[]) => {
|
||||||
set({
|
set({
|
||||||
lineup: [],
|
dataAvatar: data
|
||||||
turnHistory: [],
|
})
|
||||||
totalAV: 0,
|
},
|
||||||
totalDamage: 0,
|
onBattleBeginService: (data: BattleBeginType) => {
|
||||||
damagePerAV: 0,
|
const current = get()
|
||||||
|
const updatedHistory = current.turnHistory.map(it => ({
|
||||||
|
...it,
|
||||||
|
cycleIndex: data.max_cycles
|
||||||
|
}))
|
||||||
|
set({
|
||||||
|
maxWave: data.max_waves,
|
||||||
|
maxCycle: data.max_cycles,
|
||||||
|
turnHistory: updatedHistory
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
onDamageService: (data: AttackResultType) => {
|
onDamageService: (data: AttackResultType) => {
|
||||||
const turnHistorys = get().turnHistory
|
const skillHistory = get().skillHistory
|
||||||
const turnIdx = turnHistorys.findLastIndex(it => it.avatarId === data.attacker.id)
|
|
||||||
if (turnIdx === -1) {
|
const skillIdx = skillHistory.findLastIndex(it => it.avatarId === data.attacker.id)
|
||||||
|
if (skillIdx === -1) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const newTh = [...turnHistorys]
|
const newTh = [...skillHistory]
|
||||||
newTh[turnIdx].damageDetail.push(data.damage)
|
newTh[skillIdx].damageDetail.push({damage: data.damage, damage_type: data?.damage_type} as DamageDetailType)
|
||||||
newTh[turnIdx].totalDamage += data.damage
|
newTh[skillIdx].totalDamage += data.damage
|
||||||
set({
|
set({
|
||||||
turnHistory: newTh,
|
skillHistory: newTh,
|
||||||
totalDamage: get().totalDamage + data.damage,
|
totalDamage: get().totalDamage + data.damage,
|
||||||
damagePerAV: (get().totalDamage + data.damage) / (get().totalAV === 0 ? 1 : get().totalAV)
|
damagePerAV: (get().totalDamage + data.damage) / (get().totalAV === 0 ? 1 : get().totalAV)
|
||||||
})
|
})
|
||||||
@@ -79,37 +109,54 @@ const useBattleDataStore = create<BattleDataState>((set, get) => ({
|
|||||||
for (const avatar of data.avatars) {
|
for (const avatar of data.avatars) {
|
||||||
lineups.push({ avatarId: avatar.id, isDie: false } as AvatarBattleInfo)
|
lineups.push({ avatarId: avatar.id, isDie: false } as AvatarBattleInfo)
|
||||||
}
|
}
|
||||||
set({
|
set((state) => ({
|
||||||
lineup: lineups,
|
lineup: lineups,
|
||||||
turnHistory: [],
|
turnHistory: [{
|
||||||
|
avatarId: -1,
|
||||||
|
actionValue: 0,
|
||||||
|
waveIndex: 1,
|
||||||
|
cycleIndex: state.maxCycle,
|
||||||
|
} as TurnBattleInfo],
|
||||||
|
skillHistory: [],
|
||||||
totalAV: 0,
|
totalAV: 0,
|
||||||
totalDamage: 0,
|
totalDamage: 0,
|
||||||
damagePerAV: 0,
|
damagePerAV: 0,
|
||||||
});
|
cycleIndex: state.maxCycle,
|
||||||
|
waveIndex: 1,
|
||||||
|
}));
|
||||||
},
|
},
|
||||||
onTurnBeginService: (data: TurnBeginType) => {
|
onTurnBeginService: (data: TurnBeginType) => {
|
||||||
|
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
totalAV: data.action_value,
|
totalAV: data.action_value,
|
||||||
damagePerAV: state.totalDamage / (data.action_value === 0 ? 1 : data.action_value)
|
damagePerAV: state.totalDamage / (data.action_value === 0 ? 1 : data.action_value),
|
||||||
|
turnHistory: [...state.turnHistory, {
|
||||||
|
avatarId: data?.turn_owner?.id,
|
||||||
|
actionValue: data.action_value,
|
||||||
|
waveIndex: state.waveIndex,
|
||||||
|
cycleIndex: state.cycleIndex
|
||||||
|
} as TurnBattleInfo]
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
onTurnEndService: (data: TurnEndType) => {
|
onTurnEndService: (data: TurnEndType) => {
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
totalDamage: state.totalDamage === data.total_damage ? data.total_damage : state.totalDamage,
|
totalDamage: state.totalDamage === data.turn_info.total_damage ? data.turn_info.total_damage : state.totalDamage,
|
||||||
totalAV: data.action_value,
|
currentAV: data.turn_info.action_value,
|
||||||
damagePerAV: (state.totalDamage === data.total_damage ? data.total_damage : state.totalDamage) / (data.action_value === 0 ? 1 : data.action_value)
|
damagePerAV: (state.totalDamage === data.turn_info.total_damage ? data.turn_info.total_damage : state.totalDamage)
|
||||||
|
/ (data.turn_info.action_value === 0 ? 1 : data.turn_info.action_value)
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
onUseSkillService: (data: AvatarSkillType) => {
|
onUseSkillService: (data: AvatarSkillType) => {
|
||||||
|
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
turnHistory: [...state.turnHistory, {
|
skillHistory: [...state.skillHistory, {
|
||||||
avatarId: data.avatar.id,
|
avatarId: data.avatar.id,
|
||||||
damageDetail: [],
|
damageDetail: [],
|
||||||
totalDamage: 0,
|
totalDamage: 0,
|
||||||
actionValue: state.totalAV,
|
|
||||||
skillType: data.skill.type,
|
skillType: data.skill.type,
|
||||||
skillName: data.skill.name
|
skillName: data.skill.name,
|
||||||
} as TurnBattleInfo]
|
turnBattleId: state.turnHistory.length-1
|
||||||
|
} as SkillBattleInfo]
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
onBattleEndService: (data: BattleEndType) => {
|
onBattleEndService: (data: BattleEndType) => {
|
||||||
@@ -123,6 +170,16 @@ const useBattleDataStore = create<BattleDataState>((set, get) => ({
|
|||||||
totalAV: data.action_value,
|
totalAV: data.action_value,
|
||||||
damagePerAV: data.total_damage / (data.action_value === 0 ? 1 : data.action_value)
|
damagePerAV: data.total_damage / (data.action_value === 0 ? 1 : data.action_value)
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
OnUpdateWaveService: (data: UpdateWaveType) => {
|
||||||
|
set({
|
||||||
|
waveIndex: data.wave
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onUpdateCycleService: (data: UpdateCycleType) => {
|
||||||
|
set({
|
||||||
|
cycleIndex: data.cycle
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,53 @@
|
|||||||
import { AvatarInfo } from "./lineup";
|
import { AvatarType } from "./lineup";
|
||||||
|
|
||||||
export interface AttackResultType {
|
export interface AttackResultType {
|
||||||
attacker: AvatarInfo;
|
attacker: AvatarType;
|
||||||
damage: number;
|
damage: number;
|
||||||
|
damage_type?: AttackType
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DamageDetailType {
|
||||||
|
damage: number;
|
||||||
|
damage_type?: AttackType
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export enum AttackType {
|
||||||
|
Unknown = 0,
|
||||||
|
Normal = 1,
|
||||||
|
BPSkill = 2,
|
||||||
|
Ultra = 3,
|
||||||
|
QTE = 4,
|
||||||
|
DOT = 5,
|
||||||
|
Pursued = 6,
|
||||||
|
Maze = 7,
|
||||||
|
MazeNormal = 8,
|
||||||
|
Insert = 9,
|
||||||
|
ElementDamage = 10,
|
||||||
|
Level = 11,
|
||||||
|
Servant = 12,
|
||||||
|
TrueDamage = 13
|
||||||
|
}
|
||||||
|
|
||||||
|
export function attackTypeToString(type: AttackType | undefined): string {
|
||||||
|
if (type === undefined) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
switch (type) {
|
||||||
|
case AttackType.Unknown: return "Talent";
|
||||||
|
case AttackType.Normal: return "Basic";
|
||||||
|
case AttackType.BPSkill: return "Skill";
|
||||||
|
case AttackType.Ultra: return "Ultimate";
|
||||||
|
case AttackType.QTE: return "QTE";
|
||||||
|
case AttackType.DOT: return "DOT";
|
||||||
|
case AttackType.Pursued: return "Pursued";
|
||||||
|
case AttackType.Maze: return "Technique";
|
||||||
|
case AttackType.MazeNormal: return "MazeNormal";
|
||||||
|
case AttackType.Insert: return "Follow-up";
|
||||||
|
case AttackType.ElementDamage: return "Elemental Damage";
|
||||||
|
case AttackType.Level: return "Level";
|
||||||
|
case AttackType.Servant: return "Servant";
|
||||||
|
case AttackType.TrueDamage: return "True Damage";
|
||||||
|
default: return "Unknown";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
export interface AvatarHakushiType {
|
export interface AvatarHakushiRawType {
|
||||||
release: number;
|
release: number;
|
||||||
icon: string;
|
icon: string;
|
||||||
rank: string;
|
rank: string;
|
||||||
@@ -11,7 +11,7 @@ export interface AvatarHakushiType {
|
|||||||
jp: string;
|
jp: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AvatarType {
|
export interface AvatarHakushiType {
|
||||||
id: string;
|
id: string;
|
||||||
release: number;
|
release: number;
|
||||||
icon: string;
|
icon: string;
|
||||||
|
|||||||
@@ -1,14 +1,21 @@
|
|||||||
import { AvatarInfo } from "./lineup";
|
import { AvatarType } from "./lineup";
|
||||||
import { TurnInfo } from "./turn";
|
import { TurnInfoType } from "./turn";
|
||||||
|
|
||||||
export interface BattleEndType {
|
export interface BattleEndType {
|
||||||
avatars: AvatarInfo[];
|
avatars: AvatarType[];
|
||||||
turn_history: TurnInfo[];
|
turn_history: TurnInfoType[];
|
||||||
|
av_history: TurnInfoType[];
|
||||||
turn_count: number;
|
turn_count: number;
|
||||||
total_damage: number;
|
total_damage: number;
|
||||||
action_value: number;
|
action_value: number;
|
||||||
|
stage_id: number;
|
||||||
}
|
}
|
||||||
export interface KillType {
|
export interface KillType {
|
||||||
attacker: AvatarInfo;
|
attacker: AvatarType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BattleBeginType {
|
||||||
|
max_waves: number
|
||||||
|
max_cycles: number
|
||||||
|
stage_id: number
|
||||||
|
}
|
||||||
@@ -4,3 +4,5 @@ export * from "./battle"
|
|||||||
export * from "./lineup"
|
export * from "./lineup"
|
||||||
export * from "./skill"
|
export * from "./skill"
|
||||||
export * from "./turn"
|
export * from "./turn"
|
||||||
|
export * from "./waveAndCycle"
|
||||||
|
export * from "./srtools"
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
export interface AvatarInfo {
|
export interface AvatarType{
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LineUpType {
|
export interface LineUpType {
|
||||||
avatars: AvatarInfo[];
|
avatars: AvatarType[];
|
||||||
}
|
}
|
||||||
@@ -1,21 +1,37 @@
|
|||||||
|
import {AttackType, DamageDetailType} from "./attack";
|
||||||
|
import { AvatarAnalysisJson } from "./srtools";
|
||||||
|
|
||||||
export interface AvatarBattleInfo {
|
export interface AvatarBattleInfo {
|
||||||
avatarId: number;
|
avatarId: number;
|
||||||
isDie: boolean;
|
isDie: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SkillBattleInfo {
|
||||||
|
avatarId: number;
|
||||||
|
damageDetail: DamageDetailType[];
|
||||||
|
totalDamage: number;
|
||||||
|
skillType: AttackType;
|
||||||
|
skillName: string;
|
||||||
|
turnBattleId: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface TurnBattleInfo {
|
export interface TurnBattleInfo {
|
||||||
avatarId: number;
|
avatarId: number;
|
||||||
damageDetail: number[];
|
|
||||||
totalDamage: number;
|
|
||||||
actionValue: number;
|
actionValue: number;
|
||||||
skillType: string;
|
waveIndex: number;
|
||||||
skillName: string;
|
cycleIndex: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BattleDataStateJson {
|
export interface BattleDataStateJson {
|
||||||
lineup: AvatarBattleInfo[];
|
lineup: AvatarBattleInfo[];
|
||||||
turnHistory: TurnBattleInfo[]
|
turnHistory: TurnBattleInfo[]
|
||||||
|
skillHistory: SkillBattleInfo[]
|
||||||
|
dataAvatar: AvatarAnalysisJson[]
|
||||||
totalAV: number;
|
totalAV: number;
|
||||||
totalDamage: number;
|
totalDamage: number;
|
||||||
damagePerAV: number;
|
damagePerAV: number;
|
||||||
|
maxWave: number;
|
||||||
|
cycleIndex: number,
|
||||||
|
waveIndex: number,
|
||||||
|
maxCycle: number
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
import { AvatarInfo } from "./lineup";
|
import { AvatarType } from "./lineup";
|
||||||
|
import { AttackType } from "@/types/attack";
|
||||||
|
|
||||||
export interface SkillInfo {
|
export interface SkillInfo {
|
||||||
name: string;
|
name: string;
|
||||||
type: string;
|
type: AttackType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AvatarSkillType {
|
export interface AvatarSkillType {
|
||||||
avatar: AvatarInfo;
|
avatar: AvatarType;
|
||||||
skill: SkillInfo;
|
skill: SkillInfo;
|
||||||
}
|
}
|
||||||
42
src/types/srtools.ts
Normal file
42
src/types/srtools.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
export interface AvatarAnalysisJson {
|
||||||
|
owner_uid: number;
|
||||||
|
avatar_id: number;
|
||||||
|
data: AvatarData | null;
|
||||||
|
level: number;
|
||||||
|
promotion: number;
|
||||||
|
techniques: number[];
|
||||||
|
relics: RelicJson[];
|
||||||
|
Lightcone: LightconeJson | null;
|
||||||
|
sp_value: number;
|
||||||
|
sp_max: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AvatarData {
|
||||||
|
rank: number;
|
||||||
|
skills: Record<string, number>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubAffix {
|
||||||
|
sub_affix_id: number;
|
||||||
|
count: number;
|
||||||
|
step: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RelicJson {
|
||||||
|
level: number;
|
||||||
|
relic_id: number;
|
||||||
|
relic_set_id: number;
|
||||||
|
main_affix_id: number;
|
||||||
|
sub_affixes: SubAffix[];
|
||||||
|
internal_uid: number;
|
||||||
|
equip_avatar: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LightconeJson {
|
||||||
|
level: number;
|
||||||
|
item_id: number;
|
||||||
|
equip_avatar: number;
|
||||||
|
rank: number;
|
||||||
|
promotion: number;
|
||||||
|
internal_uid: number;
|
||||||
|
}
|
||||||
@@ -1,19 +1,20 @@
|
|||||||
import { AvatarInfo } from "./lineup";
|
import { AvatarType } from "./lineup";
|
||||||
|
|
||||||
|
|
||||||
export interface TurnInfo {
|
export interface TurnInfoType {
|
||||||
avatars_turn_damage: number[];
|
avatars_turn_damage: number[];
|
||||||
total_damage: number;
|
total_damage: number;
|
||||||
action_value: number;
|
action_value: number,
|
||||||
|
cycle: number,
|
||||||
|
wave: number,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TurnBeginType {
|
export interface TurnBeginType {
|
||||||
action_value: number;
|
action_value: number;
|
||||||
|
turn_owner?: AvatarType
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TurnEndType {
|
export interface TurnEndType {
|
||||||
avatars: AvatarInfo[];
|
avatars: AvatarType[];
|
||||||
avatars_damage: number[];
|
turn_info: TurnInfoType
|
||||||
total_damage: number;
|
|
||||||
action_value: number;
|
|
||||||
}
|
}
|
||||||
7
src/types/waveAndCycle.ts
Normal file
7
src/types/waveAndCycle.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export interface UpdateWaveType {
|
||||||
|
wave: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateCycleType {
|
||||||
|
cycle: number
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user